某些CPU中ADC/SBB和INC/DEC在紧密循环中存在问题。

17

我正在用Delphi编写一个简单的BigInteger类型。它主要由TLimb的动态数组组成,其中TLimb是32位无符号整数,还有一个32位大小字段,它也保存BigInteger的符号位。

要添加两个BigIntegers,我会创建一个适当大小的新BigInteger,然后在一些簿记之后,调用以下过程,并传递左操作数和右操作数以及结果的数组各自开始的三个指针,以及左和右的limbs数量。

源代码:

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.

这段代码运行良好,我对它非常满意,直到我注意到,在我的开发环境(iMac上的Parallels VM中的Win7),一个简单的纯PASCAL加法例程,使用变量和一些if子句来模拟进位时,比我的简单、直接手工编写的汇编程序还要

我花了一些时间才发现,在某些CPU上(包括我的iMac和一台旧笔记本电脑),DECINCADCSBB的组合可能非常缓慢。但在我的其他大多数PC上(我有另外五台PC进行测试,尽管其中四台完全相同),它运行得很快。

因此,我编写了一个新版本,使用LEAJECXZ来模拟INCDEC,如下所示:

部分模拟代码

@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专家们是否知道避免某些CPU的速度缓慢的更好方法?

更新

Peter和Nils的答案帮助我尽快进入正确的轨道。这是我最终解决方案的主要部分:

普通代码:

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...    

我删除了很多空格,我猜读者可以理解剩下的程序。它类似于主循环。对于较大的BigIntegers,速度提高了约20%,对于小型BigIntegers(仅有几个limbs),速度提高了约10%。

64位版本现在在可能的情况下使用64位加法(在主循环中以及Main3和Main2中,这些都不像上面那样“fall-through”),之前,64位比32位慢得多,但现在它比32位快30%,比原始简单的64位循环快两倍。

更新2

英特尔在其《英特尔64和IA-32体系结构优化参考手册》中提出了“3.5.2.6部分标志寄存器停顿--示例3-29”:

        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

标志位保存在AL中,并通过MOVZX存储在EAX中。它通过循环中的第一个ADD进行添加。然后需要一个ADC,因为ADD可能会产生进位。请参见注释。

由于进位保存在EAX中,我也可以使用ADD来更新指针。循环中的第一个ADD还更新了所有标志位,因此ADC不会受到部分标志寄存器停顿的影响。


这将是相关的。我实际上也相信在某些体系结构上JECXZ很慢(可能不完全相同)。我会参考像Agner Fog这样的人提供比我更好的信息。 - 500 - Internal Server Error
ADD 会完全破坏进位标志,所以我必须模拟它。我尝试过了,但是模拟的成本比使用 ADD 带来的改进还要高。我甚至尝试了 SSE,相对于我的旧代码有了速度上的提升,但是我发布的模拟代码给出了最好的结果,直到现在。之前,我尝试通过使用 ADD 并模拟进位标志来避免 ADC,我尝试通过使用 SSE 并模拟进位标志来避免 ADC,我尝试通过上面的代码摆脱 INC 和 DEC,但我感觉可能错过了一些显而易见的东西。 - Rudy Velthuis
4
如果你的项目可以使用GPL授权的代码,那么请使用GMP的现有汇编程序。如果你可以链接到LGPL授权的库,那么请使用这个方法代替。https://gmplib.org/。GMP专门为多精度整数手工调试了非常精细的例程。此外,如果可能,请明显地使用64位代码。如果BigInt性能是你代码的问题,那么发布一个BigInt性能翻倍的64位版本是值得的。 - Peter Cordes
1
@500-InternalServerError: jecxz在Intel上只有2个uops,而宏合并的测试和分支只有1个。在AMD上,它仅有一个总的macro-op。这不像LOOP指令那样慢。看起来这是一种正当的情况,因为你需要循环而不影响标志位。Nils的展开版本非常好地分摊了成本。 - Peter Cordes
1
@PeterCordes:我想我可以使用GMP,但是我想要自己完成所有的事情。我还为了好玩实现了一个与.NET兼容的十进制类型。 - Rudy Velthuis
2个回答

