我目前正在使用带有 SSE-2 指令的 x86-64 程序集编写一些 C99 标准库字符串函数的高度优化版本,例如strlen()
,memset()
等。
到目前为止,我已经在性能方面取得了出色的成绩,但是当我尝试进行更多优化时,有时会出现奇怪的行为。
例如,添加甚至删除一些简单的指令,或者只是重新组织一些与跳转一起使用的本地标签,会完全降低整体性能。就代码而言,绝对没有理由。
所以我的猜测是代码对齐和/或错误预测的分支存在一些问题。
我知道,即使使用相同的架构(x86-64),不同的 CPU 也有不同的分支预测算法。
但是,在 x86-64 上开发高性能时,是否有一些关于代码对齐和分支预测的一般建议?
特别是关于对齐,我是否应该确保跳转指令使用的所有标签都在 DWORD 上对齐?
_func:
; ... Some code ...
test rax, rax
jz .label
; ... Some code ...
ret
.label:
; ... Some code ...
ret
在前面的代码中,我是否应该在之前使用 align 指令.label:
,例如:
align 4
.label:
如果是这样,在使用 SSE-2 时对齐 DWORD 是否足够?
关于分支预测,是否有一种“首选”方式来组织跳转指令使用的标签,以帮助 CPU,或者今天的 CPU 是否足够聪明,可以在运行时通过计算分支的次数来确定这一点?
编辑
好的,这是一个具体的例子 - 这是strlen()
SSE-2 的开始:
_strlen64_sse2:
mov rsi, rdi
and rdi, -16
pxor xmm0, xmm0
pcmpeqb xmm0, [ rdi ]
pmovmskb rdx, xmm0
; ...
使用 1000 个字符的字符串运行 10'000'000 次大约需要 0.48 秒,这很好。
但它不检查 NULL 字符串输入。很明显,我将添加一个简单的检查:
_strlen64_sse2:
test rdi, rdi
jz .null
; ...
同样的测试,它现在在 0.59 秒内运行。但是,如果我在此检查后对齐代码:
_strlen64_sse2:
test rdi, rdi
jz .null
align 8
; ...
原来的表演又回来了。我使用 8 进行对齐,因为 4 不会改变任何东西。
谁能解释一下,并就何时对齐或不对齐代码段提供一些建议?
编辑 2
当然,并不是把每个分支目标都对齐那么简单。如果我这样做,性能通常会变得更糟,除非像上面的某些特定情况。