ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

c – 对齐和SSE奇怪的行为

2019-10-03 01:04:53  阅读:220  来源: 互联网

标签:c-3 c intel sse simd


我尝试与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.

有趣的事实是,如果我将目标切换到sse4.2,那么我的is_equal实现将崩溃.

另一个有趣的事实是,如果我尝试对齐第二个指针而不是第一个(因此第一个指针将不对齐,第二个对齐),那么is_equal将崩溃.

所以,我的问题是:“为什么is_equal函数工作正常,只有第一个指针对齐,如果我启用avx指令生成?”

UPD:这是C代码.我在Windows,x86下使用MinGW64 / g,gcc版本4.9.2编译我的代码.

编译字符串:g .exe main.cpp -Wall -Wextra -std = c 11 -O2 -Wcast-align -Wcast-qual -o main.exe

解决方法:

TL:DR:来自_mm_load_ *内在函数的加载可以(在编译时)折叠到其他指令的内存操作数中. The AVX versions of vector instructions don’t require alignment for memory operands,除了特殊对齐的加载/存储指令,如vmovdqa.

在矢量指令的传统SSE编码中(如pxor xmm0,[src1]),未对齐的128位存储器操作数将出现故障,除非使用特殊的未对齐加载/存储指令(如movdqu/movups).

向量指令的VEX-encoding(如vpxor xmm1,xmm0,[src1])不会因未对齐的内存而出错,除了需要对齐的加载/存储指令(如vmovdqavmovntdq).

_mm_loadu_si128与_mm_load_si128(和store / storeu)内在函数将对齐保证传递给编译器,但不强制它实际发出独立的加载指令. (或者任何东西,如果它已经在寄存器中有数据,就像解除引用标量指针一样).

优化使用内在函数的代码时,as-if规则仍然适用.可以将负载折叠到使用它的vector-ALU指令的内存操作数中,只要不引入故障风险即可.这有利于代码密度的原因,并且由于微融合(see Agner Fog’s microarch.pdf),在CPU的某些部分中跟踪的uops也更少.执行此操作的优化过程未在-O0启用,因此未经优化的代码构建可能会与未对齐的src1发生故障.

(相反,这意味着_mm_loadu_ *只能用AVX折叠成内存操作数,但不能用SSE折叠.所以即使在指针碰巧对齐时movdqu和movqda一样快的CPU上,_mm_loadu也会损害性能,因为movqdu xmm1, [rsi] / pxor xmm0,xmm1是前端发出的2个融合域uops,而pxor xmm0,[rsi]仅为1.并且不需要临时寄存器.另请参阅Micro fusion and addressing modes).

在这种情况下对as-if规则的解释是,在asm的naive转换出现故障的某些情况下,程序可以不出错. (或者相同的代码在未优化的构建中出现故障,但在优化的构建中没有故障).

这与浮点异常的规则相反,其中编译器生成的代码仍然必须引发在C抽象机器上发生的任何和所有异常.这是因为有明确定义的机制来处理FP异常,但不是处理段错误.

注意,由于存储不能折叠到ALU指令的内存操作数中,因此store(而不是storeu)内在函数将编译成代码faults with unaligned pointers even when compiling for an AVX target.

具体来说:考虑以下代码片段:

// 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加载第一个操作数并折叠第二个,而不是相反.

您可以在the Godbolt compiler explorer的代码中看到这种情况发生.不幸的是,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在许多情况下会比你的实现更好,因为它有hand-written asm versions for SSE4.1和其他处理各种缓冲区相对于彼此错位的情况. (例如,一个对齐,一个不对齐.)请注意,glibc代码是LGPLed,因此您可能无法复制它.如果您的用例具有通常对齐的较小缓冲区,则您的实现可能很好.在从其他AVX代码调用它之前不需要VZEROUPPER也不错.

编译器生成的字节循环在最后清理肯定是次优的.如果大小大于16个字节,请执行未对齐的加载,该加载以每个src的最后一个字节结束.重新比较一些已经检查过的字节并不重要.

无论如何,绝对要用系统memcmp对您的代码进行基准测试.除了库实现之外,gcc知道memcmp的作用,并且有自己的内置定义,它可以内联代码.

标签:c-3,c,intel,sse,simd
来源: https://codeday.me/bug/20191003/1845841.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有