为什么SSE有128位加载函数?



我正在别人的代码中闲逛,目前正在试图弄清楚为什么_mm_load_si128存在。

本质上,我尝试更换

_ra = _mm_load_si128(reinterpret_cast<__m128i*>(&cd->data[idx]));

_ra = *reinterpret_cast<__m128i*>(&cd->data[idx]);

它的工作原理和性能完全相同。

我认为加载函数的存在只是为了方便起见,这样人们就不必手动将它们打包到连续内存中,但对于已经按正确顺序排列的数据,为什么要打扰呢?

_mm_load_si128还有别的事情吗?或者它本质上只是一种分配值的迂回方式?

SSE 中有显式和隐式加载。

  • _mm_load_si128(reinterpret_cast<__m128i*>(&cd->data[idx]));是显式加载
  • *reinterpret_cast<__m128i*>(&cd->data[idx]);是隐式负载

通过显式加载,您可以显式指示编译器将数据加载到 XMM 寄存器 - 这是英特尔的"官方"方式。您还可以通过使用_mm_load_si128_mm_loadu_si128来控制荷载是对齐荷载还是未对齐荷载。

尽管作为扩展,大多数编译器在执行类型双关语时也能够自动生成 XMM 加载,但这样您无法控制负载是对齐还是未对齐。在这种情况下,由于在现代 CPU 上,当数据对齐时使用未对齐的负载不会对性能造成损失,因此编译器倾向于普遍使用未对齐的负载。

另一个更重要的方面是,对于隐式加载,您违反了严格的别名规则,这可能会导致未定义的行为。尽管值得一提的是,作为扩展的一部分,支持英特尔内部函数的编译器并不倾向于对 XMM 占位符类型(如__m128__m128d__m128i)强制实施严格的混叠规则。

尽管如此,我认为显式负载更清洁、更防弹。


为什么编译器不倾向于对 SSE 占位符类型强制实施严格的别名规则?

第一个原因在于SSE 内部函数的设计:在某些情况下,您必须使用类型双关语,因为没有其他方法可以使用某些内部函数。神秘客的回答完美地总结了这一点。

正如 Cody Gray 在评论中指出的那样,值得一提的是,历史上的 MMX intrisintics(现在大部分被 SSE2 取代)甚至没有提供显式加载或存储 - 你必须使用类型双关语。

第二个原因(与第一个原因有些相关)在于这些类型的类型定义。

GCC 对<xmmintrin.h ><emmintrin.h>中的 SSE/SSE2 占位符类型的typedef

/* The Intel API is flexible enough that we must allow aliasing with other
vector types, and their scalar components.  */
typedef float __m128 __attribute__ ((__vector_size__ (16), __may_alias__));    
typedef long long __m128i __attribute__ ((__vector_size__ (16), __may_alias__));
typedef double __m128d __attribute__ ((__vector_size__ (16), __may_alias__));

此处的关键是__may_alias__属性,即使使用-fstrict-aliasing标志启用了严格别名,它也使类型双关对这些类型起作用。

现在,由于clangICCGCC兼容,它们应该遵循相同的约定。所以目前,在这 3 个编译器中,隐式加载/存储在某种程度上保证即使使用-fstrict-aliasing标志也能工作。最后,MSVC根本不支持严格的混叠,因此它甚至不会成为问题。

不过,这并不意味着您应该更喜欢隐式加载/存储而不是显式加载/存储。

最新更新