从64位无符号整数初始化__m128类型

12
_mm_set_epi64和类似的*_epi64指令似乎使用并依赖于__m64类型。我想初始化一个__m128类型的变量,使其上64位为0,下64位设置为x,其中xuint64_t(或类似的无符号64位类型)。有没有“正确”的方法来做到这一点?
最好以独立于编译器的方式实现。

1
这不是微软特定的数据类型吗? - John Dibling
5
这段话的意思是:“这是x86特有的,但绝对不是MS(微软)特有的。所有主要的编译器(如VS、GCC、ICC、Clang)都支持它。” - Mysticial
3个回答

10
为了回答你关于如何将64位值加载到XMM寄存器的低64位并清零高64位的问题,_mm_loadl_epi64(&x) 将完全符合你的要求
关于_mm_set_epi64,我曾经说过,查看Agner Fog的向量类库的源代码可以回答95%的SSE / AVX问题。 Agner为多个编译器和64位和32位实现了这个(来自文件vectori128.h)。请注意,对于MSVC 32位的解决方案,Agner表示“这很低效,但其他解决方案更糟”。我想这就是Mysticial所说的“没有好的方法可行”的意思。
Vec2q(int64_t i0, int64_t i1) {
#if defined (_MSC_VER) && ! defined(__INTEL_COMPILER)
        // MS compiler has no _mm_set_epi64x in 32 bit mode
#if defined(__x86_64__)                                    // 64 bit mode
#if _MSC_VER < 1700
        __m128i x0 = _mm_cvtsi64_si128(i0);                // 64 bit load
        __m128i x1 = _mm_cvtsi64_si128(i1);                // 64 bit load
        xmm = _mm_unpacklo_epi64(x0,x1);                   // combine
#else
        xmm = _mm_set_epi64x(i1, i0);
#endif
#else   // MS compiler in 32-bit mode
        union {
            int64_t q[2];
            int32_t r[4];
        } u;
        u.q[0] = i0;  u.q[1] = i1;
        // this is inefficient, but other solutions are worse
        xmm = _mm_setr_epi32(u.r[0], u.r[1], u.r[2], u.r[3]);
#endif  // __x86_64__
#else   // Other compilers
        xmm = _mm_set_epi64x(i1, i0);
#endif
};

8

最常见的用于此的“标准”内部函数是_mm_set_epi64x

对于缺少_mm_set_epi64x的平台,您可以定义一个替换宏,如下所示:

#define _mm_set_epi64x(m0, m1) _mm_set_epi64(_m_from_int64(m0), _m_from_int64(m1))

1
对于那些不再关心32位的人来说,这个内在函数只存在于x64上。要针对32位进行目标设置,需要采用不同的方法。 - Mysticial
2
如果您不介意提供一个如何实现这个的答案,那对于后来的人可能会很有帮助。 - Gideon
3
@Gideon,这个问题并没有一个好的解决办法。最好是一开始就避免使用这些集合内置函数。我认为唯一可以接受它们的地方是在编译时常量——这种情况下,你可以手动将64位整数拆分成两半,然后使用 _mm_set_epi32() 函数。 - Mysticial
1
@Gideon,我回答了你的问题。 - Z boson

4
我想初始化一个__m128类型的变量 ...其中x是uint64_t类型。
可以使用带有uint64_t的内置函数_mm_set_epi64x(而不是使用_mm_set_epi64,它需要一个__m64)。
最近我在Solaris上遇到了这个问题,Sun Studio 12.3及以下版本没有_mm_set_epi64x。 它也缺少像_mm_cvtsi64_si128_m_from_int64这样的解决方法。
如果感兴趣,这是我使用的hack。另一种选择是禁用SSE2,但这并不太理想(在基准测试中速度慢了3倍):
// Sun Studio 12.3 and earlier lack SSE2's _mm_set_epi64 and _mm_set_epi64x.
#if defined(__SUNPRO_CC) && (__SUNPRO_CC < 0x5130)
inline __m128i _mm_set_epi64x(const uint64_t a, const uint64_t b)
{
    union INT_128_64 {
        __m128i   v128;
        uint64_t  v64[2];
    };

    INT_128_64 v;
    v.v64[0] = b; v.v64[1] = a; 
    return v.v128;
}
#endif

