如何最好地模拟 _mm_slli_si128(128 位位移)的逻辑含义,而不是 _mm_bslli_si128
How to best emulate the logical meaning of _mm_slli_si128 (128-bit bit-shift), not _mm_bslli_si128
通过 intel intrinsics 指南,我看到了这条指令。看命名模式,意思应该很清楚:“将128位寄存器左移固定位数”,但事实并非如此。实际上它移动了固定数量的 字节 ,这使得它与 _mm_bslli_si128
.
完全相同
- 这是疏忽吗?它不应该像
_mm_slli_epi32
或 _mm_slli_epi64
这样的位移动吗?
- 如果不是,我应该在什么情况下使用它而不是
_mm_bslli_si128
?
- 是否有正确执行此操作的汇编指令?
- 用更小的班次模拟这个的最佳方法是什么?
1 这不是疏忽。该指令确实按字节移动,即 8 位的倍数。
2无所谓,_mm_slli_si128
和_mm_bslli_si128
是等价的,都编译成pslldq
SSE2指令。
至于仿真,我会那样做,假设你有 C++/17。如果您正在编写 C++/14,请将 if constexpr
替换为正常的 if
,并在 static_assert
.
中添加一条消息
template<int i>
inline __m128i shiftLeftBits( __m128i vec )
{
static_assert( i >= 0 && i < 128 );
// Handle couple trivial cases
if constexpr( 0 == i )
return vec;
if constexpr( 0 == ( i % 8 ) )
return _mm_slli_si128( vec, i / 8 );
if constexpr( i > 64 )
{
// Shifting by more than 8 bytes, the lowest half will be all zeros
vec = _mm_slli_si128( vec, 8 );
return _mm_slli_epi64( vec, i - 64 );
}
else
{
// Shifting by less than 8 bytes.
// Need to propagate a few bits across 64-bit lanes.
__m128i low = _mm_slli_si128( vec, 8 );
__m128i high = _mm_slli_epi64( vec, i );
low = _mm_srli_epi64( low, 64 - i );
return _mm_or_si128( low, high );
}
}
TL:DR: 它们是同义词; bslli
名称较新,大约与新的 AVX-512 内在函数同时引入(2015 年之前的某个时间,long 在 SSE2 _mm_slli_si128
被广泛使用之后)。我发现它更清晰,并会推荐它用于新开发。
SSE/AVX2/AVX-512 没有元素大小大于 64 的移位。(或任何其他像 add
这样的位粒度操作,除了真正 128 完全独立的纯垂直按位布尔值操作,不是一个大的宽操作。或者为了 AVX-512 屏蔽和广播负载目的,可以在 dword 或 qword 块中,如 _mm512_xor_epi32
/ vpxord
)
您必须以某种方式模拟它,这对于编译时常数计数可能相当有效,因此您可以根据 c >= 64
在策略之间进行选择,c%8
的特殊情况减少到字节移位。现有的 SO 问答涵盖了这一点,或者查看@Soonts 对此问题的回答。
运行时变量计数很糟糕;您必须分支或两种方式都进行混合,这与 _mm_sll_epi64(v, _mm_cvtsi32_si128(i))
可以编译为 movd
/ psllq xmm, xmm
的元素位移不同。不幸的是,byte-shuffle/shift 指令的硬件可变计数版本不存在,仅适用于位移版本。
bslli
/ bsrli
是相同 asm 指令的新的、更清晰的内部名称
x86 的所有 4 个主要编译器的当前版本 (Godbolt) 都支持 b
名称,我建议将它们用于新开发,除非您需要向后兼容旧的编译器,或者出于某种原因你 喜欢 旧名称,它既不能将它与不同的操作区分开来。 (例如,熟悉度;如果您不希望人们不得不在手册中查找这个新奇的名称。)
- 自 4.8 以来的 gcc
- clang 自 3.7
- ICC 自 ICC13 或更早版本以来,Godbolt 没有更早的版本
- 自 19.14 或更早版本以来的 MSVC,Godbolt 没有任何旧版本
如果您查看内在函数指南,_mm_slli_si128
被列为 PSLLDQ
的内在函数,这是一个字节移位。这不是一个错误,只是英特尔的一个笑话,或者他们在 SSE2 时代用来为内在函数选择名称的任何过程。 (计算机科学中只有两个难题:缓存失效和命名事物)。
Asm 助记符也使用相同的模式,即不使字节洗牌看起来与位移不同。 psllw xmm, 1
/ pslld
/ psllq
/ pslldq
。同样,您只需要知道 128 位大小是特殊的,并且必须是字节洗牌而不是位移位,因为 x86 从来没有。 (或者你必须查看手册。)
pslldq
的 asm 手册条目依次列出了其形式的内部函数,有趣的是只使用 b
名称作为 __m512i
AVX-512BW 版本。我认为,当 SSE2 和 AVX2 是新的时,_mm_slli_si128
和 _mm256_slli_si256
是唯一可用的名称。当然它 post-dates SSE2 内在函数。
(请注意,si256
和 si512
版本只是 16 字节操作的 2 或 4 个副本, 而不是 在 128-位通道;一些其他问答要求的东西。这通常会使 AVX2 版本的洗牌像这样和 palignr
比它们本来的用处少得多:要么根本不值得使用,要么需要额外的洗牌最重要的。)
我认为这个新的 bslli
名称是在 AVX-512 是新的 时引入的。英特尔在那段时间为其他内在函数发明了一些新名称,AVX-512 load/store 内在函数使用 void*
而不是 __m512i*
,这是对代码噪声量的重大改进,尤其是对于允许隐式转换为 void*
的 C。 (创建一个未对齐的 __m512i*
,但您不能正常取消引用它,所以这是一件看起来很奇怪的事情。)所以当时内部命名发生了清理工作,我认为这是一部分。
(AVX-512 也让 Intel 有机会引入一些相当糟糕的名字,比如 _mm_loadu_epi32(const void*)
- 你会猜想这是一种严格的别名安全方式来执行 32 位 movd
加载,对吗?不,不幸的是,它是 vmovdqu32 xmm, [mem]
的内在函数,没有掩码。它只是 _mm_loadu_si128
指针 arg 具有不同的 C 类型。它的存在是为了与 [= 的命名模式保持一致46=]。void*
为 __m128i
和 __m256i
加载/存储内在函数会很好,但是如果它们有这样的误导性名称(尤其是当你不使用mask
/maskz
附近代码中的版本),我将坚持使用那些繁琐的 _mm256_loadu_si256( (const __m256i*)(arr + i) )
转换旧的内部函数,因为我喜欢输入三次 256
。 >。 <
我希望 asm 更易于维护(或者内在函数只是使用 asm 助记符),因为它更简洁;英特尔通常在命名助记符方面做得很好。
注意 epi16/32/64
和 si128
之间的区别在一定程度上但并非完全有帮助:EPI = 扩展(SSE 而不是 MMX)压缩整数。 (打包意味着多个 SIMD 元素)。 si128
表示一个完整的128位整数向量。
无法从名称中推断出您不是只是对单个 128 位整数而不是压缩元素做同样的事情。您只需要知道,没有跨越 64 位边界的位粒度事物,只有 SIMD 随机播放(以字节为单位)。这避免了构建一个非常宽的桶形移位器的组合爆炸,或者为 128 位加法在如此长的距离进行进位传播,或其他任何东西。
通过 intel intrinsics 指南,我看到了这条指令。看命名模式,意思应该很清楚:“将128位寄存器左移固定位数”,但事实并非如此。实际上它移动了固定数量的 字节 ,这使得它与 _mm_bslli_si128
.
- 这是疏忽吗?它不应该像
_mm_slli_epi32
或_mm_slli_epi64
这样的位移动吗? - 如果不是,我应该在什么情况下使用它而不是
_mm_bslli_si128
? - 是否有正确执行此操作的汇编指令?
- 用更小的班次模拟这个的最佳方法是什么?
1 这不是疏忽。该指令确实按字节移动,即 8 位的倍数。
2无所谓,_mm_slli_si128
和_mm_bslli_si128
是等价的,都编译成pslldq
SSE2指令。
至于仿真,我会那样做,假设你有 C++/17。如果您正在编写 C++/14,请将 if constexpr
替换为正常的 if
,并在 static_assert
.
template<int i>
inline __m128i shiftLeftBits( __m128i vec )
{
static_assert( i >= 0 && i < 128 );
// Handle couple trivial cases
if constexpr( 0 == i )
return vec;
if constexpr( 0 == ( i % 8 ) )
return _mm_slli_si128( vec, i / 8 );
if constexpr( i > 64 )
{
// Shifting by more than 8 bytes, the lowest half will be all zeros
vec = _mm_slli_si128( vec, 8 );
return _mm_slli_epi64( vec, i - 64 );
}
else
{
// Shifting by less than 8 bytes.
// Need to propagate a few bits across 64-bit lanes.
__m128i low = _mm_slli_si128( vec, 8 );
__m128i high = _mm_slli_epi64( vec, i );
low = _mm_srli_epi64( low, 64 - i );
return _mm_or_si128( low, high );
}
}
TL:DR: 它们是同义词; bslli
名称较新,大约与新的 AVX-512 内在函数同时引入(2015 年之前的某个时间,long 在 SSE2 _mm_slli_si128
被广泛使用之后)。我发现它更清晰,并会推荐它用于新开发。
SSE/AVX2/AVX-512 没有元素大小大于 64 的移位。(或任何其他像 add
这样的位粒度操作,除了真正 128 完全独立的纯垂直按位布尔值操作,不是一个大的宽操作。或者为了 AVX-512 屏蔽和广播负载目的,可以在 dword 或 qword 块中,如 _mm512_xor_epi32
/ vpxord
)
您必须以某种方式模拟它,这对于编译时常数计数可能相当有效,因此您可以根据 c >= 64
在策略之间进行选择,c%8
的特殊情况减少到字节移位。现有的 SO 问答涵盖了这一点,或者查看@Soonts 对此问题的回答。
运行时变量计数很糟糕;您必须分支或两种方式都进行混合,这与 _mm_sll_epi64(v, _mm_cvtsi32_si128(i))
可以编译为 movd
/ psllq xmm, xmm
的元素位移不同。不幸的是,byte-shuffle/shift 指令的硬件可变计数版本不存在,仅适用于位移版本。
bslli
/ bsrli
是相同 asm 指令的新的、更清晰的内部名称
x86 的所有 4 个主要编译器的当前版本 (Godbolt) 都支持 b
名称,我建议将它们用于新开发,除非您需要向后兼容旧的编译器,或者出于某种原因你 喜欢 旧名称,它既不能将它与不同的操作区分开来。 (例如,熟悉度;如果您不希望人们不得不在手册中查找这个新奇的名称。)
- 自 4.8 以来的 gcc
- clang 自 3.7
- ICC 自 ICC13 或更早版本以来,Godbolt 没有更早的版本
- 自 19.14 或更早版本以来的 MSVC,Godbolt 没有任何旧版本
如果您查看内在函数指南,_mm_slli_si128
被列为 PSLLDQ
的内在函数,这是一个字节移位。这不是一个错误,只是英特尔的一个笑话,或者他们在 SSE2 时代用来为内在函数选择名称的任何过程。 (计算机科学中只有两个难题:缓存失效和命名事物)。
Asm 助记符也使用相同的模式,即不使字节洗牌看起来与位移不同。 psllw xmm, 1
/ pslld
/ psllq
/ pslldq
。同样,您只需要知道 128 位大小是特殊的,并且必须是字节洗牌而不是位移位,因为 x86 从来没有。 (或者你必须查看手册。)
pslldq
的 asm 手册条目依次列出了其形式的内部函数,有趣的是只使用 b
名称作为 __m512i
AVX-512BW 版本。我认为,当 SSE2 和 AVX2 是新的时,_mm_slli_si128
和 _mm256_slli_si256
是唯一可用的名称。当然它 post-dates SSE2 内在函数。
(请注意,si256
和 si512
版本只是 16 字节操作的 2 或 4 个副本, 而不是 在 128-位通道;一些其他问答要求的东西。这通常会使 AVX2 版本的洗牌像这样和 palignr
比它们本来的用处少得多:要么根本不值得使用,要么需要额外的洗牌最重要的。)
我认为这个新的 bslli
名称是在 AVX-512 是新的 时引入的。英特尔在那段时间为其他内在函数发明了一些新名称,AVX-512 load/store 内在函数使用 void*
而不是 __m512i*
,这是对代码噪声量的重大改进,尤其是对于允许隐式转换为 void*
的 C。 (创建一个未对齐的 __m512i*
(AVX-512 也让 Intel 有机会引入一些相当糟糕的名字,比如 _mm_loadu_epi32(const void*)
- 你会猜想这是一种严格的别名安全方式来执行 32 位 movd
加载,对吗?不,不幸的是,它是 vmovdqu32 xmm, [mem]
的内在函数,没有掩码。它只是 _mm_loadu_si128
指针 arg 具有不同的 C 类型。它的存在是为了与 [= 的命名模式保持一致46=]。void*
为 __m128i
和 __m256i
加载/存储内在函数会很好,但是如果它们有这样的误导性名称(尤其是当你不使用mask
/maskz
附近代码中的版本),我将坚持使用那些繁琐的 _mm256_loadu_si256( (const __m256i*)(arr + i) )
转换旧的内部函数,因为我喜欢输入三次 256
。 >。 <
我希望 asm 更易于维护(或者内在函数只是使用 asm 助记符),因为它更简洁;英特尔通常在命名助记符方面做得很好。
注意 epi16/32/64
和 si128
之间的区别在一定程度上但并非完全有帮助:EPI = 扩展(SSE 而不是 MMX)压缩整数。 (打包意味着多个 SIMD 元素)。 si128
表示一个完整的128位整数向量。
无法从名称中推断出您不是只是对单个 128 位整数而不是压缩元素做同样的事情。您只需要知道,没有跨越 64 位边界的位粒度事物,只有 SIMD 随机播放(以字节为单位)。这避免了构建一个非常宽的桶形移位器的组合爆炸,或者为 128 位加法在如此长的距离进行进位传播,或其他任何东西。