Умножение между большими целыми числами и удвоениями

Я управляю некоторыми большими (128 ~ 256 бит) целыми числами с помощью gmp. Настал момент, когда я хотел бы умножить их на двойное число, близкое к 1 (0,1 ‹ двойное ‹ 10), при этом результат все равно будет приближенным целым числом. Хорошим примером операции, которую мне нужно сделать, является следующее:

int i = 1000000000000000000 * 1.23456789

Я искал в документации gmp, но не нашел для этого функции, поэтому в итоге я написал этот код, который, кажется, работает хорошо:

mpz_mult_d(mpz_class & r, const mpz_class & i, double d, int prec=10) {
  if (prec > 15) prec=15; //avoids overflows
  uint_fast64_t m = (uint_fast64_t) floor(d);
  r = i * m;
  uint_fast64_t pos=1;
  for (uint_fast8_t j=0; j<prec; j++) {
    const double posd = (double) pos;
    m = ((uint_fast64_t) floor(d * posd * 10.)) -
        ((uint_fast64_t) floor(d * posd)) * 10;
    pos*=10;
    r += (i * m) /pos;
  }
}

Не могли бы вы сказать мне, что вы думаете? Есть ли у вас какие-либо предложения, чтобы сделать его более надежным или быстрым?


person DarioP    schedule 27.05.2013    source источник
comment
Это вопрос для Code Review, а не для StackOverflow :)   -  person Morwenn    schedule 27.05.2013
comment
Ой, извините, я не знал об этой ветке. Однако это всего лишь мое решение очень точной проблемы. Пожалуйста, подумайте над тем, чтобы ответить на общий вопрос и только в конечном итоге прокомментировать код. Спасибо!   -  person DarioP    schedule 27.05.2013
comment
если вам нужно приближение, почему бы вам не преобразовать большое целое число в двойное?   -  person Karoly Horvath    schedule 27.05.2013
comment
@KarolyHorvath: потому что при этом я сжимаю 256 бит целого числа в 52 бита двойной мантиссы. Это полностью тратит впустую использование ints! Можно было бы преобразовать большое целое в большое число с плавающей запятой, но я думаю, что это проблематично с точки зрения размера, скорости и точности.   -  person DarioP    schedule 27.05.2013
comment
И что? Двойное число, на которое вы умножаете, также не имеет большей точности. Если вам нужна точность, используйте большое число с плавающей запятой или выполните умножение на 123456789 (сохраните его как десятичное число)   -  person Karoly Horvath    schedule 27.05.2013
comment
@KarolyHorvath это более или менее то, что я делаю в коде, но пытаюсь сохранить разумное использование памяти. У Double не должно быть проблем с обработкой таких значений, как 1.23456789, если я просто посмотрю на первые 10 или что-то в этом роде. PS. Мне нужен двойник, потому что он исходит из каких-то неприятных физических величин.   -  person DarioP    schedule 27.05.2013
comment
GMP поддерживает рациональные числа произвольной точности и числа с плавающей запятой. Преобразуйте два ваших значения в один из этих форматов и позвольте GMP выполнить умножение.   -  person brian beuning    schedule 27.05.2013
comment
Если это исходит из физических величин (вы имеете в виду измерения? что-то еще?), разве это уже не приближение? Я имею в виду, что даже если значение 1,23456789 может быть представлено точно, это не точная величина, а просто приближение с такой точностью и точностью, которую обеспечивает измерительное устройство.   -  person R. Martinho Fernandes    schedule 27.05.2013
comment
Вам следует обратиться к MPFR (mpfr.org), который является эквивалентом GMP для чисел с плавающей запятой. Это так же легко/просто в использовании, как и GMP.   -  person Vincent    schedule 12.07.2013


Ответы (3)


это то, что вы хотели:

