对齐和SSE奇怪的行为
我尝试与SSE合作,我遇到了一些奇怪的行为。
我编写简单的代码来比较两个字符串与SSE内在函数,运行它并且它工作。 但后来我明白了,在我的代码中,一个指针仍未对齐,但我使用_mm_load_si128
指令,这需要指针在16字节边界上对齐。
//Compare two different, not overlapping piece of memory __attribute((target("avx"))) int is_equal(const void* src_1, const void* src_2, size_t size) { //Skip tail for right alignment of pointer [head_1] const char* head_1 = (const char*)src_1; const char* head_2 = (const char*)src_2; size_t tail_n = 0; while (((uintptr_t)head_1 % 16) != 0 && tail_n < size) { if (*head_1 != *head_2) return 0; head_1++, head_2++, tail_n++; } //Vectorized part: check equality of memory with SSE4.1 instructions //src1 - aligned, src2 - NOT aligned const __m128i* src1 = (const __m128i*)head_1; const __m128i* src2 = (const __m128i*)head_2; const size_t n = (size - tail_n) / 32; for (size_t i = 0; i < n; ++i, src1 += 2, src2 += 2) { printf("src1 align: %d, src2 align: %d\n", align(src1) % 16, align(src2) % 16); __m128i mm11 = _mm_load_si128(src1); __m128i mm12 = _mm_load_si128(src1 + 1); __m128i mm21 = _mm_load_si128(src2); __m128i mm22 = _mm_load_si128(src2 + 1); __m128i mm1 = _mm_xor_si128(mm11, mm21); __m128i mm2 = _mm_xor_si128(mm12, mm22); __m128i mm = _mm_or_si128(mm1, mm2); if (!_mm_testz_si128(mm, mm)) return 0; } //Check tail with scalar instructions const size_t rem = (size - tail_n) % 32; const char* tail_1 = (const char*)src1; const char* tail_2 = (const char*)src2; for (size_t i = 0; i < rem; i++, tail_1++, tail_2++) { if (*tail_1 != *tail_2) return 0; } return 1; }
我打印两个指针的对齐,其中一个指针对齐,但第二个 – 不是。 并且程序仍然正常且快速地运行。
然后我创建这样的综合测试:
//printChars128(...) function just print 16 byte values from __m128i const __m128i* A = (const __m128i*)buf; const __m128i* B = (const __m128i*)(buf + rand() % 15 + 1); for (int i = 0; i < 5; i++, A++, B++) { __m128i A1 = _mm_load_si128(A); __m128i B1 = _mm_load_si128(B); printChars128(A1); printChars128(B1); }
正如我们所料,它在第一次迭代时崩溃,当尝试加载指针B.
有趣的事实是,如果我将target
切换到sse4.2
那么我的is_equal
实现将崩溃。
另一个有趣的事实是,如果我尝试对齐第二个指针而不是第一个(因此第一个指针将不对齐,第二个对齐),那么is_equal
将崩溃。
所以,我的问题是:“为什么is_equal
函数工作正常,只有第一个指针对齐,如果我启用avx
指令生成?”
UPD:这是C++
代码。 我使用MinGW64/g++, gcc version 4.9.2
在Windows,x86下编译我的代码。
编译字符串: g++.exe main.cpp -Wall -Wextra -std=c++11 -O2 -Wcast-align -Wcast-qual -o main.exe
TL:DR :来自_mm_load_*
内在函数的加载可以(在编译时)折叠到其他指令的内存操作数中。 除了专门对齐的加载/存储指令(如vmovdqa
之外, AVX版本的向量指令不需要对齐存储器操作数 。
在矢量指令的传统SSE编码中(如pxor xmm0, [src1]
),未对齐的128位存储器操作数将出现故障,除非使用特殊的未对齐加载/存储指令(如movdqu
/ movups
)。
向量指令的VEX编码 (如vpxor xmm1, xmm0, [src1]
)不会因未对齐的内存而vpxor xmm1, xmm0, [src1]
除了需要对齐的加载/存储指令(如vmovdqa
或vmovntdq
)。
_mm_loadu_si128
与_mm_load_si128
(和store / storeu)内在函数将对齐保证传递给编译器,但不强制它实际发出独立的加载指令。 (或者任何东西,如果它已经在寄存器中有数据,就像解除引用标量指针一样)。
优化使用内在函数的代码时,as-if规则仍然适用。 可以将负载折叠到使用它的vector-ALU指令的内存操作数中,只要不引入故障风险即可。 这对于代码密度的原因是有利的,并且由于微融合,在CPU的部分区域中跟踪的微量也更少(参见Agner Fog的microarch.pdf) 。 执行此操作的优化过程未在-O0
启用,因此未经优化的代码构建可能会因未对齐的src1而出现故障。
(相反,这意味着_mm_loadu_*
只能用AVX折叠成内存操作数,但不能用SSE折叠。所以即使在指针碰巧对齐时_mm_loadu
和movqda
一样快的CPU上, _mm_loadu
也会损害性能,因为movqdu xmm1, [rsi]
/ pxor xmm0, xmm1
是前端发出的2个融合域pxor xmm0, [rsi]
而pxor xmm0, [rsi]
仅为1.并且不需要临时寄存器。另请参见微融合和寻址模式 )。
在这种情况下对as-if规则的解释是,在asm的naive转换出现故障的某些情况下,程序可以不出错。 (或者相同的代码在未优化的构建中出现故障,但在优化的构建中没有故障)。
这与浮点exception的规则相反,其中编译器生成的代码仍然必须引发在C抽象机器上发生的任何和所有exception。 这是因为有明确定义的处理FPexception的机制,但不适用于处理段错误。
请注意,由于存储不能折叠到ALU指令的内存操作数中,因此即使在编译AVX目标时 , store
(不是storeu
)内在函数也会编译成使用未对齐指针的代码。
具体来说:考虑以下代码片段:
// aligned version: y = ...; // assume it's in xmm1 x = _mm_load_si128(Aptr); // Aligned pointer res = _mm_or_si128(y, x); // unaligned version: the same thing with _mm_loadu_si128(Uptr)
当针对SSE(可以在没有AVX支持的CPU上运行的代码)时,对齐版本可以将负载折叠到por xmm1, [Aptr]
,但未对齐版本必须使用
movdqu xmm0, [Uptr]
/ por xmm0, xmm1
。 如果在OR之后仍然需要y
的旧值,则对齐的版本也可以这样做。
当定位AVX( gcc -mavx
或gcc -march=sandybridge
或更高版本)时,发出的所有向量指令(包括128位)将使用VEX编码。 所以你从同一个_mm_...
内在函数中获得不同的asm。 两个版本都可以编译为vpor xmm0, xmm1, [ptr]
。 (并且3操作数非破坏性特征意味着实际发生这种情况,除非多次使用加载的原始值)。
ALU指令只有一个操作数可以是内存操作数 ,因此在您的情况下必须单独加载。 当第一个指针未对齐时你的代码出错,但不关心第二个指针的对齐,所以我们可以得出结论,gcc选择用vmovdqa
加载第一个操作数并折叠第二个,而不是反之亦然。
您可以在Godbolt编译器资源管理器的代码中看到这种情况。 不幸的是,gcc 4.9(和5.3)将其编译为某种次优的代码,该代码在al
中生成返回值然后对其进行测试,而不是仅仅分支来自vptest
的标志:( clang-3.8做得更好。
.L36: add rdi, 32 add rsi, 32 cmp rdi, rcx je .L9 .L10: vmovdqa xmm0, XMMWORD PTR [rdi] # first arg: loads that will fault on unaligned xor eax, eax vpxor xmm1, xmm0, XMMWORD PTR [rsi] # second arg: loads that don't care about alignment vmovdqa xmm0, XMMWORD PTR [rdi+16] # first arg vpxor xmm0, xmm0, XMMWORD PTR [rsi+16] # second arg vpor xmm0, xmm1, xmm0 vptest xmm0, xmm0 sete al # generate a boolean in a reg test eax, eax jne .L36 # then test&branch on it. /facepalm
请注意,您的is_equal
是memcmp
。 我认为glibc的memcmp在许多情况下会比你的实现更好,因为它有SSE4.1的手写asm版本和其他处理缓冲区相对于彼此错位的情况。 (例如,一个对齐,一个不对齐。)请注意,glibc代码是LGPLed,因此您可能无法复制它。 如果您的用例具有通常对齐的较小缓冲区,则您的实现可能很好。 在从其他AVX代码调用它之前不需要VZEROUPPER也不错。
编译器生成的字节循环在最后清理绝对是次优的。 如果大小大于16个字节,请执行未对齐的加载,该加载以每个src的最后一个字节结束。 重新比较一些已经检查过的字节并不重要。
无论如何,绝对要使用系统memcmp
对您的代码进行基准测试。 除了库实现之外,gcc知道memcmp的作用,并且有自己的内置定义,它可以内联代码。