首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >x86-64装配对齐和分支预测的性能优化

x86-64装配对齐和分支预测的性能优化
EN

Stack Overflow用户
提问于 2013-08-07 21:18:49
回答 3查看 7.8K关注 0票数 31

我目前正在编写一些C99标准库字符串函数的高度优化版本,如strlen()memset()等,使用带有SSE-2指令的x86-64程序集编写。

到目前为止,我已经在性能方面取得了很好的效果,但是当我尝试优化更多的时候,我有时会有奇怪的行为。

例如,添加或甚至删除一些简单的指令,或者简单地重新组织一些与跳转一起使用的本地标签,都会完全降低整体性能。从代码的角度来说,绝对没有任何理由。

因此,我的猜测是,代码对齐和/或被错误预测的分支存在一些问题。

我知道,即使在相同的体系结构(x86-64)下,不同的CPU也有不同的分支预测算法。

,但是在开发x86-64高性能时,是否有一些关于代码对齐和分支预测的一般性建议?

特别是关于对齐,我是否应该确保跳转指令使用的所有标签都对齐了DWORD?

代码语言:javascript
复制
_func:
    ; ... Some code ...
    test rax, rax
    jz   .label
    ; ... Some code ...
    ret
    .label:
        ; ... Some code ...
        ret

在前面的代码中,我是否应该在.label:之前使用一个对齐指令,比如:

代码语言:javascript
复制
align 4
.label:

如果是这样的话,当使用SSE-2时,在DWORD上对齐是否就足够了?

关于分支预测,是否有一种预先安排的方法来组织跳转指令所使用的标签,以帮助CPU,或者今天的CPU是否足够聪明,可以通过计算使用分支的次数来在运行时确定这一点?

编辑

好的,下面是一个具体的例子-这是strlen()与SSE-2的开始:

代码语言:javascript
复制
_strlen64_sse2:
    mov         rsi,    rdi
    and         rdi,    -16
    pxor        xmm0,   xmm0
    pcmpeqb     xmm0,   [ rdi ]
    pmovmskb    rdx,    xmm0
    ; ...

用1000个字符串运行它10,000‘000次会给出大约0.48秒的时间,这是很好的。

但是它不检查空字符串输入。因此,显然,我将添加一个简单的检查:

代码语言:javascript
复制
_strlen64_sse2:
    test       rdi,    rdi
    jz          .null
    ; ...

同样的测试,它现在在0.59秒内运行。但是,如果在检查后对齐代码:

代码语言:javascript
复制
_strlen64_sse2:
    test       rdi,    rdi
    jz          .null
    align      8
    ; ...

原来的表演又回来了。我使用8来对齐,因为4不会改变任何东西。

有人能解释一下这一点吗,并就何时对齐或不对齐代码部分给出一些建议吗?

编辑2

当然,这并不像调整每个分支目标那么简单。如果我这样做,表演通常会变得更糟,除非像上面这样的一些特殊情况。

EN

回答 3

Stack Overflow用户

发布于 2013-08-17 10:41:28

为了扩展代码艺术家的答案,他提出了一些好的观点,这里有一些额外的内容和细节,因为我实际上能够解决这个问题。

1 -代码对齐

英特尔建议在16字节边界上调整代码和分支目标。

3.4.1.5 -汇编/编译器编码规则12 (M影响,H一般性) 所有分支目标应该是16字节对齐的。

虽然这通常是一个很好的建议,但应该小心地执行

盲目地对齐所有的16字节可能会导致性能下降,因此在应用之前,应该在每个分支目标上测试这一点。

正如TheCodeArtist所指出的,使用多字节NOPs可能会有所帮助,因为简单地使用标准的单字节NOPs可能不会带来预期的代码对齐性能增益。

作为一个侧面,.p2align指令在NASM或YASM中是不可用的。

但它们确实支持与标准align指令中的NOPs以外的其他指令对齐:

代码语言:javascript
复制
align 16, xor rax, rax

2 .分支预测

事实证明这是最重要的部分。

