gpt4 book ai didi

x86-64 程序集的性能优化 - 对齐和分支预测

转载 作者:行者123 更新时间:2023-12-04 03:27:08 26 4
gpt4 key购买 nike

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

到目前为止,我已经设法在性能方面获得了出色的结果,但是当我尝试进行更多优化时,有时会出现奇怪的行为。

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

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

我知道,即使使用相同的架构(x86-64),不同的 CPU 也有不同的分支预测算法。

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

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

_func:
; ... Some code ...
test rax, rax
jz .label
; ... Some code ...
ret
.label:
; ... Some code ...
ret

在前面的代码中,我应该在 .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

当然,并不是把每个分支目标都对齐那么简单。如果我这样做,性能通常会变得更糟,除非像上面的某些特定情况。

最佳答案

对齐优化

1. 使用 .p2align <abs-expr> <abs-expr> <abs-expr> 而不是 align .

使用其 3 个参数授予细粒度控制

  • param1 - 对齐到什么边界。
  • param2 - 用什么(零或NOP s)填充填充。
  • param3 - 如果填充将超过指定的字节数,则不对齐。

  • 2. 将经常使用的代码块的开始与高速缓存行大小边界对齐。
  • 这增加了整个代码块位于单个高速缓存行中的机会。一旦加载到 L1 缓存中,就可以完全运行,而无需访问 RAM 来获取指令。这对于具有大量迭代的循环非常有益。

  • 3.使用多字节 NOP s 填充到 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. 展开循环以避免略微过高的迭代次数。
  • 循环检测逻辑保证仅适用于具有 的循环。 < 64 迭代。这是因为分支指令在单向运行时被识别为具有循环行为 n-1 次,然后走另一条路 1 时间,对于任何 n 高达 64。

    这并不真正适用于 Haswell 和以后使用 TAGE 预测器并且没有针对特定分支的专用循环检测逻辑的预测器。在 Skylake 上,对于没有其他分支的紧外循环内部的内循环来说,迭代计数约为 23 可能是最坏的情况:从内循环的退出大多数时候会出现错误预测,但行程计数非常低,以至于它经常发生。展开可以通过缩短模式来提供帮助,但是对于非常高的循环行程计数,最后的单个错误预测会在很多行程中摊销,并且需要进行不合理的展开才能对此进行任何处理。

  • 2. 坚持近跳/短跳。
  • 无法预测远跳转,即管道总是在远跳转到新代码段 (CS:RIP) 时停止。无论如何,基本上没有理由使用远跳,所以这几乎不相关。

    在大多数 CPU 上通常可以预测具有任意 64 位绝对地址的间接跳转。

    但是 Silvermont(英特尔的低功耗 CPU)在预测目标距离超过 4GB 时的间接跳转方面存在一些限制,因此通过在低 32 位虚拟地址空间中加载/映射可执行文件和共享库来避免这种情况可能是一个胜利.例如在 GNU/Linux 上通过设置环境变量 LD_PREFER_MAP_32BIT_EXEC .有关更多信息,请参阅英特尔的优化手册。
  • 关于x86-64 程序集的性能优化 - 对齐和分支预测,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/41094514/

    26 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com