Я пишу простой тип BigInteger в Delphi. Он в основном состоит из динамического массива TLimb, где TLimb - это 32-разрядное целое без знака и поле размера 32 бит, которое также содержит бит знака для BigInteger.
Чтобы добавить два BigIntegers, я создаю новый BigInteger соответствующего размера, а затем, после некоторой бухгалтерской работы, вызовите следующую процедуру, передав ей три указателя на соответствующие запуски массивов для левого и правого операнда и результата, а также количество конечностей для левого и правого, соответственно.
Обычный код:
class procedure BigInteger.PlainAdd(Left, Right, Result: PLimb; LSize, RSize: Integer);
asm
// EAX = Left, EDX = Right, ECX = Result
PUSH ESI
PUSH EDI
PUSH EBX
MOV ESI,EAX // Left
MOV EDI,EDX // Right
MOV EBX,ECX // Result
MOV ECX,RSize // Number of limbs at Left
MOV EDX,LSize // Number of limbs at Right
CMP EDX,ECX
JAE @SkipSwap
XCHG ECX,EDX // Left and LSize should be largest
XCHG ESI,EDI // so swap
@SkipSwap:
SUB EDX,ECX // EDX contains rest
PUSH EDX // ECX contains smaller size
XOR EDX,EDX
@MainLoop:
MOV EAX,[ESI + CLimbSize*EDX] // CLimbSize = SizeOf(TLimb) = 4.
ADC EAX,[EDI + CLimbSize*EDX]
MOV [EBX + CLimbSize*EDX],EAX
INC EDX
DEC ECX
JNE @MainLoop
POP EDI
INC EDI // Do not change Carry Flag
DEC EDI
JE @LastLimb
@RestLoop:
MOV EAX,[ESI + CLimbSize*EDX]
ADC EAX,ECX
MOV [EBX + CLimbSize*EDX],EAX
INC EDX
DEC EDI
JNE @RestLoop
@LastLimb:
ADC ECX,ECX // Add in final carry
MOV [EBX + CLimbSize*EDX],ECX
@Exit:
POP EBX
POP EDI
POP ESI
end;
// RET is inserted by Delphi compiler.
Этот код работал хорошо, и я был довольно удовлетворен этим, пока не заметил, что в моей настройке разработки (Win7 в Parallels VM на iMac) простая процедура добавления PURE PASCAL, делая то же самое при эмуляции переноса с помощью переменная и несколько предложений if
, были быстрее, чем моя простая, простая ручная процедура ассемблера вручную.
Мне потребовалось некоторое время, чтобы узнать, что на некоторых процессорах (включая мой iMac и старый ноутбук) комбинация DEC
или INC
и ADC
или SBB
может быть очень медленной. Но на большинстве моих других (у меня есть пять других ПК, чтобы проверить его, хотя четыре из них точно такие же), это было довольно быстро.
Итак, я написал новую версию, эмуляцию INC
и DEC
вместо LEA
и JECXZ
, например:
Часть эмулирующего кода:
@MainLoop:
MOV EAX,[ESI + EDX*CLimbSize]
LEA ECX,[ECX - 1] // Avoid INC and DEC, see above.
ADC EAX,[EDI + EDX*CLimbSize]
MOV [EBX + EDX*CLimbSize],EAX
LEA EDX,[EDX + 1]
JECXZ @DoRestLoop // LEA does not modify Zero flag, so JECXZ is used.
JMP @MainLoop
@DoRestLoop:
// similar code for the rest loop
Это сделало мой код на "медленных" машинах почти в три раза быстрее, но на 20% медленнее на "быстрых" машинах. Итак, теперь, как код инициализации, я делаю простой цикл синхронизации и использую это, чтобы решить, настрою ли я устройство для вызова простой или эмулируемой подпрограммы. Это почти всегда правильно, но иногда он выбирает (более медленные) обычные процедуры, когда он должен выбирать подражательные подпрограммы.
Но я не знаю, это лучший способ сделать это.
Вопрос
Я дал свое решение, но возможно ли, что гуру asm знают, что лучший способ избежать медленности на некоторых процессорах?
Update
Ответы Питера и Нилса помогли мне многого на правильном пути. Это основная часть моего окончательного решения для версии DEC
:
Обычный код:
class procedure BigInteger.PlainAdd(Left, Right, Result: PLimb; LSize, RSize: Integer);
asm
PUSH ESI
PUSH EDI
PUSH EBX
MOV ESI,EAX // Left
MOV EDI,EDX // Right
MOV EBX,ECX // Result
MOV ECX,RSize
MOV EDX,LSize
CMP EDX,ECX
JAE @SkipSwap
XCHG ECX,EDX
XCHG ESI,EDI
@SkipSwap:
SUB EDX,ECX
PUSH EDX
XOR EDX,EDX
XOR EAX,EAX
MOV EDX,ECX
AND EDX,$00000003
SHR ECX,2
CLC
JE @MainTail
@MainLoop:
// Unrolled 4 times. More times will not improve speed anymore.
MOV EAX,[ESI]
ADC EAX,[EDI]
MOV [EBX],EAX
MOV EAX,[ESI + CLimbSize]
ADC EAX,[EDI + CLimbSize]
MOV [EBX + CLimbSize],EAX
MOV EAX,[ESI + 2*CLimbSize]
ADC EAX,[EDI + 2*CLimbSize]
MOV [EBX + 2*CLimbSize],EAX
MOV EAX,[ESI + 3*CLimbSize]
ADC EAX,[EDI + 3*CLimbSize]
MOV [EBX + 3*CLimbSize],EAX
// Update pointers.
LEA ESI,[ESI + 4*CLimbSize]
LEA EDI,[EDI + 4*CLimbSize]
LEA EBX,[EBX + 4*CLimbSize]
// Update counter and loop if required.
DEC ECX
JNE @MainLoop
@MainTail:
// Add index*CLimbSize so @MainX branches can fall through.
LEA ESI,[ESI + EDX*CLimbSize]
LEA EDI,[EDI + EDX*CLimbSize]
LEA EBX,[EBX + EDX*CLimbSize]
// Indexed jump.
LEA ECX,[@JumpsMain]
JMP [ECX + EDX*TYPE Pointer]
// Align jump table manually, with NOPs. Update if necessary.
NOP
// Jump table.
@JumpsMain:
DD @DoRestLoop
DD @Main1
DD @Main2
DD @Main3
@Main3:
MOV EAX,[ESI - 3*CLimbSize]
ADC EAX,[EDI - 3*CLimbSize]
MOV [EBX - 3*CLimbSize],EAX
@Main2:
MOV EAX,[ESI - 2*CLimbSize]
ADC EAX,[EDI - 2*CLimbSize]
MOV [EBX - 2*CLimbSize],EAX
@Main1:
MOV EAX,[ESI - CLimbSize]
ADC EAX,[EDI - CLimbSize]
MOV [EBX - CLimbSize],EAX
@DoRestLoop:
// etc...
Я удалил много пробелов, и, я думаю, читатель может получить остальную часть рутины. Он похож на основной цикл. Улучшение скорости ок. 20% для больших BigIntegers и около 10% для маленьких (всего несколько конечностей).
64-разрядная версия теперь использует по возможности 64 бит (в основном цикле и в Main3 и Main2, которые не являются "провальными", как описано выше) и до этого, 64 бит был намного медленнее 32 бит, но теперь он на 30% быстрее, чем 32 бит и вдвое быстрее, чем оригинальный простой 64-битный цикл.