Проверка параметров умножения на константу в 64 бит

для моего кода BigInteger выход оказался медленным для очень больших BigIntegers. Итак, теперь я использую рекурсивный алгоритм "разделяй и властвуй", которому все еще нужно 2'30" для преобразования самого большого известного простого числа в десятичную строку из более чем 22 миллионов цифр (но только 135 мс, чтобы превратить его в шестнадцатеричную строку).

Я все еще хочу сократить время, поэтому мне нужна процедура, которая может разделить NativeUInt (т. е. UInt32 на 32-битных платформах, UInt64 на 64-битных платформах) на 100 очень быстрый. Поэтому я использую умножение на константу. Это отлично работает в 32-битном коде, но я не уверен на 100% для 64 бит.

Итак, мой вопрос: есть ли способ проверить надежность результатов умножения на константу для беззнаковых 64-битных значений? Я проверил 32-битные значения, просто попробовав со всеми значениями UInt32 (0..$FFFFFFFF). Это заняло ок. 3 минуты. Проверив все UInt64s уйдет намного больше, чем свою жизнь. Есть ли способ проверить, используются ли параметры (константа, post-shift) надежны?

Я заметил, что DivMod100() всегда не за значение 00004B если выбранные параметры были неправильными (но близкими). Есть ли специальные значения или диапазоны для проверки 64 бит, поэтому мне не нужно проверять все значения?

мой текущий код:

const
{$IF DEFINED(WIN32)}
  // Checked
  Div100Const = UInt32(UInt64(FFFFFFFFF) div 100 + 1);
  Div100PostShift = 5;
{$ELSEIF DEFINED(WIN64)}
  // Unchecked!!
  Div100Const = $A3D70A3D70A3D71; 
  // UInt64(UInt128( FFFF FFFF FFFF FFFF) div 100 + 1); 
  // UInt128 is fictive type.
  Div100PostShift = 2;
{$IFEND}

// Calculates X div 100 using multiplication by a constant, taking the
// high part of the 64 bit (or 128 bit) result and shifting
// right. The remainder is calculated as X - quotient * 100;
// This was tested to work safely and quickly for all values of UInt32.
function DivMod100(var X: NativeUInt): NativeUInt;
{$IFDEF WIN32}
asm
        // EAX = address of X, X is UInt32 here.
        PUSH    EBX
        MOV     EDX,Div100Const
        MOV     ECX,EAX
        MOV     EAX,[ECX]
        MOV     EBX,EAX
        MUL     EDX
        SHR     EDX,Div100PostShift
        MOV     [ECX],EDX       // Quotient

        // Slightly faster than MUL

        LEA     EDX,[EDX + 4*EDX] // EDX := EDX * 5;
        LEA     EDX,[EDX + 4*EDX] // EDX := EDX * 5;
        SHL     EDX,2             // EDX := EDX * 4; 5*5*4 = 100.

        MOV     EAX,EBX
        SUB     EAX,EDX         // Remainder
        POP     EBX
end;
{$ELSE WIN64}
asm
        .NOFRAME

        // RCX is address of X, X is UInt64 here.
        MOV     RAX,[RCX]
        MOV     R8,RAX
        XOR     RDX,RDX
        MOV     R9,Div100Const
        MUL     R9
        SHR     RDX,Div100PostShift
        MOV     [RCX],RDX      // Quotient

        // Faster than LEA and SHL

        MOV     RAX,RDX
        MOV     R9D,100
        MUL     R9
        SUB     R8,RAX
        MOV     RAX,R8         // Remainder
end;
{$ENDIF WIN32}

3 ответов


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

gcc реализует unsigned 64bit divmod с константой 0x28f5c28f5c28f5c3. Я не рассматривал подробно генерацию констант для деления, но есть алгоритмы для их генерации, которые дадут известные хорошие результаты (поэтому исчерпывающее тестирование не является необходимый.)

код на самом деле имеет несколько важных отличий: он использует константу иначе, чем константа OP.

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

#include <stdint.h>

// rem, quot ordering takes one extra instruction
struct divmod { uint64_t quotient, remainder; }
 div_by_100(uint64_t x) {
    struct divmod retval = { x%100, x/100 };
    return retval;
}

