【问题标题】:Alignment and SSE strange behaviour对齐和 SSE 奇怪的行为
【发布时间】:2016-11-21 11:00:13
【问题描述】:

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

我打印两个指针的对齐方式,其中一个 wal 对齐但第二个 - 不是。并且程序仍然可以正确快速地运行。

然后我像这样创建合成测试:

//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 将崩溃。

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

UPD:这是C++ 代码。我在 Windows、x86 下使用MinGW64/g++, gcc version 4.9.2 编译我的代码。

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

【问题讨论】:

  • 采用内存操作数(不包括对齐的移动)的 VEX 编码指令不需要对齐。指定 AVX 将使编译器使用 VEX 编码的指令。 IOW,当你打开 AVX 时它碰巧工作时,你得到了(不)幸运。如果 GCC 决定使用任何正常(对齐)移动,它仍然可能崩溃。
  • 如果负载被卷入一个参数,它会失去对齐要求(除了传统编码),反汇编确认
  • 但不一样,那里没有指令可以将负载卷入,与第一种情况不同。
  • @Olaf 这个问题对 C 和 C++ 都有效。
  • @Olaf 我只是说演员阵容对于这个问题并不重要。这个问题是关于 SSE 和对齐的,而不是关于正确的 C++ 编码风格。但无论如何,我无法阻止任何人对此进行吹毛求疵。

标签: c++ c intel sse simd


【解决方案1】:

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 规则仍然适用。负载可以折叠到使用它的向量 ALU 指令的内存操作数中,只要这不会引入错误的风险。这对于代码密度的原因是有利的,并且由于微融合(see Agner Fog's microarch.pdf),在部分 CPU 中跟踪的微指令也更少。 -O0 未启用执行此操作的优化通道,因此未优化的代码构建可能会出现未对齐的 src1 错误。

(相反,这意味着_mm_loadu_* 只能使用 AVX 折叠到内存操作数中,但不能使用 SSE。所以即使在 CPU 上 movdqumovqda 一样快时,指针恰好对齐, _mm_loadu 可能会影响性能,因为 movqdu xmm1, [rsi] / pxor xmm0, xmm1 是 2 个用于前端发布的融合域微指令,而 pxor xmm0, [rsi] 只有 1 个。并且不需要暂存寄存器。另见 Micro fusion and addressing modes) .

在这种情况下,as-if 规则的解释是,在某些情况下,如果将天真翻译成 asm 会出错,程序不会出错是可以的。 (或者相同的代码在未优化的构建中出错,但在优化的构建中没有出错)。

这与浮点异常的规则相反,在浮点异常的规则中,编译器生成的代码仍然必须引发 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 -mavxgcc -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_equalmemcmp。我认为 glibc 的 memcmp 在很多情况下会比你的实现做得更好,因为它有 hand-written asm versions for SSE4.1 和其他处理缓冲区相对于彼此未对齐的各种情况。 (例如,一个对齐,一个不对齐。)请注意,glibc 代码是 LGPLed,因此您可能无法仅复制它。如果您的用例具有通常对齐的较小缓冲区,则您的实现可能很好。在从其他 AVX 代码调用之前不需要 VZEROUPPER 也很好。

最后要清理的编译器生成的字节循环绝对不是最佳的。如果大小大于 16 字节,则执行在每个 src 的最后一个字节处结束的未对齐加载。重新比较一些已经检查过的字节并不重要。

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

【讨论】:

  • 很抱歉这么晚的评论,但为什么如果我不对齐第一个指针,而不是程序稳定崩溃?这是因为我很不幸,编译器决定不使用 VEX 编码的指令?还是有其他问题?
  • @NikitaSivukhin: vmovdqa 仍然需要对齐。如果您希望未对齐的指针只是潜在的性能问题,而不是潜在的崩溃,请使用_mm_loadu_...。 (编译器将使用vmovdqu 进行加载/存储。)
  • 好吧,我忘记了... =(但这对您来自 Godbold 的 asm 来说很奇怪:vmovdqaXMMWORD PTR [rsi+16]XMMWORD PTR [rdi] 一起使用,这是两个不同的指针 - 一个在src_1 和 src_2 中的一个。其中一个未对齐,此代码必须崩溃。那么,这是真的吗?在我的计算机上生成的代码略有不同还是我错了?
  • @NikitaSivukhin:是的,如果任何一个指针 arg 未对齐,gcc 5.3 生成的代码似乎都会出错。哦,我想这不是我的回答文字所说的。这就是你想要指出的吗?我将godbolt链接更改为像您使用的那样使用gcc 4.9.2。它仍然针对 x86-64 SysV ABI,而不是 mingw,但应该像您描述的那样运行。您当然可以通过objdumpgcc -S 检查自己; Godbolt 只是一个用于清理编译器 asm 输出的文本过滤器。
猜你喜欢
  • 1970-01-01
  • 2012-09-03
  • 1970-01-01
  • 2018-06-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多