20

您在旧的P6系列CPU上看到的是部分标志位停顿。
早期的Sandybridge系列处理器更有效地处理合并,而后来的SnB系列(例如Skylake)根本没有合并成本:需要同时读取CF和SPAZO组中某些标志位的uops将它们作为2个单独的输入读取

除了P4之外,英特尔CPU单独重命名每个标志位,因此JNE仅取决于设置其使用的所有标志位的最后一条指令(在这种情况下,只有Z标志)。事实上,最近的英特尔CPU甚至可以inc/jne内部组合成单个inc-and-branch uop(宏融合)。但是,当读取上一个更新任何标志的指令未修改的标志位时,问题就出现了。

Agner Fog表示,即使是PPro/PII的Intel CPU也不会在inc/jnz上停顿。实际上,造成停顿的不是inc/jnz,而是在下一次迭代中的adc需要在inc写入其他标志位但未修改CF后读取CF标志位。请保留HTML标签。
; 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不改变进位标志的事实的代码”(适用于Pentium M/Core2/Nehalem等处理器)。完全避免使用inc/dec的建议已经过时,仅适用于P4。其他CPU单独重命名EFLAGS的不同部分,并且只有在需要合并时(读取上一个指令未修改的标志)才会出现问题。

对于Sandybridge及其之后的处理器,它们会插入一个额外的uop来合并标志寄存器,当您读取上一个指令未修改的标志位时。这比停顿7个周期要快得多,但仍然不是理想的。

P4始终跟踪整个寄存器,而不是重命名部分寄存器,甚至不包括EFLAGS。因此,inc/jz对先前写入标志的任何内容都具有“虚假”的依赖性。这意味着循环条件无法检测到循环结束,直到执行adc dep链到达为止,因此当循环分支停止被采取时可能发生的分支错误预测无法早期检测到。但它确实可以防止任何部分标志的停顿。

你的 lea / jecxz 很好地避免了这个问题。但是它在 SnB 及以后的处理器上速度较慢,因为你没有展开循环。你的 LEA 版本有 11 个 uops(每 3 个时钟周期可以发出一次迭代),而 inc 版本有 7 个 uops(每 2 个时钟周期可以发出一次迭代),不包括插入的标志合并 uop,以替代停顿。

如果 loop 指令不慢,它将非常适合这种情况。实际上,在 AMD Bulldozer-family(1 m-op,与融合比较和分支的成本相同)和 Via Nano3000 上速度很快。但是,在所有 Intel CPU 上都很差(SnB-family 有 7 个 uops)。


展开循环

在展开循环时,使用指针而非索引寻址模式可以带来一些微小的收益,因为2个寄存器寻址模式在Snb及之后版本上无法进行微融合。一组load/adc/store指令在未进行微融合时占用6个uops,但是在进行微融合时只占用4个。CPU每个时钟周期可以发出4个融合域uop。(有关此级别的详细信息,请参见Agner Fog的CPU微体系结构文档和指令表)

尽可能地节省uops,以确保CPU能够比执行更快地发出指令,以确保它可以预先查看指令流,并吸收任何指令获取(例如分支错误预测)中的气泡。适应28uop循环缓冲区也意味着节省功率(在Nehalem上,避免指令解码瓶颈)。还有一些像指令对齐和跨越uop缓存线路界限的事情使得在没有循环缓冲区的情况下维持完整的4个uops / clock变得困难。

另一个技巧是保持指针在缓冲区末尾,并向零计数。 (因此,在循环开始时,您可以将第一个项目作为end[-idx]获取。)
        ; 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:

