我目前正在使用带有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
在前面的代码中,我应该在
.label:
之前使用align伪指令,例如:align 4
.label:
如果是这样,使用SSE-2时是否足以在DWORD上对齐?
关于分支预测,是否有一种“优先的”方法来组织跳转指令使用的标签以帮助CPU,或者当今的CPU足够聪明,可以通过计算执行分支的次数来确定运行时的状态?
编辑
好的,这是一个具体示例-这是SSE-2的
strlen()
的开始:_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
当然,这不像对齐每个分支目标那么简单。如果我这样做,性能通常会变差,除非出现上述特定情况。
最佳答案
对齐优化
1.使用 .p2align <abs-expr> <abs-expr> <abs-expr>
代替align
。
使用其3个参数授予细粒度的控制
NOP
)。 2.将常用代码块的开头与高速缓存行大小边界对齐。
3.使用多字节
NOP
填充到reduce the time spent executing NOP
s。 /* nop */
static const char nop_1[] = { 0x90 };
/* xchg %ax,%ax */
static const char nop_2[] = { 0x66, 0x90 };
/* nopl (%[re]ax) */
static const char nop_3[] = { 0x0f, 0x1f, 0x00 };
/* nopl 0(%[re]ax) */
static const char nop_4[] = { 0x0f, 0x1f, 0x40, 0x00 };
/* nopl 0(%[re]ax,%[re]ax,1) */
static const char nop_5[] = { 0x0f, 0x1f, 0x44, 0x00, 0x00 };
/* nopw 0(%[re]ax,%[re]ax,1) */
static const char nop_6[] = { 0x66, 0x0f, 0x1f, 0x44, 0x00, 0x00 };
/* nopl 0L(%[re]ax) */
static const char nop_7[] = { 0x0f, 0x1f, 0x80, 0x00, 0x00, 0x00, 0x00 };
/* nopl 0L(%[re]ax,%[re]ax,1) */
static const char nop_8[] =
{ 0x0f, 0x1f, 0x84, 0x00, 0x00, 0x00, 0x00, 0x00};
/* nopw 0L(%[re]ax,%[re]ax,1) */
static const char nop_9[] =
{ 0x66, 0x0f, 0x1f, 0x84, 0x00, 0x00, 0x00, 0x00, 0x00 };
/* nopw %cs:0L(%[re]ax,%[re]ax,1) */
static const char nop_10[] =
{ 0x66, 0x2e, 0x0f, 0x1f, 0x84, 0x00, 0x00, 0x00, 0x00, 0x00 };
(对于x86,最多 10字节
NOP
。来源binutils-2.2.3。)分支预测优化
x86_64微架构/世代之间有很多差异。但是,可以将适用于所有准则的一组通用准则总结如下。 引用:Section 3 of Agner Fog's x86 micro-architecture manual。
1.展开循环,以避免迭代计数过高。
这实际上不适用于Haswell及以后的使用TAGE预测器的预测器,并且没有针对特定分支的专用环路检测逻辑。在Skylake上,对于没有其他分支的紧密外部循环中的内部循环而言,〜23的迭代次数可能是最坏的情况:从内部循环退出的次数最多,但跳闸次数很少,因此经常发生。展开可以通过缩短模式来提供帮助,但是对于非常高的循环行程计数,最后的单个错误预测会在很多行程中分期摊销,并且为此做任何事情都会花费不合理的展开费用。
2.坚持近距离/短距离跳跃。
通常在大多数CPU上都可以预测具有任意64位绝对地址的间接跳转。
但是Silvermont(英特尔的低功耗CPU)在目标距离超过4GB时预测间接跳转方面有一些限制,因此避免在虚拟地址空间的低32位加载/映射可执行文件和共享库可以避免这种情况。 。例如在GNU / Linux上,通过设置环境变量
LD_PREFER_MAP_32BIT_EXEC
。有关更多信息,请参阅英特尔的优化手册。 关于performance - x86-64组件的性能优化-对齐和分支预测,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/18113995/