在 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,并进行了循环展开等。
我有一个循环,我用它来添加带进位的数字。
我想知道 .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,并进行了循环展开等。