我想编写快速simd代码来计算复杂数组的乘法减少。在标准C中,这是:
#include <complex.h>
complex float f(complex float x[], int n ) {
complex float p = 1.0;
for (int i = 0; i < n; i++)
p *= x[i];
return p;
}
n
最多为50个。
Gcc无法自动矢量化复数乘法,但是,我很乐意假设gcc编译器,如果我知道我想要定位sse3,我可以关注How to enable sse3 autovectorization in gcc并写:
typedef float v4sf __attribute__ ((vector_size (16)));
typedef union {
v4sf v;
float e[4];
} float4
typedef struct {
float4 x;
float4 y;
} complex4;
static complex4 complex4_mul(complex4 a, complex4 b) {
return (complex4){a.x.v*b.x.v -a.y.v*b.y.v, a.y.v*b.x.v + a.x.v*b.y.v};
}
complex4 f4(complex4 x[], int n) {
v4sf one = {1,1,1,1};
complex4 p = {one,one};
for (int i = 0; i < n; i++) p = complex4_mul(p, x[i]);
return p;
}
这确实使用gcc生成快速矢量化汇编代码。虽然你仍然需要将你的输入填充到4的倍数。你得到的程序集是:
.L3:
vmovaps xmm0, XMMWORD PTR 16[rsi]
add rsi, 32
vmulps xmm1, xmm0, xmm2
vmulps xmm0, xmm0, xmm3
vfmsubps xmm1, xmm3, XMMWORD PTR -32[rsi], xmm1
vmovaps xmm3, xmm1
vfmaddps xmm2, xmm2, XMMWORD PTR -32[rsi], xmm0
cmp rdx, rsi
jne .L3
但是,它专为精确的simd指令集而设计,对于avx2或avx512而言并非最佳,例如您需要更改代码。
如何编写gcc将产生最佳效果的C或C ++代码 编译为sse,avx2或avx512中的任何一个时的代码?也就是说,对于每个不同宽度的SIMD寄存器,您是否总是需要手动编写单独的函数?
是否有任何开源库可以让这更容易?
答案 0 :(得分:12)
以下是使用Eigen library:
的示例#include <Eigen/Core>
std::complex<float> f(const std::complex<float> *x, int n)
{
return Eigen::VectorXcf::Map(x, n).prod();
}
如果使用clang或g ++和sse或avx enabled(和-O2)编译它,你应该得到相当不错的机器代码。它也适用于其他一些架构,如Altivec或NEON。如果您知道x
的第一个条目已对齐,则可以使用MapAligned
代替Map
。
如果您在编译时碰巧知道矢量的大小,那么您可以获得更好的代码:
template<int n>
std::complex<float> f(const std::complex<float> *x)
{
return Eigen::Matrix<std::complex<float>, n, 1> >::MapAligned(x).prod();
}
注意:上述函数直接对应于OP的函数f
。
然而,正如@PeterCordes所指出的那样,存储交错复数的通常是不好的,因为这将需要大量的重排以进行乘法。相反,应该以一种可以直接加载一个数据包的方式存储实部和虚部。
编辑/附录:要实现像复数乘法一样的数组结构,您实际上可以编写如下内容:
typedef Eigen::Array<float, 8, 1> v8sf; // Eigen::Array allows element-wise standard operations
typedef std::complex<v8sf> complex8;
complex8 prod(const complex8& a, const complex8& b)
{
return a*b;
}
或更通用(使用C ++ 11):
template<int size, typename Scalar = float> using complexX = std::complex<Eigen::Array<Scalar, size, 1> >;
template<int size>
complexX<size> prod(const complexX<size>& a, const complexX<size>& b)
{
return a*b;
}
使用-mavx -O2
编译时,会编译为类似的内容(使用g ++ - 5.4):
vmovaps 32(%rsi), %ymm1
movq %rdi, %rax
vmovaps (%rsi), %ymm0
vmovaps 32(%rdi), %ymm3
vmovaps (%rdi), %ymm4
vmulps %ymm0, %ymm3, %ymm2
vmulps %ymm4, %ymm1, %ymm5
vmulps %ymm4, %ymm0, %ymm0
vmulps %ymm3, %ymm1, %ymm1
vaddps %ymm5, %ymm2, %ymm2
vsubps %ymm1, %ymm0, %ymm0
vmovaps %ymm2, 32(%rdi)
vmovaps %ymm0, (%rdi)
vzeroupper
ret
由于我不明白的原因,这实际上隐藏在一个方法中,该方法由实际方法调用,只是在一些内存中移动 - 我不知道为什么Eigen / gcc不会认为参数已经存在正确对齐。如果我使用clang 3.8.0(和相同的参数)编译相同的内容,则编译为:
vmovaps (%rsi), %ymm0
vmovaps %ymm0, (%rdi)
vmovaps 32(%rsi), %ymm0
vmovaps %ymm0, 32(%rdi)
vmovaps (%rdi), %ymm1
vmovaps (%rdx), %ymm2
vmovaps 32(%rdx), %ymm3
vmulps %ymm2, %ymm1, %ymm4
vmulps %ymm3, %ymm0, %ymm5
vsubps %ymm5, %ymm4, %ymm4
vmulps %ymm3, %ymm1, %ymm1
vmulps %ymm0, %ymm2, %ymm0
vaddps %ymm1, %ymm0, %ymm0
vmovaps %ymm0, 32(%rdi)
vmovaps %ymm4, (%rdi)
movq %rdi, %rax
vzeroupper
retq
同样,开头的记忆运动很奇怪,但至少那是矢量化的。对于gcc和clang,当在循环中调用时,这会被优化掉:
complex8 f8(complex8 x[], int n) {
if(n==0)
return complex8(v8sf::Ones(),v8sf::Zero()); // I guess you want p = 1 + 0*i at the beginning?
complex8 p = x[0];
for (int i = 1; i < n; i++) p = prod(p, x[i]);
return p;
}
这里的区别在于clang会将每个循环的外循环展开为2次乘法。另一方面,当使用-mfma
编译时,gcc将使用融合乘法加法指令。
f8
函数当然也可以推广到任意维度:
template<int size>
complexX<size> fX(complexX<size> x[], int n) {
using S= typename complexX<size>::value_type;
if(n==0)
return complexX<size>(S::Ones(),S::Zero());
complexX<size> p = x[0];
for (int i = 1; i < n; i++) p *=x[i];
return p;
}
为了将complexX<N>
缩减为单个std::complex
,可以使用以下函数:
// only works for powers of two
template<int size> EIGEN_ALWAYS_INLINE
std::complex<float> redux(const complexX<size>& var) {
complexX<size/2> a(var.real().template head<size/2>(), var.imag().template head<size/2>());
complexX<size/2> b(var.real().template tail<size/2>(), var.imag().template tail<size/2>());
return redux(a*b);
}
template<> EIGEN_ALWAYS_INLINE
std::complex<float> redux(const complexX<1>& var) {
return std::complex<float>(var.real()[0], var.imag()[0]);
}
但是,根据我是否使用clang或g ++,我得到了完全不同的汇编输出。总的来说,g ++倾向于无法内联加载输入参数,并且clang无法使用FMA操作(YMMV ...) 实质上,您还需要检查生成的汇编代码。更重要的是,您应该对代码进行基准测试(不确定,此例程对您的整体问题有多大影响)。
另外,我想说明Eigen实际上是一个线性代数库。利用纯粹的便携式SIMD代码生成它并不是真正的设计目标。
答案 1 :(得分:3)
如果您主要关心可移植性,那么有许多库here以自己的语法提供SIMD指令。他们中的大多数做的显式矢量化比内在函数更简单和便携。 This Library (UME::SIMD)最近发布,效果很好
在this paper(UME::SIMD)中,已建立基于Vc的界面 被命名为UME :: SIMD。它允许程序员访问SIMD 功能,无需广泛了解SIMD ISA。 UME :: SIMD提供简单,灵活和可移植的抽象 与...相比没有性能损失的显式矢量化 内在
答案 2 :(得分:1)
我认为你没有完全通用的解决方案。你可以增加你的&#34; vector_size&#34;到32:
typedef float v4sf __attribute__ ((vector_size (32)));
还要将所有数组增加到8个元素:
typedef float v8sf __attribute__ ((vector_size (32)));
typedef union {
v8sf v;
float e[8];
} float8;
typedef struct {
float8 x;
float8 y;
} complex8;
static complex8 complex8_mul(complex8 a, complex8 b) {
return (complex8){a.x.v*b.x.v -a.y.v*b.y.v, a.y.v*b.x.v + a.x.v*b.y.v};
}
这将使编译器能够生成AVX512代码(不要忘记添加-mavx512f
),但是通过使内存传输次优,会使您的代码在SSE中稍微恶化。但是,它肯定不会禁用SSE矢量化。
你可以保留两个版本(带有4个和8个数组元素),通过一些标志在它们之间切换,但它可能太繁琐而没什么好处。