用 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 位整数向量相乘的最快方法的主要内容,如果未能解决你的问题,请参考以下文章