一个展开为4应该就可以了。不需要过度,因为你可能只需要通过3或4个展开来饱和Haswell之前的负载/存储端口,甚至可能只需要2个。
将上述循环展开为2次,对于英特尔CPU来说,会产生14个融合域uops。adc是2个ALU(+1个融合内存),jecxz是2个,其余部分(包括LEA)都是1个。在未融合的域中,有10个ALU/分支和6个内存(如果您真的将存储地址和存储数据分别计算,则为8个内存)。
  • 每次迭代有14个融合域uops:每4个时钟周期发出一次迭代。(最后的奇数2个uops必须作为一组2个发出,即使是从循环缓冲区中发出。)
  • 10个ALU和分支uops:在Haswell之前的CPU上需要3.33个时钟周期才能执行它们。我认为没有任何一个端口会成为瓶颈:adc的uops可以在任何端口上运行,lea可以在p0/p1上运行。跳转使用port5(jecx也使用p0/p1之一)。
  • 6个内存操作:在Haswell之前的CPU上需要3个时钟周期才能执行,可以每个时钟周期处理2个。Haswell添加了一个专用的AGU用于存储,因此它可以维持每个时钟周期2个load+1个store。
对于pre-haswell CPU,使用LEA/JECXZ,展开2次无法完全饱和ALU或load/store端口。展开4次将使其达到22个融合uop(发出需要6个时钟周期)。14个ALU和分支:执行需要4.66个时钟周期。12个内存:执行需要6个时钟周期。因此,展开4次将饱和pre-Haswell CPU,但仅仅是勉强。CPU不会有任何指令缓冲区来处理分支预测错误。
Haswell及更高版本始终会受到前端瓶颈的限制(每个时钟周期4个uops的限制),因为load/adc/store组合需要4个uops,并且可以保持每个时钟周期一个。因此,在不影响adc吞吐量的情况下,永远不会有循环开销的“空间”。这就是你必须知道不要过度展开的地方。
在Broadwell / Skylake上,adc是只有1c延迟的单个uop,而load / adc r, m / store似乎是最佳序列。adc m,r/i是4个uops。这应该可以支持每个时钟一个adc,就像AMD一样。
在AMD CPU上,adc只有一个宏操作,因此如果CPU可以维持4个问题率(即没有解码瓶颈),则它们也可以使用其2个加载/ 1个存储端口来击败Haswell。此外,对于AMD上的jecxz与其他分支一样有效:只有一个宏操作。多精度数学是AMD CPU擅长的少数几件事之一。某些整数指令的较低延迟使它们在某些GMP例程中具有优势。
一个大于5的展开可能会影响 Nehalem 的性能,因为这将使循环的大小大于 28uop 循环缓冲区。指令解码将限制您每个时钟周期不到 4 个 uops。甚至在更早的 Core2 上,有一个 64B 的 x86 指令循环缓冲区(64B 的 x86 代码,而不是 uops),可以帮助一些解码。

除非这个 adc 程序是您的应用程序中唯一的瓶颈,否则我会将展开因子保持在 2 左右。或者,如果这样可以节省大量的序言/尾声代码,并且您的 BigInts 不太大,则甚至可以不展开。您不想使代码膨胀太多,并在调用者调用许多不同的 BigInteger 函数(例如 add、sub、mul)并在其中执行其他操作时创建缓存未命中。如果您的程序在每次调用时不在内部循环中花费很长时间,则过度展开以赢得微基准测试可能会适得其反。

如果您的BigInt值通常不是非常大,那么您需要调整的不仅仅是循环。较小的展开可能有助于简化序言/尾声逻辑。当然,确保检查长度,以便ECX不会在从未为零的情况下越过零。这就是展开和向量化的问题。 :/

保存/恢复旧CPU的CF,而不是无标志循环:

这可能是最有效的方法:

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

使用与adc dep链相同的寄存器并不是一个问题:eax将始终在最后一个adcCF输出同时准备好。(在AMD和P4 / Silvermont中,部分寄存器写入对完整寄存器有误导性的依赖关系。它们不会单独重命名部分寄存器)。保存/恢复是adc dep链的一部分,而不是循环条件dep链的一部分。
循环条件仅检查由cmpsubdec编写的标志。在其周围保存/恢复标志不会使其成为adc dep链的一部分,因此可以在adc执行到达之前检测到循环末尾的分支预测失败。(本答案的先前版本错误地得出了这个结论。)

