用 SSE 在 C++ 中将两个 32 位整数向量相乘的最快方法

Posted

技术标签:

【中文标题】用 SSE 在 C++ 中将两个 32 位整数向量相乘的最快方法【英文标题】:Fastest way to multiply two vectors of 32bit integers in C++, with SSE 【发布时间】:2013-06-23 19:21:31 【问题描述】:

我有两个无符号向量,大小均为 4

vector<unsigned> v1 = 2, 4, 6, 8
vector<unsigned> v2 = 1, 10, 11, 13

现在我想将这两个向量相乘得到一个新的向量

vector<unsigned> v_result = 2*1, 4*10, 6*11, 8*13

要使用什么 SSE 操作?是跨平台还是只有 在某些特定平台上?

添加: 如果我的目标是加法而不是乘法,我可以超快地完成:

__m128i a = _mm_set_epi32(1,2,3,4);
__m128i b = _mm_set_epi32(1,2,3,4);
__m128i c;
c = _mm_add_epi32(a,b);

【问题讨论】:

即使编译器可以推断出足够多的大小、对齐方式等来满足矢量化,我怀疑它会在这里使用 SSE,因为涉及的加载/存储成本。 你知道_mm_mul_ps吗? @rwols: mulps 进行单精度乘法,OP 需要无符号整数乘法。 是的,但我不确定平台,它是否随处可用?如果不是,解决方法是什么? 【参考方案1】:

对所有元素使用诸如_mm_set_epi32 之类的集合内在函数是低效的。最好使用负载内在函数。有关Where does the SSE instructions outperform normal instructions 的更多信息,请参阅此讨论。如果数组是 16 字节对齐的,您可以使用 _mm_load_si128_mm_loadu_si128(对于对齐的内存,它们的效率几乎相同)否则使用 _mm_loadu_si128。但是对齐的内存效率更高。为了获得对齐的内存,我推荐_mm_malloc_mm_free,或C11 aligned_alloc,这样你就可以使用普通的free


要回答您的其余问题,假设您已将两个向量加载到 SSE 寄存器 __m128i a__m128i b

对于 SSE 版本 >=SSE4.1 使用

_mm_mullo_epi32(a, b);

没有 SSE4.1:

此代码是从 Agner Fog 的Vector Class Library 复制的(并被此答案的原作者抄袭):

// Vec4i operator * (Vec4i const & a, Vec4i const & b) 
// #ifdef
__m128i a13    = _mm_shuffle_epi32(a, 0xF5);          // (-,a3,-,a1)
__m128i b13    = _mm_shuffle_epi32(b, 0xF5);          // (-,b3,-,b1)
__m128i prod02 = _mm_mul_epu32(a, b);                 // (-,a2*b2,-,a0*b0)
__m128i prod13 = _mm_mul_epu32(a13, b13);             // (-,a3*b3,-,a1*b1)
__m128i prod01 = _mm_unpacklo_epi32(prod02,prod13);   // (-,-,a1*b1,a0*b0) 
__m128i prod23 = _mm_unpackhi_epi32(prod02,prod13);   // (-,-,a3*b3,a2*b2) 
__m128i prod   = _mm_unpacklo_epi64(prod01,prod23);   // (ab3,ab2,ab1,ab0)

【讨论】:

【参考方案2】:

_mm_mul_epu32 仅适用于 SSE2 并使用 pmuludq 指令。因为它是 SSE2 指令,所以 99.9% 的 CPU 都支持它(我认为最现代的 CPU 是 AMD Athlon XP)。

它有一个显着的缺点,它一次只能乘以两个整数,因为它返回 64 位结果,并且您只能将其中两个放入寄存器中。这意味着您可能需要进行大量洗牌,这会增加成本。

【讨论】:

【参考方案3】:

可能 _mm_mullo_epi32 是您需要的,尽管它的预期用途是用于有符号整数。只要 v1 和 v2 非常小以至于这些整数的最高有效位为 0,这不会导致问题。它是 SSE 4.1。作为替代方案,您可能需要考虑 _mm_mul_epu32。

【讨论】:

符号与乘法的低位词无关。它不应该被记录为有符号的乘法 - 它不是,它是一个无符号的乘法。将add 记录为“签名添加”是一件愚蠢的事情。当然,他们对imul也犯了同样的错误。 @harold:我同意。好点子。 Intel SSE4 programming reference 中的表 2.1 让我很困惑。【参考方案4】:

您可以(如果 SSE 4.1 可用)使用

__m128i _mm_mullo_epi32 (__m128i a, __m128i b);

将压缩的 32 位整数相乘。 否则你必须洗牌两个包才能使用_mm_mul_epu32 两次。有关显式代码,请参阅@user2088790 的答案。

请注意,您也可以使用 _mm_mul_epi32,但那是 SSE4,所以无论如何您宁愿使用 _mm_mullo_epi32

【讨论】:

对,我在问这个 _mm_mul_epi32 的平台。它是随处可用还是仅在少数地方可用? 请参阅Wikipedia/SSE4 了解有关它将出现的架构的信息。 AMD 自 K10 以来就拥有它,而英特尔自 Core 2 天以来就拥有它。 OP 似乎要求 32 位结果,而不是扩大/完全乘法 (32x32->64)。由于 SSE4.1 还添加了_mm_mullo_epi32 (pmulld),它给出了四个 32 位的结果,这是错误的答案,@user2088790 的答案是正确的答案。 SSE4.1 的_mm_mul_epi32 是SSE2 _mm_mul_epu32 的签名版本。 @PeterCordes:答案说它是将低半部分乘以 64 位。它还说你需要再打电话给_mm_mul_epi32 以及一些洗牌。这基本上就是 user2088790 对未签名版本_mm_mul_epu32 的回答。但我承认,我建议使用 OP 表示无符号值的有符号内在函数。 @Pixelchemist:我的主要观点是,由于您的建议无论如何都需要 SSE4.1,因此您应该使用 _mm_mullo_epi32 来获得四个 32 位结果。请注意 user2088790 答案的 SSE4.1 部分。 _mm_mullo_epi32_mm_mul_epu32 on some CPUs 慢,但仍然比两次乘法 + 改组快,因此它是 SSE4.1 的唯一有效答案。 (另外,如果你扔掉上半部分,签名与未签名并不重要)。【参考方案5】:

std::transform 将给定函数应用于范围并存储 导致另一个范围

std::vector<unsigned> result;

std::transform( v1.begin()+1, v1.end(), v2.begin()+1, v.begin(),std::multiplies<unsigned>() );

【讨论】:

以上是关于用 SSE 在 C++ 中将两个 32 位整数向量相乘的最快方法的主要内容,如果未能解决你的问题,请参考以下文章

numpy ufunc/算术性能 - 整数不使用 SSE?

两个 16 位整数向量与 C++ 中的 AVX2 的内积

你如何在 SSE2 上进行带符号的 32 位扩展乘法?

使用 SSE(IA32 汇编)执行简单的算术运算

随机播放 16 位向量 SSE

如何在 AVX2 中将 32 位无符号整数转换为 16 位无符号整数?