我相信C++11可以做更多的事情来帮助编译器和提高性能,比如初始化一个常量数组:

const INT_128_64 v = {a,b};
return v.v128;

有一个很大的警告...我认为会出现未定义行为,因为在联合中使用v64成员进行写入,然后使用v128成员进行读取。在SunCC下测试表明编译器正在执行预期的(但技术上不正确的)操作。
我认为您可以通过使用memcpy来规避未定义的行为,但这可能会影响性能。还请参阅Peter Cordes在How to swap two __m128i variables in C++03 given its an opaque type and an array?中的回答和讨论。
以下也可能是一个不错的选择,以避免使用未激活的联合成员导致的未定义行为。但是,我对该游戏的实施方法并不确定。
INT_128_64 v;
v.v64[0] = b; v.v64[1] = a;
return *(reinterpret_cast<__m128i*>(v.v64));

EDIT(三个月后):Solaris和SunCC不喜欢这种玩笑。它为我们生成了糟糕的代码,我们不得不将值复制到__m128i中。Unix、Linux、Windows、GCC、Clang、ICC、MSC都没有问题。只有SunCC给我们带来了麻烦。


使用联合类型进行类型转换比使用指针强制转换更可取。虽然它们都是未定义行为,但至少在gcc中,联合类型是安全的。指针强制转换技术在真正的编译器中并不安全。(除了SIMD __m128类型,它们带有may_alias属性或类似属性。希望SunCC也能类似地定义它。) - Peter Cordes
如果您不确定基于联合体的类型转换是否安全,那么尝试使用memcpy是值得的。一些编译器可以很好地优化它,但正如您所说,我展示了并非所有编译器都能生成可接受的代码。据我所知,memcpy是唯一由ISO C和C++保证可移植的类型转换技术,但基于联合体的类型转换在实际生活中被广泛使用。 - Peter Cordes
@PeterCordes - 查看SSE2 _mm_loadl_pi,它可能是一个合适的替代品。它允许加载未对齐的64位值。使用 _mm_loadl_pi 两次并进行中间移位可能可以避免一些理论上的问题。 - jww
如果你真的想让编译器发出加载指令,而不是 movd xmm0, eaxmovq xmm0, rax 或其他类似的指令,那么也许可以考虑使用 _mm_loadu_si64 (void const* mem_addr) 这个内部函数(用于将低 64 位加载到 xmm 寄存器中,上 64 位清零)。或者在不支持该函数的编译器上,可以使用 _mm_loadl_epi64 函数,它也会被编译成一个 movq 指令。然后再使用 _mm_loadh_pi 函数将上半部分加载到 xmm 寄存器中,使用移位内部函数是愚蠢的,绝对不要使用两次! - Peter Cordes
无论如何,movq / movl/h 对于一对非相邻的64位值可能是很好的选择。顺便说一下,没有指令要求64位或更小的操作数对齐;你不需要一个特殊的内部函数来实现这个。我也可以避免在一对相邻的最近写入的64位值上出现存储转发失败停顿,而是使用 _mm_loadu_si128。(但是 _mm_loadu 的指针转换应该是别名安全的。可能你的转换版本实际上是安全的,但我认为我曾经看到过一个SO问题,类似的东西并没有做到OP想要的。) - Peter Cordes
关于我之前的评论更新:基于联合的类型转换在C99/C11中是可以保证工作的,但在C++中不行。将 *(__m128i*)&v64[0] 强制转换应该是安全的,因为 __m128 类型是特殊的,允许别名,不像 *(double *)&v64[0]。例如,gcc使用 __attribute__((may_alias)) 定义 __m128i,而不是它所基于的内部 v2qi 本地向量类型。我从来不喜欢使用 memcpy 进行类型转换,但也许编译器已经足够好了,可以看穿它,以至于你不会在实践中得到糟糕的代码。我忘记是否曾经见过它没有被优化掉的例子。 - Peter Cordes

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