Подтвердить что ты не робот

Самый точный способ выполнить комбинированную операцию умножения и деления в 64-битном режиме?

Каков наиболее точный способ выполнения операции умножения и деления для 64-разрядных целых чисел, которые работают как в 32-разрядных, так и в 64-разрядных программах (в Visual С++)? (В случае переполнения мне нужен результат mod 2 64.)

(Я ищу что-то вроде MulDiv64, за исключением того, что этот использует встроенную сборку, которая работает только в 32-битных программах.)

Очевидно, что при нажатии на double и обратно возможно, но мне интересно, есть ли более точный способ, который не слишком сложный. (т.е. я не ищу здесь арифметические библиотеки произвольной точности!)

4b9b3361

Ответ 1

Так как это помечено Visual С++, я дам решение, которое нарушает специфические для MSVC встроенные функции.

Этот пример довольно сложный. Это очень упрощенная версия того же алгоритма, который используется GMP и java.math.BigInteger для большого деления.

Хотя у меня есть более простой алгоритм, он, вероятно, примерно на 30 раз медленнее.

Это решение имеет следующие ограничения/поведение:

  • Для этого требуется x64. Он не будет компилироваться на x86.
  • Фактор не равен нулю.
  • Фактор насыщается, если он переполняет 64-разрядные файлы.

Обратите внимание, что это для целых чисел без знака. Это тривиально, чтобы создать обертку вокруг этого, чтобы он работал и для подписанных случаев. Этот пример также должен давать правильно усеченные результаты.

Этот код не полностью протестирован. Однако он прошел все те тесты, которые я на него набросил.
(Даже случаи, которые я намеренно сконструировал, чтобы попытаться сломать алгоритм.)

#include <intrin.h>

uint64_t muldiv2(uint64_t a, uint64_t b, uint64_t c){
    //  Normalize divisor
    unsigned long shift;
    _BitScanReverse64(&shift,c);
    shift = 63 - shift;

    c <<= shift;

    //  Multiply
    a = _umul128(a,b,&b);
    if (((b << shift) >> shift) != b){
        cout << "Overflow" << endl;
        return 0xffffffffffffffff;
    }
    b = __shiftleft128(a,b,shift);
    a <<= shift;


    uint32_t div;
    uint32_t q0,q1;
    uint64_t t0,t1;

    //  1st Reduction
    div = (uint32_t)(c >> 32);
    t0 = b / div;
    if (t0 > 0xffffffff)
        t0 = 0xffffffff;
    q1 = (uint32_t)t0;
    while (1){
        t0 = _umul128(c,(uint64_t)q1 << 32,&t1);
        if (t1 < b || (t1 == b && t0 <= a))
            break;
        q1--;
//        cout << "correction 0" << endl;
    }
    b -= t1;
    if (t0 > a) b--;
    a -= t0;

    if (b > 0xffffffff){
        cout << "Overflow" << endl;
        return 0xffffffffffffffff;
    }

    //  2nd reduction
    t0 = ((b << 32) | (a >> 32)) / div;
    if (t0 > 0xffffffff)
        t0 = 0xffffffff;
    q0 = (uint32_t)t0;

    while (1){
        t0 = _umul128(c,q0,&t1);
        if (t1 < b || (t1 == b && t0 <= a))
            break;
        q0--;
//        cout << "correction 1" << endl;
    }

//    //  (a - t0) gives the modulus.
//    a -= t0;

    return ((uint64_t)q1 << 32) | q0;
}

Обратите внимание, что если вам не нужен абсолютно усеченный результат, вы можете полностью удалить последний цикл. Если вы сделаете это, ответ будет не больше, чем на 2 больше, чем правильный коэффициент.

Тестовые случаи:

cout << muldiv2(4984198405165151231,6132198419878046132,9156498145135109843) << endl;
cout << muldiv2(11540173641653250113, 10150593219136339683, 13592284235543989460) << endl;
cout << muldiv2(449033535071450778, 3155170653582908051, 4945421831474875872) << endl;
cout << muldiv2(303601908757, 829267376026, 659820219978) << endl;
cout << muldiv2(449033535071450778, 829267376026, 659820219978) << endl;
cout << muldiv2(1234568, 829267376026, 1) << endl;
cout << muldiv2(6991754535226557229, 7798003721120799096, 4923601287520449332) << endl;
cout << muldiv2(9223372036854775808, 2147483648, 18446744073709551615) << endl;
cout << muldiv2(9223372032559808512, 9223372036854775807, 9223372036854775807) << endl;
cout << muldiv2(9223372032559808512, 9223372036854775807, 12) << endl;
cout << muldiv2(18446744073709551615, 18446744073709551615, 9223372036854775808) << endl;

Вывод:

3337967539561099935
8618095846487663363
286482625873293138
381569328444
564348969767547451
1023786965885666768
11073546515850664288
1073741824
9223372032559808512
Overflow
18446744073709551615
Overflow
18446744073709551615

Ответ 2

Вам просто нужны 64-битные целые числа. Есть несколько избыточных операций, но это позволяет использовать 10 в качестве базы и шаг в отладчике.

uint64_t const base = 1ULL<<32;
uint64_t const maxdiv = (base-1)*base + (base-1);

uint64_t multdiv(uint64_t a, uint64_t b, uint64_t c)
{
    // First get the easy thing
    uint64_t res = (a/c) * b + (a%c) * (b/c);
    a %= c;
    b %= c;
    // Are we done?
    if (a == 0 || b == 0)
        return res;
    // Is it easy to compute what remain to be added?
    if (c < base)
        return res + (a*b/c);
    // Now 0 < a < c, 0 < b < c, c >= 1ULL
    // Normalize
    uint64_t norm = maxdiv/c;
    c *= norm;
    a *= norm;
    // split into 2 digits
    uint64_t ah = a / base, al = a % base;
    uint64_t bh = b / base, bl = b % base;
    uint64_t ch = c / base, cl = c % base;
    // compute the product
    uint64_t p0 = al*bl;
    uint64_t p1 = p0 / base + al*bh;
    p0 %= base;
    uint64_t p2 = p1 / base + ah*bh;
    p1 = (p1 % base) + ah * bl;
    p2 += p1 / base;
    p1 %= base;
    // p2 holds 2 digits, p1 and p0 one

    // first digit is easy, not null only in case of overflow
    uint64_t q2 = p2 / c;
    p2 = p2 % c;

    // second digit, estimate
    uint64_t q1 = p2 / ch;
    // and now adjust
    uint64_t rhat = p2 % ch;
    // the loop can be unrolled, it will be executed at most twice for
    // even bases -- three times for odd one -- due to the normalisation above
    while (q1 >= base || (rhat < base && q1*cl > rhat*base+p1)) {
        q1--;
        rhat += ch;
    }
    // subtract 
    p1 = ((p2 % base) * base + p1) - q1 * cl;
    p2 = (p2 / base * base + p1 / base) - q1 * ch;
    p1 = p1 % base + (p2 % base) * base;

    // now p1 hold 2 digits, p0 one and p2 is to be ignored
    uint64_t q0 = p1 / ch;
    rhat = p1 % ch;
    while (q0 >= base || (rhat < base && q0*cl > rhat*base+p0)) {
        q0--;
        rhat += ch;
    }
    // we don't need to do the subtraction (needed only to get the remainder,
    // in which case we have to divide it by norm)
    return res + q0 + q1 * base; // + q2 *base*base
}

Ответ 3

Это ответ вики сообщества, поскольку это действительно просто куча указателей на другие документы/ссылки (я не могу опубликовать соответствующий код).

Умножение двух 64-битных ints на 128-битный результат довольно легко, используя прямое применение карандаша и бумажной техники, которые каждый изучает в начальной школе.