// BYTE lint[_N]   ... lint[0]=MSB, lint[_N-1]=LSB
void mul(BYTE *c,BYTE *a,double b)  // c[_N]=a[_N]*b
    {
    int i; DWORD cc;
    double q[_N+1],aa,bb;
    for (q[0]=0.0,i=0;i<_N;)        // mul,carry down
        {
        bb=double(a[i])*b; aa=floor(bb); bb-=aa;
        q[i]+=aa; i++;
        q[i]=bb*256.0;
        }
    cc=0; if (q[_N]>127.0) cc=1.0;  // round
    for (i=_N-1;i>=0;i--)           // carry up
        {
        double aa,bb;
        cc+=q[i];
        c[i]=cc&255;
        cc>>=8;
        }
    }

_N — это число битов/8 на большое целое, большое целое — это массив из _N BYTE, где первый байт — MSB (самый значащий BYTE), а последний BYTE — младший значащий BYTE (младший значащий BYTE) функция не обрабатывает сигнум, но это только один, если и немного xor/inc для добавления.

проблема в том, что double имеет низкую точность даже для вашего числа 1.23456789 !!! из-за потери точности результат не совсем такой, каким он должен быть (1234387129122386944 вместо 1234567890000000000). Я думаю, что мой код намного быстрее и даже точнее, чем ваш, потому что мне не нужно число mul/mod/div на 10, вместо этого я использую битовый сдвиг там, где это возможно, и не на 10 разрядов, а на 256 разрядов (8 бит). если вам нужна большая точность, чем использовать длинную арифметику. вы можете ускорить этот код, используя большие цифры (16,32, ... бит)

Моя длинная арифметика для точных астрономических вычислений обычно представляет собой 256,256-битные числа с фиксированной точкой, состоящие из 2 * 8 DWORD + signum, но, конечно, это намного медленнее, и некоторые гониометрические функции действительно сложно реализовать, но если вам нужны только базовые функции, чем кодируйте свой собственный лон арифметика не так уж сложна.

также, если вы хотите, чтобы числа часто были в читаемой форме, хорошо найти компромисс между скоростью/размером и рассмотреть возможность использования не двоично-кодированных чисел, а двоично-десятичных кодированных чисел.

person Spektre    schedule 04.08.2013

Я не настолько знаком ни с C++, ни с GMP, чтобы предложить исходный код без синтаксических ошибок, но то, что вы делаете, сложнее, чем должно, и может ввести ненужное приближение.

Вместо этого я предлагаю вам написать функцию mpz_mult_d() следующим образом:

mpz_mult_d(mpz_class & r, const mpz_class & i, double d) {
  d = ldexp(d, 52); /* exact, no overflow because 1 <= d <= 10 */
  unsigned long long l = d; /* exact because d is an integer */
  p = l * i; /* exact, in GMP */
  (quotient, remainder) = p / 2^52; /* in GMP */

И теперь следующий шаг зависит от того, какое округление вы хотите. Если вы хотите, чтобы умножение d на i дало результат, округленный в сторону -inf, просто верните quotient в качестве результата функции. Если вы хотите, чтобы результат был округлен до ближайшего целого числа, вы должны посмотреть на remainder:

 assert(0 <= remainder);  /* proper Euclidean division */
 assert(remainder < 2^52);
 if (remainder < 2^51) return quotient;
 if (remainder > 2^51) return quotient + 1; /* in GMP */
 if (remainder == 2^51) return quotient + (quotient & 1); /* in GMP, round to “even” */

PS: я нашел ваш вопрос случайным образом, но если бы вы пометили его как «с плавающей запятой», люди, более компетентные, чем я, могли бы быстро ответить на него.

person Pascal Cuoq    schedule 05.08.2013

Попробуйте эту стратегию:

  1. Преобразование целочисленного значения в большое число с плавающей запятой
  2. Преобразовать двойное значение в большое число с плавающей запятой
  3. Сделать продукт
  4. Преобразовать результат в целое число

    mpf_set_z(...)
    mpf_set_d(...)
    mpf_mul(...)
    mpz_set_f(...)
    
person GeScript    schedule 25.01.2018