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

要添加两个BigInteger,我创建一个适当大小的新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在iMac上的Parallels VM中)是一个简单的PURE PASCAL加载例程,在使用变量模拟进位的同时,几个if子句比我简单直接的手工汇编程序要快.

在某些cpu(包括我的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的大师也许知道一个更好的方法来避免某些cpu的缓慢?

更新

彼得和尼尔斯的回答帮助我很多东西走上正轨.这是DEC版本的最终解决方案的主要部分:

普通代码:

class procedure BigInteger.PlainAdd(Left,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,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],[ESI + 2*climbSize]
        ADC     EAX,[EDI + 2*climbSize]
        MOV     [EBX + 2*climbSize],[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...

我删除了很多空格,我猜读者可以得到其余的例程.它与主循环类似.速度提高约较大的BigInteger为20%,小型BigInteger为10%(仅限少数肢体).

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

解决方法

你看到的是一个部分的旗帜摊位.

Intel cpu(P4除外)分别重命名每个标志位,因此JNE仅依赖于最后一条指令,该指令设置其使用的所有标志(在本例中为Z标志).其实最近英特尔cpu甚至可以是internally combine an inc/jne into a single inc-and-branch uop(宏融合).但是,当读取最后一条更新任何标志的指令未修改的标志位时,会出现故障.

Agner Fog表示,英特尔cpu(甚至PPro / PII)不会在inc / jnz上停顿.实际上并不是这个inc / jnz的停顿,在下一次迭代中,adc在写入其他标志但是未修改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使进位标志不变的事实.” (适用于Pentium M / Core2 / Nehalem).避免inc / dec的建议完全是过时的,只适用于P4.其他cpu分别重命名EFLAGS的不同部分,只有在需要合并时才会遇到问题(读取最后一个insn写入任何标志的未修改的标志).

在其快速的机器(Sandybridge及更高版本)上,当您读取未被修改它的最后一条指令写入的位时,它们会插入一个额外的uop来合并标志寄存器.这比拖延7个周期要快得多,但仍然不理想.

P4总是跟踪整个寄存器,而不是重命名部分寄存器,甚至不是EFLAGS.所以inc / jz对于它之前写的标志有一个“虚假的”依赖.这意味着循环条件无法检测循环的结束,直到执行adc dep链到达,所以当循环分支停止被采取时可能发生的分支错误预测不能及早被检测到.尽管如此,它确实防止了任何部分标志的停顿.

你的lea / jecxz很好地避免了这个问题.它在SnB上较慢,后来因为你没有解开你的循环.您的LEA版本是11个uops(可以每3个周期发出一次迭代),而inc版本是7个uops(可以每2个周期发出一个iter),不计算插入的标志合并uop,而不是停止.

如果the loop instruction wasn’t slow,这将是完美的.在AMD推土机系列(1 m-op,与融合比较分支机构相同的成本)以及Via Nano3000中,实际上是快速的.不过,在所有英特尔®cpu上都是坏的(7位在SnB系列上).

展开

当您展开时,您可以使用指针而不是索引寻址模式获得另一个小增益.because 2-reg addressing modes can’t micro-fuse on SnB and later.一组加载/ adc / store指令是6个无微型融合的指令,只有4个带有微熔点. cpu可以发出4个融合域uops / clock. (有关此级别的详细信息,请参阅Agner Fog的cpu microarch文档和说明表).

当您可以确保cpu可以比执行时更快地发出指令,以确保它能够在指令流中足够长的时间内捕获任何未获取的气泡(例如分支错误预测).在28uop循环缓冲区中的安装也意味着节电(并且在Nehalem上,避免了指令解码瓶颈.)有诸如指令对齐和跨越Uop高速缓存行界限的事情,使得难以在没有循环的情况下维持一个完整的4个uop /缓冲区也是.

另一个诀窍是保持指针到缓冲区的末尾,并计数到零. (所以在你的循环开始,你得到第一个项目作为结束[-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的展开应该是好的.没有必要过度,因为你是prob.将能够使Haswell之前的加载/存储端口饱和,只能打开3或4,甚至可能是2.

2的展开将使上述循环正好是英特尔cpu的14个融合域. adc是2个ALU(1个融合存储器),jecxz是2,其余的(包括LEA)都是1个.在未分配的域中,有10个ALU /分支和6个内存(如果你真的算存储地址和存储数据分开).

>每次迭代14个融合域uops:每4个时钟发出一次迭代. (最后的奇怪的2个uops必须作为一组2发出,甚至从循环缓冲区发出.)
> 10 ALU&分支uops:要3.33c在pre-haswell之前执行它们.我不认为任何一个端口将成为瓶颈,无论是:adc的uop可以在任何端口上运行,lea可以在p0 / p1上运行.跳转使用port5(和jecx也使用p0 / p1之一)
> 6个内存操作:使3c执行前的Haswell cpu,每个时钟可以处理2个. Haswell为商店添加了专门的AGU,因此可以承受2负载1 /时钟.

因此,对于pre-haswell cpu,使用LEA / JECXZ,展开2将不会使ALU或加载/存储端口饱和. 4号的展开将带来22个融合(发布6个周期). 14 ALU& branch:4.66c执行. 12个内存:执行6个周期.所以4的展开将使Haswell之前的cpus饱和,但只是勉强. cpu不会有任何缓冲区的指示,以便在分支错误预测中进行调整.

Haswell和以后将永远在前端瓶颈(每个时钟限制为4 uops),因为加载/ adc / store组合需要4个uops,并且可以每秒钟一个维持.所以没有任何“空间”的循环开销,而不会削减adc吞吐量.这是你必须知道的,不要超过它并展开太多.

在broadwell / Skylake,adc is only a single uop with 1c latency,and load / adc r,m / store appears to be the best sequence. adc m,r / i是4 uops.这应该像AMD那样支持一个adc.

在AMD cpu上,adc只是一个宏操作,所以如果cpu可以维持4的发布率(即没有解码瓶颈),那么他们也可以使用他们的2个加载/ 1存储端口来击败Haswell.另外,AMD的jecxz与其他分支一样高效:只有一个宏操作.多精度数学是AMD cpu擅长的几个方面之一.某些整数指令的较低延迟给了他们一些GMP例程的优势.

超过5次的展开可能会伤害Nehalem的性能,因为这会使循环大于28uop循环缓冲区.然后指令解码将限制您每时钟少于4个uops.甚至更早(Core2),还有一个64B x86指令循环缓冲区(64位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将始终与最后一个adc的CF输出同时准备就绪. (在AMD和P4 / Silvermont部分注册表中有一个完全注册的虚假的dep,它们不会单独重命名部分注册表).保存/恢复是adc dep链的一部分,而不是循环条件解码链.

循环条件只检查由cmp,sub或dec写的标志.保存/恢复其周围的标志不会使其成为adc dep链的一部分,因此在adc执行到达之前可以检测到循环结束时的分支错误预测. (此答案的以前版本出错)

几乎肯定有一些空间可以擦除设置代码中的指令,也许通过使用值开始的寄存器.您不必将edi和esi用于指针,尽管我知道当您以与“传统”使用方式一致的方式使用寄存器时,初始化开发变得更简单. (例如EDI中的目标指针).

Delphi是否让您使用ebp?有一个第七个注册表很高兴

显然,64位代码将使您的BigInt代码运行速度提高约两倍,即使您必须担心在64位adc循环结束时执行一个32b adc.它也会给你2x的数量的寄存器.

delphi – 某些CPU在紧循环中的ADC / SBB和INC / DEC存在问题的更多相关文章

  1. 汇编 – “LES”8086指令未按预期工作

    当我调试编译的exe时,我注意到ES和DI寄存器没有加载正确的值.在加载段和从RAM偏移之前,需要将DS寄存器设置为实际指向数据段.默认情况下,DS指向您的PSP,而PSP不是您希望它指向的位置.

  2. php – 如何在ESI中设置cookie:include脚本?

    我有一个基本的PHP页面通过Varnish加载,其中包含一个ESI回调服务器来设置cookie.cookie是通过域访问等设置的,但是当通过ESI调用时,cookie永远不会被设置.如果直接访问ESIinclude路径,则cookie设置没有问题.我甚至设置了我的Varnish配置永远不会缓存任何东西,认为VCL可能会杀死cookie.这个………我是Varnish和ESI的新手,所以我开始怀疑这是否是一个已知的限制,但我无法在网上找到任何关于我的问题的讨论.一个有趣的问题,在(SettingCookies

  3. php函数foo(array $arg = NULL) – 为什么数组和NULL?

    最近我看过以下几次:我的问题是为什么当默认值为$argNULL时,它将被转换成数组?

  4. 动态缓存技术之CSI,SSI,ESI

    动态内容缓存技术,总体来说就是该静态化的静态化,该动态的保持动态,最后进行组合!可行的方案大致有三种:CSI,SSI,ESI一、CSI含义:通过iframe、javascript、ajax等方式将另外一个页面的内容动态包含进来。与SSI不同的是,ESI多在缓存服务器或代理服务器上执行!

  5. winapi – 如何在Win32中获取CPU周期数?

    在Win32中,有没有办法获得一个独特的cpu循环计数或类似的东西,对于多个进程/语言/系统/等是统一的.我正在创建一些日志文件,但是必须生成多个日志文件,因为我们正在托管.NET运行时,并且我想避免从一个调用到另一个来进行日志记录.因此,我在想我只生成两个文件,将它们组合起来,然后对它们进行排序,以获得涉及跨世界调用的连贯时间线.但是,每次通话都不会增加GetTickCount,因此不可靠.是否

  6. 为什么添加额外的AngularJS验证指令会导致$asyncValidators多次运行?

    我创建了一个实现$asyncValidators的自定义指令.这是该自定义指令的基本结构:控制器初始化tailNumber模型值,如下所示:这是用户保存的指令在页面加载时运行3次的html:当我删除ng-minlength=“2”时,用户保存的指令在页面加载时运行两次(2次).这是移除了ng-minlength=“2”的html:当我删除ng-pattern=“/^[A-z][a-zA-Z0-9]*$/”时,用户保存的指令只运行一次.这是移除ng-pattern=“/^[A-z][a-zA-Z0-9]*$

  7. angularjs – 更改指令中的控制器范围变量不会反映在控制器功能中

    在我的指令中,我有一个控制器变量,当你按下指令中的按钮时,页面会增加.但是,调用控制器函数的下一行scope.alertPage()不会反映此更改.请注意,当您单击按钮页面时仍然会被警告为1!

  8. [译] Angular 属性绑定更新机制

    同样,Angular也提供了两种方式来实现父子组件通信:输入输出绑定和共享服务。本文主要介绍输入输出绑定方式,特别是当父组件输入绑定值变化时,Angular如何更新子组件输入值。如果想了解Angular如何更新当前组件DOM,可以查看译AngularDOM更新机制,这篇文章也会有助于加深对本文的理解。你可能好奇Angular是怎么知道BComponent和span支持textContent绑定的。其中,因为Angular会更新每一个视图的DOM,所以需要传入当前视图的索引。

  9. angularjs – Angular JS – 滚动$窗口

    好吧,我有点不解。我试图认为角度方式来自jQuery背景。如果有人向下滚动页面,我想隐藏的元素。我已经尝试创建一个自定义指令,但我不能让它工作,因为滚动事件不发射。基本指令看起来像这样。一个关键点是你需要调用范围$apply(),因为滚动将在正常的摘要周期之外运行。

  10. Angular Directive Lifecycle

    Angular中所有的钩子如下图所示:怎么那么多钩子,是不是被吓到了,没事我们基于指令与组件的区别来分个类:指令与组件共有的钩子ngOnChangesngOnInitngDoCheckngOnDestroy组件特有的钩子ngAfterContentinitngAfterContentCheckedngAfterViewInitngAfterViewChecked生命周期的顺序ngOnChanges()当被绑定的输入属性的值发生变化时调用,该方法接受当前和上一属性值的SimpleChanges对象,首次调用

随机推荐

  1. delphi – 主窗口按进程名称处理

    DelphiXe,Win7x64如何从进程名称(exe文件的完整路径)获取主窗口句柄,或至少一个类或窗口名称(如果该进程只有一个窗口).例:解决方法我同意Petesh的说法,你需要枚举顶级窗口并检查创建它的进程的模块文件名.为了帮助您开始枚举顶级窗口,这是一个delphi实现.首先,当你回调给你时,你需要一些与EnumWindows方法通信的方式.为此声明一条记录,该记录将保存您要查找的模块的文件

  2. 如何在Delphi中纯粹通过RTTI信息(即不使用任何实际对象实例)获取TObjectList的子项类型?

    我正在使用RTTI实现用于流式传输任意Delphi对象的通用代码,并且为了使其工作(更具体地说,为了使加载部分工作),我需要以某种方式获得TObjectList的子项类型<T>不使用任何实际对象实例的字段.要求不使用任何实际对象实例的明显原因是,在从流加载对象的情况下(仅基于要加载的对象的类类型的知识),我将不会有任何实例在加载完成之前完全可用–我宁愿只能访问相关类的纯RTTI数据.我希望能

  3. inno-setup – Inno Setup – 安装程序背景图片

    图像作为安装程序背景如何用inno5.5.9做到这一点?

  4. inno-setup – Inno Setup – 如何添加多个arc文件进行解压缩?

    使用InnoSetup解压缩弧文件.我希望有可能解压缩多个arc文件以从组件选择中安装文件(例如).但仍然显示所有提取的整体进度条.这可能吗?的回答的修改预备是相同的,参考其他答案.在ExtractArc中,为要提取的每个存档调用AddArchive.

  5. delphi – 如何在DataSet的帮助下在TAdvStringGrid中显示数据库中的BLOB图像

    解决方法CreateBlobStream正在创建一个TStream对象,而不是TMemoryStream.由于您不想将JPG写入数据库,因此应使用bmRead而不是bmReadWrite.我不习惯sqlite,但你必须确保使用合适的二进制日期类型.为了确保存储的图像真的是JPG,您应该编写JPG以进行测试,例如:

  6. inno-setup – 在Inno Setup的Code部分下载程序后运行程序

    如何运行我通过Internet下载的应用程序,在代码部分中使用,并等待该应用程序完成运行.我有,使用InnoTools下载程序,下载这两个文件,我想,在第二个完成下载后运行该下载,或jdk-8u111-windows-x64.exe,然后继续安装.解决方法使用其他下载插件,而不是ITD(请参阅下面的原因).例如,InnoDownloadPlugin.当您包含idp.iss时,它定义了一个全局IDP

  7. progress-bar – Inno Setup Run部分的简单进度页面

    我的安装程序非常简单,它基本上是:>欢迎页面>进展页面>最终页面欢迎页面和最终页面是标准页面.在Progress页面,我正在静默安装一堆其他程序.实际的脚本是在[Run]部分中安装每个程序.问题是酒吧达到100%然后停留在那里.我只能更改消息文本.我想要实现的是使用Pascal脚本显示进度,例如:这样我就可以显示更准确的进度条.这就是我所拥有的:问题是,当我构建安装程序时,它不显示欢迎页面.我做错了什么?

  8. delphi – 如何使“显示/隐藏桌面图标”设置生效?

    下面的代码调用SHGetSetSettings函数来隐藏桌面图标但它只是从视图菜单中取消选中“显示桌面图标”.我打电话给SHChangeNotify;更新桌面,但这不起作用?解决方法isa,要刷新桌面,您可以将F5键发送到progman窗口隐藏桌面图标的另一种方法是再次显示

  9. inno-setup – Inno Setup – 避免显示子安装程序的文件名

    我试图使用InnoSetup–Howtohidecertainfilenameswhileinstalling?(FilenameLabel)的想法Theonlysuresolutionistoavoidinstallingthefiles,youdonotwanttoshow,usingthe[Files]section.Installthemusingacodeinstead.UsetheEx

  10. inno-setup – Inno Setup磁力链接下载实施

    我目前正在使用InnoDownloadPlugin为我的安装程序下载文件,这个问题最大的问题是faila正确下载文件.因为连接不良等诸多原因.我想添加一种替代方法来下载文件,因此用户可以选择是否需要常规方式或torrent方式.我知道我可以使用aria2c.exe应用程序(https://aria2.github.io/),有人可以帮我实现它的inno设置代码吗?我需要的是使用torrent(ar

返回
顶部