Комментарий GregS верен: в разделе "Искусство компьютерного программирования, второе издание, том 2/" Семинумерные алгоритмы "в конце раздела 4.3.1" Множество прецизионных арифметических/классических алгоритмов "(стр. 255 - 265) копия). Это нелегко прочитать, по крайней мере, не для кого-то вроде меня, который забыл большинство математик за пределами алгебры 7-го класса. Как раз перед, Кнут также охватывает сторону умножения вещей.

Некоторые другие варианты идей (эти примечания предназначены для алгоритмов деления, но большинство также обсуждает умножение):

  • Джек Креншоу раскрывает алгоритмы деления Кнута более читаемым образом в серии статей из журнала Embedded System Programming 1997 (к сожалению, в моих заметках нет точных проблем). К сожалению, статьи из старых вопросов ESP нелегко найти в Интернете. Если у вас есть доступ к университетской библиотеке, возможно, вам понадобятся некоторые проблемы с обратной связью или копия библиотеки CD-ROM ESP.
  • Томас Родеффер из исследования Microsoft опубликовал статью о подразделении Software Integer: http://research.microsoft.com/pubs/70645/tr-2008-141.pdf
  • Статья Карла Хассельстрема "Быстрое разделение больших целых чисел": http://www.treskal.com/kalle/exjobb/original-report.pdf
  • Randall Hyde "Искусство языка ассемблера" (http://webster.cs.ucr.edu/AoA/Windows/HTML/AoATOC.html), в частности раздел четвертый раздел 4.2.5 (расширенный прецизионный отдел): http://webster.cs.ucr.edu/AoA/Windows/HTML/AdvancedArithmetica2.html#998729, это вариант Hyde для ассемблера x86, но также есть псевдокод и достаточно объяснений для переноса алгоритма на C. Это тоже медленное - выполнение бит-по-бит...

Ответ 4

Для этого вам не нужна арифметика произвольной точности. Вам нужна только 128-разрядная арифметика. То есть вам нужно 64 * 64 = 128 умножения и 128/64 = 64 деления (с надлежащим поведением переполнения). Это не так сложно реализовать вручную.

Ответ 5

Хорошо, вы можете нарезать 64-разрядные операнды на 32-битные куски (низкая и высокая часть). Затем сделайте операцию, которую вы хотите. Все промежуточные результаты будут меньше 64 бит и поэтому могут храниться в типах данных, которые у вас есть.

Ответ 6

У вас есть тип COMP (64-разрядный целочисленный тип на основе x87) в вашем распоряжении в VС++? Я использовал его иногда в Delphi в прошлом, когда мне нужна 64-битная целочисленная математика. В течение многих лет он был быстрее, чем библиотечная 64-битная целочисленная математика - конечно, когда было задействовано подразделение.

В Delphi 2007 (последнее, что я установил - 32 бита), я бы реализовал MulDiv64 следующим образом:

function MulDiv64(const a1, a2, a3: int64): int64;
var
  c1: comp absolute a1;
  c2: comp absolute a2;
  c3: comp absolute a3;
  r: comp absolute result;
begin
  r := c1*c2/c3;
end;

(Эти странные абсолютные инструкции выравнивают переменные comp поверх своих 64-разрядных целочисленных счетных частей. Я бы использовал простые типы приведения, за исключением того, что компилятор Delphi запутался в этом - возможно, потому, что язык Delphi (или что-то, что он называет теперь) не имеет четкого синтаксического различия между типом casting (reinterpret) и преобразованием типа значения.)

В любом случае, Delphi 2007 делает следующее:

0046129C 55               push ebp
0046129D 8BEC             mov ebp,esp
0046129F 83C4F8           add esp,-$08

004612A2 DF6D18           fild qword ptr [ebp+$18]
004612A5 DF6D10           fild qword ptr [ebp+$10]
004612A8 DEC9             fmulp st(1)
004612AA DF6D08           fild qword ptr [ebp+$08]
004612AD DEF9             fdivp st(1)
004612AF DF7DF8           fistp qword ptr [ebp-$08]
004612B2 9B               wait 

004612B3 8B45F8           mov eax,[ebp-$08]
004612B6 8B55FC           mov edx,[ebp-$04]
004612B9 59               pop ecx
004612BA 59               pop ecx
004612BB 5D               pop ebp
004612BC C21800           ret $0018

Следующий оператор дает 256204778801521550, который выглядит правильно.

writeln(MulDiv64($aaaaaaaaaaaaaaa, $555555555555555, $1000000000000000));

Если вы хотите реализовать это как встроенную сборку VС++, возможно, вам понадобится выполнить некоторую настройку флажков округления по умолчанию, чтобы выполнить одно и то же, я не знаю - у меня не было необходимости узнайте - пока:)