компилируется в (gcc 5.3 -O3 -mtune=haswell):

    movabs  rdx, 2951479051793528259
    mov     rax, rdi            ; Function arg starts in RDI (SysV ABI)
    shr     rax, 2
    mul     rdx
    shr     rdx, 2
    lea     rax, [rdx+rdx*4]    ; multiply by 5
    lea     rax, [rax+rax*4]    ; multiply by another 5
    sal     rax, 2              ; imul rax, rdx, 100 is better here (Intel SnB).
    sub     rdi, rax
    mov     rax, rdi
    ret
; return values in rdx:rax

используйте опцию "binary", чтобы увидеть константу в hex, так как выход дизассемблера делает это таким образом, в отличие от выхода источника asm gcc.


часть умножить на 100.

gcc использует вышеуказанную последовательность lea/lea / shl, такую же, как в вашем вопросе. Ваш ответ использует mov imm/mul последовательности.

ваши комментарии каждой версии они выбрали быстрее. Если так, то это из-за некоторых тонких выравнивание инструкций или другой вторичный эффект: на Intel SnB-family это столько же uops (3), и та же задержка критического пути (mov imm находится вне критического пути, и mul есть 3 цикла).

clang использует что я думаю это лучший вариант (imul rax, rdx, 100). Я подумал об этом прежде, чем увидел, что клэнг выбрал его, Хотя это не имеет значения. Это 1 плавленый домен uop (который может выполняться только на p0), все еще с задержкой 3c. Поэтому, если вы связаны с задержкой используя эту процедуру для мульти-точности, это, вероятно, не поможет, но это лучший выбор. (Если вы привязаны к задержке, вставка кода в цикл вместо передачи одного из параметров через память может сэкономить много циклов.)

imul работает, потому что вы используете только низкий 64b результата. Нет 2 или 3 операнд формы mul потому что низкая половина результата одинакова независимо от подписанной или неподписанной интерпретации входной.

кстати, с лязгом -march=native использует mulx для 64x64 - > 128, вместо mul, но ничего не выигрывает от этого. Согласно таблицам Agner Fog, это на один цикл хуже, чем mul.


AMD имеет хуже, чем задержка 3c для imul r,r,i (esp. версия 64b), возможно, поэтому gcc избегает ее. IDK сколько работы GCC сопровождающих положить в настройки затрат так Настройки, как -mtune=haswell работать хорошо, но много код не компилируется с -mtune настройка (даже подразумеваемого -march), поэтому я не удивлен, когда gcc делает выбор, который был оптимальным для старых процессоров или для AMD.

clang все еще использует imul r64, r64, imm С -mtune=bdver1 (бульдозер), что экономит m-ops, но при стоимости задержки 1c больше, чем при использовании lea/lea/shl. (lea со шкалой>1-задержка 2C на бульдозере).


Я нашел решение с libdivide.h. Вот немного более сложная часть для Win64:

{$ELSE WIN64}
asm
        .NOFRAME

        MOV     RAX,[RCX]
        MOV     R8,RAX
        XOR     RDX,RDX
        MOV     R9,Div100Const       // New: AE147AE147AE15
        MUL     R9                   // Preliminary result Q in RDX

        // Additional part: add/shift

        ADD     RDX,R8               // Q := Q + X shr 1;
        RCR     RDX,1

        SHR     RDX,Div100PostShift  // Q := Q shr 6;
        MOV     [RCX],RDX            // X := Q;

        // Faster than LEA and SHL

        MOV     RAX,RDX
        MOV     R9D,100
        MUL     R9
        SUB     R8,RAX
        MOV     RAX,R8         // Remainder
end;
{$ENDIF WIN32}

код в ответе @Rudy приводит к следующим шагам:

  1. напишите 1/100 в двоичной форме:0.000000(10100011110101110000);
  2. количество ведущих нулей после десятичной точки:S = 6;
  3. 72 первых значимых бита:

1010 0011 1101 0111 0000 1010 0011 1101 0111 0000 1010 0011 1101 0111 0000 1010 0011 1101

  1. круглый до 65 бит; есть какая-то магия в том, как это округление выполняется; путем обратной инженерии константы из ответа Руди правильное округление есть:

1010 0011 1101 0111 0000 1010 0011 1101 0111 0000 1010 0011 1101 0111 0000 1010 1

  1. снимите ведущий 1 бит:

0100 0111 1010 1110 0001 0100 0111 1010 1110 0001 0100 0111 1010 1110 0001 0101

  1. напишите в шестнадцатеричной форме (возвращая отомщенную константу):

A = 47 AE 14 7A E1 47 AE 15

  1. X div 100 = (((uint128(X) * uint128(A)) shr 64) + X) shr 7 (7 = 1 + S)