Nik*_*hin 2 c c++ sse intel simd
我尝试与SSE合作,但遇到了一些奇怪的行为。
我编写了简单的代码,用于将两个字符串与SSE Intrinsics进行比较,然后运行它并起作用。但是后来我明白了,在我的代码中,指针之一仍然没有对齐,但是我使用了_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;
}
Run Code Online (Sandbox Code Playgroud)
我打印了两个指针的对齐方式,而其中一个沃尔码对齐了,但是第二个对齐了-不是。并且程序仍然正确且快速地运行。
然后我创建这样的综合测试:
//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);
}
Run Code Online (Sandbox Code Playgroud)
如我们所料,第一次尝试加载指针B时,它崩溃了。
有趣的事实是,如果我切换target到,sse4.2则我的实现is_equal会崩溃。
另一个有趣的事实是,如果我尝试将第二个指针而不是第一个指针对齐(因此第一个指针将不对齐,第二个-对齐),is_equal则将崩溃。
因此,我的问题是:“ is_equal如果启用了avx指令生成功能,为什么函数只在第一个指针对齐的情况下才能正常工作?”
UPD:这是C++代码。我使用MinGW64/g++, gcc version 4.9.2Windows x86 编译我的代码。
编译字符串: g++.exe main.cpp -Wall -Wextra -std=c++11 -O2 -Wcast-align -Wcast-qual -o main.exe
TL:DR:_mm_load_*可以将内在函数的负载(在编译时)折叠为其他指令的内存操作数。 矢量指令的AVX版本不需要对内存操作数进行对齐,除了专门对齐的加载/存储指令(如)外vmovdqa。
在向量指令的传统SSE编码(如pxor xmm0, [src1])中,未对齐的128位内存操作数将出错,除非使用特殊的未对齐的加载/存储指令(如movdqu/ movups)。
矢量指令(如)的VEX编码vpxor xmm1, xmm0, [src1]不会因为未对齐的内存而出错,除非需要对齐的加载/存储指令(如vmovdqa或vmovntdq)。
将_mm_loadu_si128与_mm_load_si128(和存储/ storeu的)内部沟通对准担保的编译器,但不强迫它实际上是发出一个独立的加载指令。(或者,如果它已经在寄存器中保存了数据,则什么也没有,就像解引用标量指针一样)。
当优化使用内部函数的代码时,按样规则仍然适用。可以将负载折叠到使用它的vector-ALU指令的内存操作数中,只要不引入故障风险即可。出于代码密度的原因,这是有利的,而且由于微融合,因此在CPU的某些部分中跟踪的uops也更少(请参见Agner Fog的microarch.pdf)。在处未启用执行此操作的优化过程-O0,因此未优化的代码构建可能会因未对齐的src1而出错。
(相反,这意味着_mm_loadu_*只能使用AVX折叠到内存操作数中,而不能使用SSE。因此,即使在CPU上与指针恰好对齐时movdqu一样快movqda,_mm_loadu也会损害性能,因为movqdu xmm1, [rsi]/ pxor xmm0, xmm1是2个融合域uops。 ,而发布的前端pxor xmm0, [rsi]只有1个,并且不需要暂存器。另请参见微融合和寻址模式)。
在这种情况下,按条件规则的解释是,在某些情况下,如果天真的将asm转换为错误,程序就不会出错。(或者使同一代码在未优化的构建中出错,但在优化的构建中不出错)。
这与浮点异常规则相反,在浮点异常规则中,编译器生成的代码仍必须引发C抽象机上可能发生的所有异常。这是因为存在定义良好的机制来处理FP异常,但不能处理段错误。
请注意,由于存储无法折叠成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)
Run Code Online (Sandbox Code Playgroud)
以SSE(可以在不支持AVX的CPU上运行的代码)为目标时,对齐的版本可以将负载折叠到其中por xmm1, [Aptr],但是未对齐的版本必须使用
movdqu xmm0, [Uptr]/ por xmm0, xmm1。如果y在OR之后仍需要旧值,则对齐的版本也可以这样做。
当靶向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
Run Code Online (Sandbox Code Playgroud)
请注意,您is_equal是memcmp。我认为glibc的memcmp在许多情况下都会比您的实现做得更好,因为它具有用于SSE4.1的手写asm版本以及其他版本,这些版本可以处理缓冲区彼此之间未对齐的各种情况。(例如,一个对齐,一个未对齐。)请注意,glibc代码是LGPLed,因此您可能无法仅复制它。如果用例具有通常对齐的较小缓冲区,则您的实现可能很好。从其他AVX代码调用VZEROUPPER之前也不需要它。
最终由编译器生成的字节循环进行清理肯定不是最佳选择。如果大小大于16个字节,请执行未对齐的加载,该加载以每个src的最后一个字节结束。重新比较已经检查过的一些字节没关系。
无论如何,一定要使用system基准测试您的代码memcmp。除了库实现之外,gcc还知道memcmp的作用,并且具有自己的内置定义,可以内联代码。