x86 SIMD 指令汇编中的 16 字节对齐(无 C 内在函数)
x86 SIMD instructions 16 byte alignment in assembly (Without C intrinsics)
假设我有一个8 字节 元素未知长度 的数组从内存传递到我的汇编函数。我想对其进行一些 128 位 SIMD 操作(最高 SSE4)。内存最好是16字节对齐的。所以我会检查数组是否对齐,然后根据使用 movaps 或 movups.
我知道您可以使用以下方法检查 16 字节对齐:
test dil, 0xf ; rdi stores address of array
如果它不是 16 字节对齐的,是否也检查它是否是 8 字节对齐的(这意味着它是 8 的奇数倍)是否好或有用?
test dil, 0x7 ; ZF=1 here after rdi&0xf !=0 implies rdi%16 == 8
如果这是真的,那么我是否应该对数组的第一个元素执行额外的步骤,然后 movaps 来加载数组元素?否则我应该只使用未对齐的操作,例如 movups?
是这样的吗?
如果您的数组通常 以 16 位对齐,最好不要做更多的检查来寻找奇数开头的情况,只需使用您的未对齐版本,除非它是由于某种原因更糟。
但是,如果它们通常按 8 对齐(但不知道它们是否按 16 对齐),那么 您也许可以只检查按 8 对齐和无分支处理对齐案例的可能未对齐的第一次迭代,见下文。 (否则就退回到完全未对齐的情况。)
如果重叠不是问题(例如 c[] = a[]+b[],或类似 memset 的存储或其他),一个好的技术是 always 用未对齐的 load/store 做第一个向量,然后前进到第一个对齐的向量 (add rdi, 16
/ and rdi, -16
)。如果输入对齐,则不会重叠。否则,它部分重叠并且存储缓冲区 + L1d 缓存有效地处理它。
这使对齐案例的成本保持最低,并避免了分支预测错误的可能性。
将指针 up/down 舍入到对齐边界很便宜,只是一个 and
,但是您确实有剥离循环体的整个副本的代码大小成本。因此,就启动开销而言,它并非完全免费,但至少这种启动开销可以与数据缓存未命中重叠。
但请注意,许多 SIMD 函数具有多个指针输入,这些输入可能彼此不对齐。在这种情况下,标准建议是对齐输出并继续使用 movups
作为输入。尽管如果前端是瓶颈,您可能会选择达到输入的对齐边界,这样您就可以将内存源操作数折叠到 ALU 指令中,例如 xorps xmm0, [rdi]
并使用 movups
store.
但如果前端以外的任何东西,例如高速缓存或内存吞吐量,都是瓶颈,那么您更经常希望对齐目标。 Intel 的优化手册对此有一些建议。部分原因是负载吞吐量通常是存储吞吐量的 2 倍(直到 IceLake),因此负载硬件可以更容易地吸收拆分负载的额外工作。此外,用更少的存储存储一个完整的缓存行可以帮助减少一行被逐出(写回)但随后再次存储到它并且它必须被获取+弄脏并最终再次写回的情况,而不是仅仅被获取。
假设我有一个8 字节 元素未知长度 的数组从内存传递到我的汇编函数。我想对其进行一些 128 位 SIMD 操作(最高 SSE4)。内存最好是16字节对齐的。所以我会检查数组是否对齐,然后根据使用 movaps 或 movups.
我知道您可以使用以下方法检查 16 字节对齐:
test dil, 0xf ; rdi stores address of array
如果它不是 16 字节对齐的,是否也检查它是否是 8 字节对齐的(这意味着它是 8 的奇数倍)是否好或有用?
test dil, 0x7 ; ZF=1 here after rdi&0xf !=0 implies rdi%16 == 8
如果这是真的,那么我是否应该对数组的第一个元素执行额外的步骤,然后 movaps 来加载数组元素?否则我应该只使用未对齐的操作,例如 movups?
是这样的吗?
如果您的数组通常 以 16 位对齐,最好不要做更多的检查来寻找奇数开头的情况,只需使用您的未对齐版本,除非它是由于某种原因更糟。
但是,如果它们通常按 8 对齐(但不知道它们是否按 16 对齐),那么 您也许可以只检查按 8 对齐和无分支处理对齐案例的可能未对齐的第一次迭代,见下文。 (否则就退回到完全未对齐的情况。)
如果重叠不是问题(例如 c[] = a[]+b[],或类似 memset 的存储或其他),一个好的技术是 always 用未对齐的 load/store 做第一个向量,然后前进到第一个对齐的向量 (add rdi, 16
/ and rdi, -16
)。如果输入对齐,则不会重叠。否则,它部分重叠并且存储缓冲区 + L1d 缓存有效地处理它。
这使对齐案例的成本保持最低,并避免了分支预测错误的可能性。
将指针 up/down 舍入到对齐边界很便宜,只是一个 and
,但是您确实有剥离循环体的整个副本的代码大小成本。因此,就启动开销而言,它并非完全免费,但至少这种启动开销可以与数据缓存未命中重叠。
但请注意,许多 SIMD 函数具有多个指针输入,这些输入可能彼此不对齐。在这种情况下,标准建议是对齐输出并继续使用 movups
作为输入。尽管如果前端是瓶颈,您可能会选择达到输入的对齐边界,这样您就可以将内存源操作数折叠到 ALU 指令中,例如 xorps xmm0, [rdi]
并使用 movups
store.
但如果前端以外的任何东西,例如高速缓存或内存吞吐量,都是瓶颈,那么您更经常希望对齐目标。 Intel 的优化手册对此有一些建议。部分原因是负载吞吐量通常是存储吞吐量的 2 倍(直到 IceLake),因此负载硬件可以更容易地吸收拆分负载的额外工作。此外,用更少的存储存储一个完整的缓存行可以帮助减少一行被逐出(写回)但随后再次存储到它并且它必须被获取+弄脏并最终再次写回的情况,而不是仅仅被获取。