在 Intel/AMD 64 中几乎从未采用过的非对齐 Jcc 是否会受到惩罚?

Is there a penalty in having a non-aligned Jcc which is nearly never taken in Intel/AMD 64?

我有一个循环,我用它来添加带进位的数字。

我想知道 .done: 对齐是否会给我带来什么?毕竟,每次调用该函数它只会在那里分支一次。我知道 C 编译器可能会对齐所有受循环影响的分支 。但我认为它不应该造成任何惩罚(特别是因为我们现在每天都有相当大的指令缓存)。

//    // corresponding C function declaration
//    int add(uint64_t * a, uint64_t const * b, uint64_t const * c, uint64_t size);
//
// Compile with:   gcc -c add.s -o add.o
//
// WARNING: at this point I've not worked on the input registers & registers to save
//          do not attempt to use in your C program with this very code.

    .text
    .p2align    4,,15
    .globl      add
    .type       add, @function
add:
    test        %rcx, %rcx
    je          .done
    clc
    xor         %rbp, %rbp

    .p2align    4,,10
    .p2align    3
.loop:
    mov         (%rax, %rbp, 8), %rdx
    adc         (%rbx, %rbp, 8), %rdx
    mov         %rdx, (%rdi, %rbp, 8)
    inc         %rbp
    dec         %rcx
    jrcxz       .done
    jmp         .loop

    // -- is alignment here necessary? --
.done:
    setc        %al
    movzx       %al, %rax
    ret

Intel 或 AMD 是否有关于此特定案例的明确文档?


我实际上决定通过删除循环来简化,因为我只有 3 种大小(128、256 和 512),因此编写展开的循环很容易。但是,我只需要一个添加,所以我真的不想为此使用 GMP。

这是应该在您的 C 程序中运行的最终代码。这个专门用于 512 位。只需对 256 位版本使用三个 add_with_carry,对 128 位版本仅使用一个。

//    // corresponding C function declaration
//    void add512(uint64_t * dst, uint64_t const * src);
//

    .macro add_with_carry offset
        mov         \offset(%rsi), %rax
        adc         %rax, \offset(%rdi)
    .endm

    .text
    .p2align    4,,15
    .globl      add512
    .type       add512, @function
add512:
    mov         (%rsi), %rax
    add         %rax, (%rdi)

    add_with_carry 8
    add_with_carry 16
    add_with_carry 24
    add_with_carry 32
    add_with_carry 40
    add_with_carry 48
    add_with_carry 56

    ret

请注意,我不需要 clc,因为我第一次使用 add(忽略进位)。我还把它添加到目的地(即 C 中的 dest[n] += src[n]),因为我的代码中不太可能需要一个副本。

偏移量允许我不增加指针,每次添加它们只使用一个额外的字节。

神圣的时钟周期蝙蝠侠,当你在 jmp 上使用 jrcxz 而不是在 dec 之后仅使用 jnz 时,你问的是效率问题?

如果您使用 lea 1(%rcx), %rcx 完全 避免 FLAGS 写入,您只会考虑慢 loop 或稍慢 jrcxz . dec 写入除 CF 之外的所有标志,CF 在 Sandybridge 之前曾导致 CPU 上的 ADC 循环中出现部分标志停顿,但现在没问题了。 dec/jnz 循环非常适合现代 CPU 上的 ADC 循环。您可能希望避免 adc and/or 存储的索引寻址模式(可能带有循环展开),因此 adc 可以微融合负载,因此存储地址 uop可以 运行 在 Haswell 及更高版本的端口 7 上。您可以索引 mov 负载相对于您使用 LEA 递增的其他指针之一。


但无论如何不,从未采用的分支目标的对齐是无关紧要的。除了通常的代码之外,总是失败的分支的失败路径的对齐也是如此-对齐/解码器效果。

很少采用分支目标的对齐也不是什么大问题;代价可能是前端的额外周期,或者在一个时钟周期中准备好预解码的指令更少。 因此,在实际执行该路径的情况下,我们讨论的是在前端提前 1 个时钟周期。这就是为什么对齐循环顶部很重要,尤其是在没有循环缓冲区的 CPU。 (And/or 除了极少数情况外,没有 uop 缓存和其他隐藏前端气泡的东西。)。

正确的分支预测通常会隐藏 1 个循环,但通常留下一个循环会导致 错误地 预测分支,除非迭代计数很小且每次都相同。第一个周期可能只会在 16 字节的提取块末尾附近获取一条有用的指令(如果第一条指令跨 16 字节边界拆分,则甚至为零),后面的指令只会在下一个周期加载。有关 Agner Fog 的微架构指南和 asm 优化指南,请参阅 https://agner.org/optimize/。 IDK 最近他更新了 asm 优化手册中的对齐指南;我主要只是看他对新微架构的微架构指南的更新。

一般来说,流水线阶段之间的 uop 缓存和缓冲区使代码对齐 很多 不像以前那样重要。将循环顶部对齐 8 或 16 仍然是一个好主意,但否则通常不值得在将要执行的任何地方放置额外的 nop

你可以想象它可能会产生更大影响的情况,比如如果以前的代码永远不会执行,对齐缓存行或页面边界可以避免触及否则冷的缓存行或页面。您的代码不会发生这种情况;在您的跳转目标之前有 "hot" 条指令少于 64 字节。但这与通常的代码对齐目标不同。


更多代码审查:

RBP是调用保留寄存器。如果您想从 C 中调用它,请选择一个寄存器,如 RA/C/DX、RSI、RDI 或 R8..R11,您不使用任何寄存器。或者对于 Windows x64,有更少的调用破坏 "legacy" regs(不需要 REX 前缀)。看起来您的所有循环指令都需要 64 位操作数大小的 REX 前缀。

clc 是不必要的:xor %ebp, %ebp 到零 RBP 已经清除 CF。说到这一点,32 位操作数大小对于异或归零更有效。它节省了代码大小。

您还可以通过从数组的 end 开始索引来避免循环中的 dec,负索引向零计数。例如rdi += len; rsi += len; 等等。 RCX = -len。所以 inc %rcx / jnz 作为你的循环条件, 作为你的索引增量。

但是就像我上面说的,你可能会更好地使用 lea 来获得一个指针增量并相对于它索引你的其他数组。 (p1 -= p2,然后使用 *(p1 + p2)*p2,并在 asm 中将两者递增一个 p2++。)因此您可能仍需要一个单独的计数器。

您可以调用 GMP 库函数而不是编写自己的扩展精度循环。他们为许多不同的 x86 微体系结构手动调整了 asm,并进行了循环展开等。