我有一个矩阵类(4x4)
class matrix {
public:
matrix() {}
matrix(float m11,float m21,float m31,float m41,
float m12,float m22,float m32,float m42,
float m13,float m23,float m33,float m43,
float m14,float m24,float m34,float m44);
matrix(const float*);
matrix(const matrix&);
matrix operator *(const matrix& other)const;
static const matrix identity;
private:
union {
float m[16];
struct {
float m11,m21,m31,m41;
float m12,m22,m32,m42;
float m13,m23,m33,m43;
float m14,m24,m34,m44;
};
struct {
float element[4][4];
};
};
};
下面是乘法运算符的第一个实现,
matrix matrix::operator*(const matrix &other) const{
return matrix(
m11*other.m11+m12*other.m21+m13*other.m31+m14*other.m41,
m21*other.m11+m22*other.m21+m23*other.m31+m24*other.m41,
m31*other.m11+m32*other.m21+m33*other.m31+m34*other.m41,
m41*other.m11+m42*other.m21+m43*other.m31+m44*other.m41,
m11*other.m12+m12*other.m22+m13*other.m32+m14*other.m42,
m21*other.m12+m22*other.m22+m23*other.m32+m24*other.m42,
m31*other.m12+m32*other.m22+m33*other.m32+m34*other.m42,
m41*other.m12+m42*other.m22+m43*other.m32+m44*other.m42,
m11*other.m13+m12*other.m23+m13*other.m33+m14*other.m43,
m21*other.m13+m22*other.m23+m23*other.m33+m24*other.m43,
m31*other.m13+m32*other.m23+m33*other.m33+m34*other.m43,
m41*other.m13+m42*other.m23+m43*other.m33+m44*other.m43,
m11*other.m14+m12*other.m24+m13*other.m34+m14*other.m44,
m21*other.m14+m22*other.m24+m23*other.m34+m24*other.m44,
m31*other.m14+m32*other.m24+m33*other.m34+m34*other.m44,
m41*other.m14+m42*other.m24+m43*other.m34+m44*other.m44
);
}
我尝试使用sse说明加速下面的版本,
matrix matrix::operator*(const matrix &other) const{
float r[4][4];
__m128 c1=_mm_loadu_ps(&m11);
__m128 c2=_mm_loadu_ps(&m12);
__m128 c3=_mm_loadu_ps(&m13);
__m128 c4=_mm_loadu_ps(&m14);
for (int i = 0;i < 4; ++i) {
__m128 v1 = _mm_set1_ps(other.element[i][0]);
__m128 v2 = _mm_set1_ps(other.element[i][1]);
__m128 v3 = _mm_set1_ps(other.element[i][2]);
__m128 v4 = _mm_set1_ps(other.element[i][3]);
__m128 col = _mm_add_ps(
_mm_add_ps(_mm_mul_ps(v1,c1),_mm_mul_ps(v2,c2)),
_mm_add_ps(_mm_mul_ps(v3,c3),_mm_mul_ps(v4,c4))
);
_mm_storeu_ps(r[i], col);
}
return matrix(&r[0][0]);
}
但是在我的macbookpro上,第一个版本的100000矩阵乘法成本约为6ms,第二个版本约为8ms。 我想知道为什么会这样。 也许是因为cpu管道使第一个版本运行并发计算而加载/保存滞后于第二个版本?
答案 0 :(得分:3)
当您允许编译器优化代码时,您可以从第一个(标量)情况下的大量指令并行性中受益。通过arranging the code so as to minimize data dependencies,即使这可能导致需要更多的总指令,每条指令也可以在不同的执行单元上同时运行。有批次的寄存器可用,因此大多数值可以保持注册,最大限度地减少了昂贵的内存读取需求,即使需要内存读取,也可以在其他操作时几乎免费完成由于乱序执行计划,正在完成。我会进一步推测你在这里受益于μ-op缓存,其好处在于补偿增加的代码大小。
在第二个(并行)情况下,您将创建重要的数据依赖项。即使编译器发出最佳目标代码(当使用内在函数时也不一定如此),强制执行此并行性会产生成本。如果你ask the compiler to show you an assembly listing,你可以看到。在操作之间对SSE寄存器内的浮点操作数进行打包和重新排序需要大量的shufps
指令。现代英特尔架构 * 只需要一个周期,但后续的addps
和mulps
操作无法并行执行。他们必须等待它完成。这个代码可能会遇到严重的μ-op吞吐量瓶颈,这很有可能。 (您可能还会在此代码中支付未对齐的数据惩罚,但这在现代架构中很少。)
换句话说,您为了增加数据依赖性而交换并行性(以更大的代码为代价)(尽管代码更小)。至少,这将是我半教育的猜测,看看你的示例代码的反汇编。在这种情况下,您的基准测试会非常清楚地告诉您它不会对您有利。
如果您指示编译器采用AVX支持,情况可能会发生变化。如果目标体系结构不支持AVX,则编译器别无选择,只能将_mm_set1_ps
内在函数转换为一对movss
,shufps
指令。如果你启用AVX支持,你会得到一条vbroadcastss
指令,这可能会更快,特别是在支持AVX2的情况下,你可以从寄存器到寄存器进行广播(而不是仅从存储器到寄存器进行广播) )。通过AVX支持,您还可以获得VEX编码指令的好处。
* 虽然在某些较旧的体系结构(如Core 2)上,shufps
是一个基于整数的指令,因此当它跟随一个浮点时会导致延迟像addps
或mulps
这样的说明。我不记得究竟什么时候修好了,但肯定不是Sandy Bridge及以后的问题。