Delphi 某些CPU上的ADC/SBB和INC/DEC在紧循环中存在问题
我正在用Delphi编写一个简单的BigInteger类型。它主要由TLimb的动态数组(其中TLimb是32位无符号整数)和32位大小字段组成,该字段还保存BigInteger的符号位 为了添加两个BigInteger,我创建了一个大小合适的新BigInteger,然后在簿记之后,调用以下过程,将三个指针分别传递给左操作数和右操作数的数组开始和结果,以及左操作数和右操作数的分支数 普通代码: 这段代码运行得很好,我对它非常满意,直到我注意到,在我的开发设置(iMac上的Parallels VM中的Win7)上,一个简单的纯PASCAL加法例程,在用一个变量和几个Delphi 某些CPU上的ADC/SBB和INC/DEC在紧循环中存在问题,delphi,assembly,x86,cpu-architecture,bigint,Delphi,Assembly,X86,Cpu Architecture,Bigint,我正在用Delphi编写一个简单的BigInteger类型。它主要由TLimb的动态数组(其中TLimb是32位无符号整数)和32位大小字段组成,该字段还保存BigInteger的符号位 为了添加两个BigInteger,我创建了一个大小合适的新BigInteger,然后在簿记之后,调用以下过程,将三个指针分别传递给左操作数和右操作数的数组开始和结果,以及左操作数和右操作数的分支数 普通代码: 这段代码运行得很好,我对它非常满意,直到我注意到,在我的开发设置(iMac上的Parallels VM
if
子句模拟进位的同时也做了同样的操作,比我的普通程序快,简单的手工汇编程序
我花了一段时间才发现,在某些CPU(包括我的iMac和旧笔记本电脑)上,DEC
或INC
和ADC
或SBB
的组合可能非常慢。但在我的大多数其他电脑上(我还有五台电脑要测试,尽管其中四台完全相同),速度相当快
因此,我编写了一个新版本,使用LEA
和JECXZ
模拟INC
和DEC
,如下所示:
模拟代码的一部分:
这使得我在“慢”机器上的代码速度几乎是“快”机器上的三倍,但在“快”机器上大约慢了20%。所以现在,作为初始化代码,我做了一个简单的定时循环,并用它来决定是将单元设置为调用普通例程还是模拟例程。这几乎总是正确的,但有时它选择(较慢的)普通例程,而它本应选择模拟例程
但我不知道这是否是最好的方法
问题:
我给出了我的解决方案,但是这里的asm专家是否知道一种更好的方法来避免某些CPU上的速度慢
更新
彼得和尼尔斯的回答帮助我走上了正确的道路。这是我针对DEC
版本的最终解决方案的主要部分:
普通代码:
我删除了很多空白,我想读者可以完成剩下的程序。它类似于主回路。大整数的速度提高约20%,小整数的速度提高约10%(只有几个肢体)
64位版本现在在可能的情况下使用64位加法(在主循环中以及在Main3和Main2中,它们不是像上面那样的“失败”),在此之前,64位比32位慢很多,但现在它比32位快30%,比原来的简单64位循环快两倍
更新2
英特尔在其《英特尔64和IA-32体系结构优化参考手册》中提出3.5.2.6部分标志寄存器暂停——示例3-29:
标志保存在AL
中,并通过EAX
中的MOVZX
保存。它是通过循环中的第一个ADD
添加的。然后需要一个ADC
,因为ADD
可能会生成进位。另见评论
因为进位保存在
EAX
中,所以我还可以使用ADD
来更新指针。循环中的第一个ADD
也会更新所有标志,因此ADC
不会出现部分标志寄存器暂停。有太多的x86芯片在使用中,它们的时间相差悬殊,因此您不可能为所有这些芯片都提供最佳的代码。您在使用之前拥有两个已知的好函数和基准的方法已经相当先进了
但是,根据大整数的大小,您可能可以通过简单的循环展开来改进代码。这将大大减少循环开销
例如,您可以执行一个专门的块,该块将八个整数相加,如下所示:
@AddEight:
MOV EAX,[ESI + EDX*CLimbSize + 0*CLimbSize]
ADC EAX,[EDI + EDX*CLimbSize + 0*CLimbSize]
MOV [EBX + EDX*CLimbSize + 0*CLimbSize],EAX
MOV EAX,[ESI + EDX*CLimbSize + 1*CLimbSize]
ADC EAX,[EDI + EDX*CLimbSize + 1*CLimbSize]
MOV [EBX + EDX*CLimbSize + 1*CLimbSize],EAX
MOV EAX,[ESI + EDX*CLimbSize + 2*CLimbSize]
ADC EAX,[EDI + EDX*CLimbSize + 2*CLimbSize]
MOV [EBX + EDX*CLimbSize + 2*CLimbSize],EAX
MOV EAX,[ESI + EDX*CLimbSize + 3*CLimbSize]
ADC EAX,[EDI + EDX*CLimbSize + 3*CLimbSize]
MOV [EBX + EDX*CLimbSize + 3*CLimbSize],EAX
MOV EAX,[ESI + EDX*CLimbSize + 4*CLimbSize]
ADC EAX,[EDI + EDX*CLimbSize + 4*CLimbSize]
MOV [EBX + EDX*CLimbSize + 4*CLimbSize],EAX
MOV EAX,[ESI + EDX*CLimbSize + 5*CLimbSize]
ADC EAX,[EDI + EDX*CLimbSize + 5*CLimbSize]
MOV [EBX + EDX*CLimbSize + 5*CLimbSize],EAX
MOV EAX,[ESI + EDX*CLimbSize + 6*CLimbSize]
ADC EAX,[EDI + EDX*CLimbSize + 6*CLimbSize]
MOV [EBX + EDX*CLimbSize + 6*CLimbSize],EAX
MOV EAX,[ESI + EDX*CLimbSize + 7*CLimbSize]
ADC EAX,[EDI + EDX*CLimbSize + 7*CLimbSize]
MOV [EBX + EDX*CLimbSize + 7*CLimbSize],EAX
LEA ECX,[ECX - 8]
现在重建循环,只要有8个以上的元素要处理,就执行上面的块,并使用已有的单元素添加循环执行剩余的几个元素
对于大型咬人者,你将把大部分时间花在展开部分,现在应该执行得更快
如果您想要更快,那么再写七个专用于剩余元素计数的附加块,并基于元素计数分支到它们。最好将这七个地址存储在一个查找表中,从中加载地址并直接跳转到专用代码中
对于小元素计数,这将完全删除整个循环,对于大元素,您将获得展开循环的全部好处。您在旧P6系列CPU上看到的是部分标志暂停。
早期的Sandybridge家族更有效地处理合并,而后来的SnB家族(如Skylake)根本没有合并成本: 英特尔CPU(P4除外)分别重命名每个标志位,因此
JNE
仅取决于设置其使用的所有标志的最后一条指令(在本例中,仅限Z
标志)。事实上,最近的英特尔CPU甚至可以(宏融合)。但是,当读取上次更新任何标志的指令未修改的标志位时,就会出现问题
表示英特尔CPU(甚至PPro/PII)不会在inc/jnz
上暂停。实际上不是inc/jnz
停止,而是下一次迭代中的adc
必须在inc
写入其他标志后读取CF
标志,但未修改CF
; Example 5.21. Partial flags stall when reading unmodified flag bits
cmp eax, ebx
inc ecx
jc xx
; Partial flags stall (P6 / PIII / PM / Core2 / Nehalem)
Agner Fog还更笼统地说:“避免使用依赖INC或DEC保持进位标志不变的代码。”。完全避免inc
/dec
的建议已经过时,只适用于P4。其他CPU分别重命名EFLAG的不同部分,并且只有troubl
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...
XOR EAX,EAX
.ALIGN 16
@MainLoop:
ADD EAX,[ESI] // Sets all flags, so no partial flag register stall
ADC EAX,[EDI] // ADD added in previous carry, so its result might have carry
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
SETC AL // Save carry for next iteration
MOVZX EAX,AL
ADD ESI,CUnrollIncrement*CLimbSize // LEA has slightly worse latency
ADD EDI,CUnrollIncrement*CLimbSize
ADD EBX,CUnrollIncrement*CLimbSize
DEC ECX
JNZ @MainLoop
@AddEight:
MOV EAX,[ESI + EDX*CLimbSize + 0*CLimbSize]
ADC EAX,[EDI + EDX*CLimbSize + 0*CLimbSize]
MOV [EBX + EDX*CLimbSize + 0*CLimbSize],EAX
MOV EAX,[ESI + EDX*CLimbSize + 1*CLimbSize]
ADC EAX,[EDI + EDX*CLimbSize + 1*CLimbSize]
MOV [EBX + EDX*CLimbSize + 1*CLimbSize],EAX
MOV EAX,[ESI + EDX*CLimbSize + 2*CLimbSize]
ADC EAX,[EDI + EDX*CLimbSize + 2*CLimbSize]
MOV [EBX + EDX*CLimbSize + 2*CLimbSize],EAX
MOV EAX,[ESI + EDX*CLimbSize + 3*CLimbSize]
ADC EAX,[EDI + EDX*CLimbSize + 3*CLimbSize]
MOV [EBX + EDX*CLimbSize + 3*CLimbSize],EAX
MOV EAX,[ESI + EDX*CLimbSize + 4*CLimbSize]
ADC EAX,[EDI + EDX*CLimbSize + 4*CLimbSize]
MOV [EBX + EDX*CLimbSize + 4*CLimbSize],EAX
MOV EAX,[ESI + EDX*CLimbSize + 5*CLimbSize]
ADC EAX,[EDI + EDX*CLimbSize + 5*CLimbSize]
MOV [EBX + EDX*CLimbSize + 5*CLimbSize],EAX
MOV EAX,[ESI + EDX*CLimbSize + 6*CLimbSize]
ADC EAX,[EDI + EDX*CLimbSize + 6*CLimbSize]
MOV [EBX + EDX*CLimbSize + 6*CLimbSize],EAX
MOV EAX,[ESI + EDX*CLimbSize + 7*CLimbSize]
ADC EAX,[EDI + EDX*CLimbSize + 7*CLimbSize]
MOV [EBX + EDX*CLimbSize + 7*CLimbSize],EAX
LEA ECX,[ECX - 8]
; Example 5.21. Partial flags stall when reading unmodified flag bits
cmp eax, ebx
inc ecx
jc xx
; Partial flags stall (P6 / PIII / PM / Core2 / Nehalem)
; pure loads are always one uop, so we can still index it
; with no perf hit on SnB
add esi, ecx ; point to end of src1
neg ecx
UNROLL equ 4
@MainLoop:
MOV EAX, [ESI + 0*CLimbSize + ECX*CLimbSize]
ADC EAX, [EDI + 0*CLimbSize]
MOV [EBX + 0*CLimbSize], EAX
MOV EAX, [ESI + 1*CLimbSize + ECX*CLimbSize]
ADC EAX, [EDI + 1*CLimbSize]
MOV [EBX + 1*CLimbSize], EAX
; ... repeated UNROLL times. Use an assembler macro to repeat these 3 instructions with increasing offsets
LEA ECX, [ECX+UNROLL] ; loop counter
LEA EDI, [EDI+ClimbSize*UNROLL] ; Unrolling makes it worth doing
LEA EBX, [EBX+ClimbSize*UNROLL] ; a separate increment to save a uop for every ADC and store on SnB & later.
JECXZ @DoRestLoop // LEA does not modify Zero flag, so JECXZ is used.
JMP @MainLoop
@DoRestLoop:
lahf
# clobber flags
sahf ; cheap on AMD and Intel. This doesn't restore OF, but we only care about CF
# or
setc al
# clobber flags
add al, 255 ; generate a carry if al is non-zero