增加指针速度更快还是执行"mov [pointer+1],eax"更快?

5

假设我们想将一个字符串存储在EDI寄存器中。这种方式存储会更快吗?

mov byte [edi],0
mov byte [edi+1],1
mov byte [edi+2],2
mov byte [edi+3],3
...

这样做好还是那样做好?
mov byte [edi],0
inc edi
mov byte [edi],1
inc edi
mov byte [edi],2
inc edi
mov byte [edi],3
inc edi
...

有些人可能会建议使用小端序的方式:
mov dword [edi],0x3210

或者按照大端字节序如下:
mov dword [edi],0x0123

但这不是我问题的重点。我的问题是,递增指针然后执行mov指令是否更快,需要更多的指令,还是在每个mov指令中指定要添加到由EDI指向的偏移地址的数量更快?如果后者是正确的,在具有相同的添加到偏移地址的数字的多少个mov指令之后,将值得增加该指针的数量?换句话说,这是

mov byte [edi+5],0xFF
mov byte [edi+5],0xFF
mov byte [edi+5],0xFF
mov byte [edi+5],0xFF

比这更快?
add edi,5
mov byte [edi],0xFF
mov byte [edi],0xFF
mov byte [edi],0xFF
mov byte [edi],0xFF

不了解处理器品牌、架构等信息,这个问题是没有意义的。通常情况下,一次数据宽度的移动将比相同数据的按字节移动更快。但即使如此,现代架构也会优化这种情况,所以也不是绝对的。 - Gene
3
注意:应该是0x00010203而不是0x0123。 - Sami Kuhmonen
1
具有偏移量的指令经过高度优化,不会阻塞流水线,所以我倾向于说它们会更快。但无论如何,您可能会受到内存速度的限制,即使您正在写入缓存。 - Mark Ransom
如果存储序列长度有限,则它所需的其他执行资源量决定了它与周围指令混合的效果如何。@MarkRansom - Peter Cordes
1个回答

11
请参阅http://agner.org/optimize/维基中的其他链接,了解有关如何优化汇编代码的文档。
这样:
mov byte [edi],0
mov byte [edi+1],1
mov byte [edi+2],2
mov byte [edi+3],3
...

将会更快。据我所知,在任何当前的微架构中使用位移都没有额外的成本,除了指令大小增加的额外一个或四个字节。在英特尔SnB系列CPU上,双寄存器寻址模式可能会更慢, 但固定的位移是可以的。

像gcc和clang这样的实际编译器在展开循环时始终使用第一种方法(有效地址中的位移)。


顺便说一句,存储0x03020100的4字节比四个单独的1字节存储快近4倍。大多数现代CPU都有128位数据路径,因此任何单个存储器高达128位需要与8位存储相同的执行资源。在Intel SnB / IvB上,AVX 256b存储仍然比两个128b存储要便宜得多(如果对齐),而Intel Haswell及更高版本可以在单个操作中执行256b存储。然而,mov-immediate到内存只适用于8、16和32位操作数。mov r64,imm64(仅限寄存器)在64位模式下可用,但没有128或256 mov-immediate指令。


在32位模式下,可以使用一个字节的编码来实现inc reg,因此inc edi/mov byte [edi],1的代码大小相等,但在最近的英特尔和AMD微架构上,仍会解码为两倍的uops。如果代码仍然受存储吞吐量或其他问题的限制,则这可能不是一个问题,但无论如何都不会更好。 CPU非常复杂,通过计算uops的简单分析并不总是与实际结果匹配,但我认为在每个存储之间运行inc会更快的可能性非常小。你能说的最好的就是它可能不会运行得更慢。它可能会使用更多的电力/热量,并且对于超线程来说不太友好。
在64位模式下,inc rdx需要3个字节进行编码:1个REX指定64位操作数大小(而不是默认的32位),1个操作码字节指定inc r/m,以及1个mod/rm字节指定rdx作为操作数。
在64位模式下,有一个代码大小的不利影响。 在两种情况下,“inc”解决方案将使用高价值uop-cache(在Intel SnB家族CPU上保持融合域uops)中的两倍条目,这是一种代码储存器。
更多的uops也意味着ROB中有更多的空间,因此乱序执行不能查看太远的前面。
此外,“inc”指令链将延迟存储地址uops从先前计算多个存储地址(并将它们写入存储缓冲区)。 Intel Ice Lake有两个端口可以运行存储地址uops(从Haswell的3个端口降至)。 如果存储地址尽早准备好,则对后面的负载更好,因此CPU可以确定它们是独立的,或者它们是否重叠。 它还让它们更早地离开调度程序(RS),从而释放出乱序执行结构中的空间。

第二部分:

mov byte [edi+5],0xFF
mov byte [edi+5],0xFF
mov byte [edi+5],0xFF
mov byte [edi+5],0xFF

对抗。

add edi,5            ; 3 bytes to encode.
mov byte [edi],0xFF  ; saving one byte in each instruction
mov byte [edi],0xFF
mov byte [edi],0xFF
mov byte [edi],0xFF

除非代码大小非常重要(不太可能),或者存在更多的存储器,否则请使用第一种形式。第二种形式比第一种多一个字节,但少了一个融合域uop。在具有uop缓存的CPU上,它将使用更少的空间。在较旧的CPU上(没有uop缓存),指令解码是瓶颈,因此有些情况下指令更好地对齐为4个一组会成为瓶颈。如果您已经受到存储端口的限制,则不会出现这种情况。

1
请注意,如果您使用无inc版本,则可以并行进行有效地址计算。对于inc版本,每个有效地址计算都会在前面的inc上停顿。(或者至少在 Pentium 上是这样,那是我最后一次研究这种东西的时候。) - Raymond Chen
@RaymondChen:是的,没错。我在回答中没有提到这一点,因为它们是存储操作,所以即使它们最终退役需要很长时间也没关系。不过现在想想,未解决的存储地址意味着所有后续的加载都必须等待,以防有读写依赖性。此外,能够执行存储地址 uops 可以将它们从调度器中移出,而调度器比重排序缓冲区小得多(32 vs. 192 或其他)。 - Peter Cordes
如果你真的想深入了解,inc指令会带来麻烦,因为它修改了一些标志位但是保持其他不变。这意味着在执行多个指令时,inc之后的标志位状态取决于执行顺序,这进一步阻碍了并行性。 - Raymond Chen
@Raymond:这只是P4上的软件问题。 P6和SnB微架构系列以及AMD都单独重命名EFLAGS的不同部分,因此仅在读取上一个设置标志的指令未修改的标志时才会有惩罚(例如adc / dec / jnz循环)。 在旧CPU上,惩罚更严重(停顿与额外的uop合并)。现代硬件尽可能地花费晶体管和功率来避免标志上的错误依赖,因此最好节省insn字节。 - Peter Cordes

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