我正在编写一个x86汇编例程,它的参数为:
1.在[ESP+4]
处:后面的32位整数的个数。
1.从[ESP+8]
开始:要相加的32位整数的列表。
它返回从[ESP+8]
开始传递的所有整数的和,所以,基本上,这个函数的C原型应该是:int addN(int numberofitems, ...);
我可以选择以两种方式编写这个x86汇编例程:
- 第一种方式(在地址位移内乘以项大小):
addN PROC
mov ecx, dword ptr [esp+4] ; Dec ecx per argument processed
mov edx, 2 ; Offset into variable length argument list
xor eax, eax ; Accumulator
AdderLoop:
add eax, dword ptr [esp+edx*4]
inc edx
dec ecx
jnz AdderLoop
ret
addN ENDP
- 第二种方式(将项目大小添加到
edx
本身):
addN PROC
mov ecx, dword ptr [esp+4] ; Dec ecx per argument processed
mov edx, 8 ; Offset into variable length argument list
xor eax, eax ; Accumulator
AdderLoop:
add eax, dword ptr [esp+edx]
add edx, 4
dec ecx
jnz AdderLoop
ret
addN ENDP
在性能方面或其他方面,偏好一种方式胜过另一种方式是否有任何优势?
2条答案
按热度按时间u1ehiz5o1#
在二进制机器码中,比例因子编码为2位移位计数(这就是为什么只支持2的0到3的幂,而不支持任意乘法器),因此机器码中的
[esp+edx]
实际上编码为[esp+edx*1]
:仍然存在偏移量,但它被设置为0。移位计数=0(即比例因子=1)不是硬件的特例,因为移位对硬件来说非常容易。因此,就硬件内部行为而言,两个环路实际上使用的是相同的寻址模式。
所以@Ped7g是对的:循环之间的区别在于使用
inc
而不是add
可以节省代码长度。实际加速
查看x86标签wiki以获取性能链接,特别是Agner Fog's guides。
显然,使用SSE 2或AVX 2向量对数组求和会快得多,使用PADDD(由于需要一次处理16 B,因此不能同时使用INC和比例因子,可以使用4加,比例因子为4)。
完全避免使用索引寻址模式会更有效,Skylake之前的Intel Sandybridge系列CPU不能微熔丝索引寻址模式。
add eax, dword ptr [edx]
甚至可以在Sandybridge上进行微熔丝,CMP/JB可以在比DEC/JNZ更多的CPU上进行宏熔丝。(AMD和Intel Core 2/Nehalem只能熔丝CMP/JB)请注意,这会在循环外花费额外的指令。你甚至可以减少循环中的指令数,方法是向上计数到零,然后使用计数器从数组末尾开始索引,或者,因为你只是对数组求和,所以顺序并不重要,你可以向下循环:
由于现代的x86 CPU每个时钟周期可以执行两次加载,如果不展开,我们只能获得一半的吞吐量。
(This实际上并不是最优的。它演示了我之前提到的从上到零的技术。在意识到向下循环是最好的之后,我没有时间重写这个。)
在某些CPU上,这应该会以两倍的速度运行(如果数据在L1缓存中是热的)。使用多个累加器(在本例中是EAX和ESI)对于高延迟操作(如FP add)是非常有用的技术。这里我们只需要两个累加器,因为整数ADD在每个x86微体系结构上都有1个周期延迟。
在Intel pre-Skylake上,使用非索引寻址模式(和
add edx, 8
)会更好,因为每个循环有两个内存寻址操作,但仍然只有一个分支(这需要CMP/JB,而不是通过递增索引来测试标志集)。展开时,更常见的是使用非展开循环来处理第一次或最后一次剩余迭代。我可以使用移位和CMOV来初始化其中一个累加器,因为我们只展开2,索引寻址模式的比例因子高达8。(我也可以用
and ecx, ~1
屏蔽ECX,清除低位,而不是移位,然后用更高的比例因子进行补偿。)voase2hg2#
用现代的CPU,理论化的分析特定来源的性能是非常棘手的。但是我还是会把自己烧了。
特别是在过去的十年里,我没有费心去学习任何关于ASM性能编码的知识,所以我的大多数评论都是基于对这里和那里的事物的一瞥,而不是任何透彻的知识和经验。
第0步:弄清楚你将如何剖析你的代码。没有真实世界的剖析,你将一事无成,因为我接下来将描述的一切都可能使结果更快或更慢,显然是在不同的目标CPU上,但即使在同一台目标机器上-取决于其余的可执行文件将如何着陆,因此其他函数将如何对齐,缓存将如何覆盖其他函数代码。
第一件事在循环开始时使用align指令。(或者以这样的方式对齐过程开始,循环的第一条指令将被对齐)。多少?看起来16通常会在大多数当前CPU上加速。这可能对性能有实际影响,但不仅仅是积极的,建议仅对频繁分支到的地址使用。
微妙之处:
让我们测试几个变体,它们是如何编译成机器码的:
如您所见,
*4
与*1
变体具有相同的长度,性能也相同,因此在寻址时不必担心*4
。因此,请使用任何一种模式,使其余代码更短/更快。
inc edx
是1B长的操作码,add edx,4
是3B长的操作码,所以我会选择第一种模式,因为在复杂的可执行文件中,较短的机器码更适合缓存,并且在现代x86上,inc
和add
之间不应该有任何性能差异--当与其余代码隔离考虑时。如果不孤立地考虑inc
was evil on the Intel Pentium 4 CPUs a few years back,但最近几代产品也可以,因此它应该与add
一样快。(Now我确实注意到您使用
add eax,...
,所以再一次使用不同的变体来解决这个问题):现在我想我看到了一些关于通过
esp
寻址的东西,有额外的前缀字节,但我在这里没有看到,所以可能是在16b中?这就是为什么我也测试了ebp
的变体,以摆脱esp
。但由于esp
的机器码较短(ebp
强制位移+0x0
字节使用),我会保留它就像你现在使用它一样。在一些较旧的CPU上,交叉使用相关指令可能会更快:
但是最新的体系结构使用了"macro-fusion"指令,
dec + jnz
对现在应该保持在一起。如果你知道参数的数量在大多数情况下会相当大(不太可能,因为这会溢出结果值),你当然可以展开循环进行几次迭代(4、8或16次,由于大代码的缓存污染,不会再高了)。
同样,如果参数的数量相当多,您可能会停止等待内存加载大部分循环的值。
然后,上面的任何代码变体都将以相同的性能结束,因为内存缓存未命中值数十到数百条指令,而不是寻址模式中的单指令细微差别。
我警告过你吗,这很棘手?我警告过,在第一句话里。
结论:
别为这事费心了,你在浪费时间。
只需编写最简单、可读性最强的正确源代码(在您的特定情况下,我更喜欢带有类似"index"的源代码的
*4
变体)。一旦你完成了你的申请,分析它。
解决真正的瓶颈。