Ответ 7

Для 64-битного режима кода вы можете реализовать умножение 64 * 64 = 128 аналогично реализации 128/64 = 64: 64 раздела здесь.

Для 32-битного кода он будет более сложным, потому что нет инструкции по процессору, которая будет делать умножение или деление таких длинных операндов в 32-битном режиме, и вам придется объединить несколько меньших умножений в более крупный и переопределить длинное разделение.

Вы можете использовать код этого ответа в качестве основы для построения длинного разделения.

Конечно, если ваши разделители всегда меньше 2 32 (или еще лучше 2 16), вы можете сделать гораздо более быстрое деление в 32-битном режиме путем цепочки несколько делений (разделите самые значительные 64 (или 32) бита дивиденда на 32-разрядный (или 16-разрядный) делитель, затем объедините остаток от этого деления со следующими 64 (32) битами дивиденда и разделите это по делителю и продолжайте делать это, пока не будете использовать весь дивиденд). Кроме того, если делитель большой, но может быть учтен в достаточно малые числа, деление на его коэффициенты с использованием этого цепного деления будет лучше, чем классическое решение петли.

Ответ 8

Здесь метод аппроксимации, который вы можете использовать: (полная точность, если a > 0x7FFFFFFF или b > 0x7FFFFFFF и c больше, чем a или b)

constexpr int64_t muldiv(int64_t a, int64_t b, int64_t c, unsigned n = 0) {
  return (a < 0x7FFFFFFF && b < 0x7FFFFFFF) ? (a * b) / c : (n != 2) ? (c <= a) ? ((a / c) * b + muldiv(b, a % c, c, n + 1)) : muldiv(a, b, c / 2) / 2 : 0;
}

Модуль используется для поиска потери точности, которая затем снова включается в функцию. Это похоже на алгоритм классического деления.

2 было выбрано потому, что (x % x) % x = x % x.

Ответ 9

В C нет 32x32→64 или 64x64→128 умножений/делений. Результат всегда имеет тот же размер, что и самый большой из множителей и множитель. Это означает, что только int32 x int32 → int32 и int64 x int64 → int64. В случае переполнения результатом являются младшие биты. То же самое для делений. Таким образом, на самом деле вам нужно объявить переменные как __int64 или передать один из множителей/делителей в __int64, и результат будет ниже 64 бит по мере необходимости.

__int64 a, b, c;
c = a * b;
__int32 d, e;
b = __int64(e) / d;

Ответ 10

Считайте, что вы хотите умножить a на b, а затем разделите на d:

uint64_t LossyMulDiv64(uint64_t a, uint64_t b, uint64_t d)
{
    long double f = long double(b)/d;
    uint64_t highPart = uint64_t((a & ~0xffffffff) * f + 0.5);
    uint64_t lowPart = uint64_t((a & 0xffffffff) * f + 0.5);
    return highPart + lowPart;
}

Этот код разбивает значение a на более высокие и более низкие 32-битные части, а затем умножает 32-битные части отдельно на 52-битное точное отношение b до d, округляет частичные умножения и суммирует их обратно на целое число. Некоторая точность все еще теряется, но результат более точен, чем просто return a * double(b) / d;.