在设置代码中几乎肯定有一些可以削减指令的空间,也许可以通过使用寄存器来减少指令。您不必使用edi和esi作为指针,尽管我知道当您使用与其“传统”用途一致的方式使用寄存器时,这使得初始开发更加容易。(例如,EDI中的目标指针)。

Delphi是否允许您使用ebp?拥有第七个寄存器很好。

显然,64位代码将使您的BigInt代码运行速度约快两倍,即使您需要担心在64位 adc 循环的末尾执行单个32b adc 。它还将为您提供2倍的寄存器数量。


哇!我需要一些时间来消化(理解)所有这些内容,但看起来很不错。我已经得到了一般的想法,并且已经阅读了关于这个部分标志延迟的内容。不幸的是,Delphi的内置汇编器(BASM)没有宏,所以我将不得不进行一些复制和粘贴,或者编写一个小生成器来生成展开的循环。谢谢! - Rudy Velthuis
展开循环并在循环结束时使用RCL EAX,1保存进位,将所有指针/索引操作放在循环结尾和循环开始处,在循环中再次执行RCR EAX,1以恢复进位,这样怎么样?或者这样做太慢了吗?我知道我可以简单地尝试并测量,但不幸的是现在无法进行。我只需要在(展开的)循环内再次使用EAX。 - Rudy Velthuis
Delphi让我使用我喜欢的一切。EBP用于局部变量,但在这个函数中我不需要任何局部变量。 - Rudy Velthuis
在此提供我的实验结果,我之前确实使用过单寄存器寻址模式和增加指针的方法,但测量结果并未发现与索引访问有任何差别。也许是在展开循环中? - Rudy Velthuis
1
我终于有时间实现了单寄存器寻址模式。这对于非常长的BigIntegers来说,整体速度提升了约12%。我认为现在已经挤不出更多性能了。我尝试使用更小的BigIntegers(最多1到10个limbs,平均约3个limbs),但是与简单循环相比没有显着的时间差异,因此我将使用优化的展开循环例程,即最后一个版本,作为替换。它变得不那么易读了,所以我不得不添加了一些注释。<g> - Rudy Velthuis
显示剩余10条评论

8

现在有很多使用不同时序的x86芯片,你不可能为所有芯片都编写最优代码。你的方法是先有两个已知好的函数,在使用前进行基准测试,这已经相当高级了。

但是,根据您的BigIntegers的大小,您可以通过简单的循环展开来改进代码。 这将大大减少循环开销。

例如,您可以执行专门的块,像这样添加八个整数:

@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个元素需要处理时才执行上述块,并使用已经有的单个元素添加循环来处理剩余的几个元素。

对于大型BitInteger,您将花费大部分时间在展开的部分中,现在应该会执行得更快。

如果您想要更快,那么编写另外7个专门针对剩余元素计数的块并根据元素计数进行分支跳转到它们。这可以通过将7个地址存储在查找表中来实现,从中加载地址并直接跳转到专门的代码来完成。

对于小元素计数,这将完全删除整个循环,对于大元素计数,您将获得展开循环的全部好处。


谢谢,这给了我正确方向上的启示。虽然代码会变得稍微复杂一些,但应该会有显著的改进。我会进行测试。问题是,我现在不在家(这台笔记本电脑也没有显示问题),所以我只能在下周一进行测试。 - Rudy Velthuis
我将制作一个特殊的循环展开版本,用于处理更大的BigIntegers。我将测试“更大”有多大。 - Rudy Velthuis
顺便说一下,对于需要大量几乎相同的专门例程的优化问题,我通常会编写一个短程序来生成汇编代码。这样改变算术或大小就变得非常方便了。 - Nils Pipenbrinck

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接