如何在C++03中交换两个__m128i变量,给定它是一个不透明类型和一个数组?

3

如何最佳实践地交换__m128i变量?

背景是一个在Sun Studio 12.2下的编译错误,这是一个C++03编译器。 __m128i是与MMX和SSE指令一起使用的不透明类型,通常是unsigned long long[2]。 C ++03不支持交换数组,并且在编译器下std:swap(__m128i a, __m128i b)会失败。


以下是一些相关问题,它们没有完全符合本题。它们并不适用,因为std::vector不可用。

2个回答

2

通过memcpy进行交换?

#include <emmintrin.h>
#include <cstring>

template<class T>
void memswap(T& a, T& b)
{
    T t;
    std::memcpy(&t, &a, sizeof(t));
    std::memcpy(&a, &b, sizeof(t));
    std::memcpy(&b, &t, sizeof(t));
}

int main() {
    __m128i x;
    __m128i y;
    memswap(x, y);
    return 0;
}

2
如果你要编写自定义的交换函数,只需使用按值赋值,因为这对于__m128i值是最优的。gcc确实可以优化掉memcpy并将值保留在寄存器中(使用一个接受两个__m128i参数并返回__m128i的测试函数),但ICC13则不行。简单的赋值对优化的负面影响要小得多。例如,ICC13没有问题。 - Peter Cordes
即使使用gcc在__m128i上使用memswap时,如果这些值已经在内存中,也可能会出现问题。请参阅我的答案。 - Peter Cordes

2
这似乎不是最佳实践问题,而是您需要解决严重破坏内在函数的实现的问题。如果__m128i tmp = a;不能编译,那就相当糟糕。
如果您要编写自定义交换函数,请保持简单。 __m128i是一个POD类型,适合于单个矢量寄存器。不要做任何会鼓励编译器将其溢出到内存的事情。一些编译器甚至会为微不足道的测试用例生成非常可怕的代码,即使是gcc / clang也可能会因为优化大型复杂函数中的memcpy而出错。
由于编译器无法处理构造函数,只需使用普通初始化程序声明一个tmp变量,并使用=赋值进行复制。这在任何支持__m128i的编译器中都可以高效地工作,并且是一种常见模式。
从内存中的值直接赋值/复制,就像_mm_store_si128 / _mm_load_si128一样:即movdqa对齐存储/加载,如果在不对齐的地址上使用,则会失败。 (当然,优化可能导致加载折叠到另一个矢量指令的内存操作数中,或者根本不发生存储。)
// alternate names: assignment_swap
// or swap128, but then the name doesn't fit for __m256i...

// __m128i t(a) errors, so just use simple initializers / assignment
template<class T>
void vecswap(T& a, T& b) {
    // T t = a;     // Apparently SunCC even choked on this
    T t;
    t = a;
    a = b;
    b = t;
}

测试用例:即使使用像ICC13这样的老旧编译器,也能够获得最优代码,尽管该编译器在memcpy版本上表现很差。来自Godbolt编译器探险家,使用icc13 -O3的汇编输出。
__m128i test_return2nd(__m128i x, __m128i y) {
    vecswap(x, y);
    return x;
}

    movdqa    xmm0, xmm1
    ret                    # returning the 2nd arg, which was in xmm1


__m128i test_return1st(__m128i x, __m128i y) {
    vecswap(x, y);
    return y;
}

    ret                   # returning the first arg, already in xmm0

使用memswap,您可以得到类似以下内容的效果:
return1st_memcpy(__m128i, __m128i):        ## ICC13 -O3
    movdqa    XMMWORD PTR [-56+rsp], xmm0
    movdqa    XMMWORD PTR [-40+rsp], xmm1    # spill both
    movaps    xmm2, XMMWORD PTR [-56+rsp]    # reload x
    movaps    XMMWORD PTR [-24+rsp], xmm2    # copy x to tmp
    movaps    xmm0, XMMWORD PTR [-40+rsp]    # reload y
    movaps    XMMWORD PTR [-56+rsp], xmm0    # copy y to x
    movaps    xmm0, XMMWORD PTR [-24+rsp]    # reload tmp
    movaps    XMMWORD PTR [-40+rsp], xmm0    # copy tmp to y
    movdqa    xmm0, XMMWORD PTR [-40+rsp]    # reload y
    ret                                      # return y

这几乎是你能想象到的在交换两个寄存器时最大的溢出/重载量,因为icc13根本不会在内联的memcpy之间进行优化,甚至不记得寄存器中剩下了什么。

交换已经存储在内存中的值

即使是gcc使用memcpy版本也会生成更糟糕的代码。它使用64位整数加载/存储来进行复制,而不是128位矢量加载/存储。如果你正要加载向量(存储转发停顿),这是可怕的,否则只是不好(需要更多的微操作来完成同样的工作)。
// the memcpy version of this compiles badly
void test_mem(__m128i *x, __m128i *y) {
    vecswap(*x, *y);
}
    # gcc 5.3 and ICC13 make the same code here, since it's easy to optimize
    movdqa  xmm0, XMMWORD PTR [rdi]
    movdqa  xmm1, XMMWORD PTR [rsi]
    movaps  XMMWORD PTR [rdi], xmm1
    movaps  XMMWORD PTR [rsi], xmm0
    ret

// gcc 5.3 with memswap instead of vecswap.  ICC13 is similar
test_mem_memcpy(long long __vector(2)*, long long __vector(2)*):
    mov     rax, QWORD PTR [rdi]
    mov     rdx, QWORD PTR [rdi+8]
    mov     r9, QWORD PTR [rsi]
    mov     r10, QWORD PTR [rsi+8]
    mov     QWORD PTR [rdi], r9
    mov     QWORD PTR [rdi+8], r10
    mov     QWORD PTR [rsi], rax
    mov     QWORD PTR [rsi+8], rdx
    ret

1
谢谢Peter,这正是我在寻找的见解。最后一个未决问题:我应该为__m128i提供std::swap专门化,还是只留下它作为一个独立的函数? - jww
1
最好将其称为vecswap,这样您就可以在派生类型上使用它,例如Agner Fog的矢量类库包装器,如Vec4i。如果您进行专门化,您可能需要对__m128i__m128d__m128__m256*__m512*和其他架构的SIMD类型进行专门化,(如ARM NEON或其他)。这很笨重。我更愿意使用一个不同的函数,我确信它总是轻量级的,并且能够良好地优化。如果您确定只需要少数类型,则值得考虑进行专门化。 - Peter Cordes
1
信不信由你,这导致了在SunCC下的原始编译问题:T t = a; a = b; b = t;。我不得不使用 T t; t=a, a=b, b=t; 来让编译器停止尝试初始化它。 - jww
@jww:哇,那个编译器会在很多代码上卡住。我猜它也会在__m128i v = _mm_add_epi32(a,b);这一行卡住?对于大多数代码,我认为我会考虑它已经坏了,而不是为了它的好处重写任何重要的代码。 - Peter Cordes
@PeterCordes vecswap()std::swap() 有什么不同? - Leon
@Leon:我不知道,我没有SunCC。也许它是相同的,因为jww必须修改我的代码以避免错误,可以看到2个评论之前的内容。我已经更新了我的答案中的代码,这是我3小时前就应该做的事情了。 - Peter Cordes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接