Проверка параметров умножения на константу в 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/100 в двоичной форме:
0.000000(10100011110101110000)
; - количество ведущих нулей после десятичной точки:
S = 6
; - 72 первых значимых бита:
1010 0011 1101 0111 0000 1010 0011 1101 0111 0000 1010 0011 1101 0111 0000 1010 0011 1101
- круглый до 65 бит; есть какая-то магия в том, как это округление выполняется; путем обратной инженерии константы из ответа Руди правильное округление есть:
1010 0011 1101 0111 0000 1010 0011 1101 0111 0000 1010 0011 1101 0111 0000 1010 1
- снимите ведущий
1
бит:
0100 0111 1010 1110 0001 0100 0111 1010 1110 0001 0100 0111 1010 1110 0001 0101
- напишите в шестнадцатеричной форме (возвращая отомщенную константу):
A = 47 AE 14 7A E1 47 AE 15
-
X div 100 = (((uint128(X) * uint128(A)) shr 64) + X) shr 7
(7 = 1 + S)