虽然每一代x86-64 CPU都有不同的分支预测算法,但通常可以应用一些简单的规则来帮助CPU预测哪个分支可能会被采用。

CPU试图在BTB (分支目标缓冲区)中保持分支历史。

但是,当BTB中没有分支信息时,CPU将使用他们所称的静态预测,它遵循简单的规则,如英特尔手册中所提到的:

  1. 预测前向条件分支不会被接受。
  2. 预测要采取的向后条件分支。

下面是第一个例子:

代码语言:javascript
复制
test rax, rax
jz   .label

; Fallthrough - Most likely

.label:

    ; Forward branch - Most unlikely

.label下的指令是不太可能的条件,因为.label在实际分支之后被声明为

关于第二个案件:

代码语言:javascript
复制
.label:

    ; Backward branch - Most likely

test rax, rax
jz   .label

; Fallthrough - Most unlikely

在这里,.label下的指令是可能的条件,因为.label在实际分支之前被声明为

因此,每个条件分支都应该是,总是遵循这个简单的模式。

当然,这也适用于循环。

,正如我前面提到的,这是最重要的部分。

在添加简单的测试时,我遇到了无法预测的性能损益,这些测试应该在逻辑上提高总体性能。

盲目地坚持这些规则解决了这些问题。

如果不是,为优化目的增加一个分支可能会产生相反的结果。

TheCodeArtist在回答中还提到了循环展开。

虽然这不是问题,因为我的循环已经展开,但我在这里提到它,因为它确实是极其重要的,并带来了显著的性能提升。

作为读者的最后一个注意,虽然这看起来很明显,也不是这里的问题,但是当没有必要的时候,不要分支。

从奔腾Pro开始,x86处理器有条件移动指令,这可能有助于消除分支和抑制错误预测的风险:

代码语言:javascript
复制
test   rax, rax
cmovz  rbx, rcx

所以以防万一,记住这是件好事。

票数 24
EN

Stack Overflow用户

发布于 2015-07-27 00:18:36

为了更好地理解对齐的原因和方式,请查看阿格纳·福格的微结构医生,特别是。有关各种CPU设计的指令获取前端的部分.Sandybridge引入了uop缓存,这与吞吐量(特别是吞吐量)有很大的不同。在SSE代码中,指令长度通常太长,每周期16 B,无法覆盖4条指令。

填充uop缓存行的规则很复杂,但是一个新的32B指令块总是启动一个新的缓存行IIRC。因此,将热函数入口点与32B对齐是个好主意。在其他情况下,这么多的填充物对I$ density的伤害可能比帮助更大。(不过,L1 I$仍然有64B个缓存行,所以有些东西可能会在帮助uop缓存密度的同时损害L1 I$ density。)

循环缓冲区也有帮助,但是分支破坏了每个周期的4个uop,特别是在Haswell之前。例如,在SnB/IvB上执行一个由3个uop组成的循环,如abcabc、而不是abcabcda。因此,5-uop循环每2个周期进行一次迭代,而不是1.25次迭代.这使得展开更有价值。(Haswell和后来似乎在LSD中展开了小循环,使5-uop循环没有那么糟糕:当执行uop计数不是处理器宽度倍数的循环时,性能会下降吗?)

票数 5
EN

Stack Overflow用户

发布于 2015-07-26 23:28:02

“分支目标应该是16字节对齐规则”不是绝对的。该规则的原因是,使用16字节对齐,16字节指令可以在一个周期中读取,然后在下一个周期中读取另16个字节。如果您的目标是偏移量16n + 2,那么处理器仍然可以在一个周期内读取14个字节的指令(缓存行的其余部分),这通常是足够好的。然而,在偏移量16n + 15处启动循环是个坏主意,因为一次只能读取一个指令字节。更有用的方法是将整个循环保持在尽可能少的缓存行中。

在某些处理器上,分支预测具有8或4个字节内所有分支都使用相同分支预测器的奇怪行为。移动分支,以便每个条件分支使用自己的分支预测器。

两者的共同点是,插入一些代码可以改变行为,使其更快或更慢。

票数 3
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/18113995

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档