2015-07-19 103 views
0

作为一种学习练习,我正在努力加快在各种体系结构上使用SIMD的矩阵乘法代码。我对SSE2的3D矩阵乘法码有一个奇怪的问题,它的性能在两个极端之间跳跃,大约5ms(预期)或100万次操作的〜100ms。这个SSE2代码为什么执行不一致?

这段代码所做的唯一不好的事情就是未对齐的存储/加载和最后的黑客来将向量存储到内存中而没有第四个元素践踏内存。这可以解释一些性能差异,但性能差异如此之大的事实让我怀疑我错过了一些重要的东西。

我已经尝试了几件事情,但在睡眠后我会再试一次。

查看下面的代码。 m_matrix变量在16字节边界上对齐。

void Matrix3x3::MultiplySSE2(Matrix3x3 &other, Matrix3x3 &output) 
{ 
    __m128 a_row, r_row; 
    __m128 a1_row, r1_row; 
    __m128 a2_row, r2_row; 

    const __m128 b_row0 = _mm_load_ps(&other.m_matrix[0]); 
    const __m128 b_row1 = _mm_loadu_ps(&other.m_matrix[3]); 
    const __m128 b_row2 = _mm_loadu_ps(&other.m_matrix[6]); 

    // Perform dot products with first row 
    a_row = _mm_set1_ps(m_matrix[0]); 
    r_row = _mm_mul_ps(a_row, b_row0); 
    a_row = _mm_set1_ps(m_matrix[1]); 
    r_row = _mm_add_ps(_mm_mul_ps(a_row, b_row1), r_row); 
    a_row = _mm_set1_ps(m_matrix[2]); 
    r_row = _mm_add_ps(_mm_mul_ps(a_row, b_row2), r_row); 

    _mm_store_ps(&output.m_matrix[0], r_row); 

    // Perform dot products with second row 
    a1_row = _mm_set1_ps(m_matrix[3]); 
    r1_row = _mm_mul_ps(a1_row, b_row0); 
    a1_row = _mm_set1_ps(m_matrix[4]); 
    r1_row = _mm_add_ps(_mm_mul_ps(a1_row, b_row1), r1_row); 
    a1_row = _mm_set1_ps(m_matrix[5]); 
    r1_row = _mm_add_ps(_mm_mul_ps(a1_row, b_row2), r1_row); 

    _mm_storeu_ps(&output.m_matrix[3], r1_row); 

    // Perform dot products with third row 
    a2_row = _mm_set1_ps(m_matrix[6]); 
    r2_row = _mm_mul_ps(a2_row, b_row0); 
    a2_row = _mm_set1_ps(m_matrix[7]); 
    r2_row = _mm_add_ps(_mm_mul_ps(a2_row, b_row1), r2_row); 
    a2_row = _mm_set1_ps(m_matrix[8]); 
    r2_row = _mm_add_ps(_mm_mul_ps(a2_row, b_row2), r2_row); 

    // Store only the first 3 elements in a vector so we dont trample memory 
    _mm_store_ss(&output.m_matrix[6], _mm_shuffle_ps(r2_row, r2_row,  _MM_SHUFFLE(0, 0, 0, 0))); 
    _mm_store_ss(&output.m_matrix[7], _mm_shuffle_ps(r2_row, r2_row, _MM_SHUFFLE(1, 1, 1, 1))); 
    _mm_store_ss(&output.m_matrix[8], _mm_shuffle_ps(r2_row, r2_row, _MM_SHUFFLE(2, 2, 2, 2))); 
} 
+1

做100万次测试几次,并获得平均时间。只做一次测试会给你带来不可靠的结果。 – CoffeeandCode

+0

@CoffeeandCode我正在将平均数作为测试平台的一部分。与具有SSE2的2D和4D矩阵相比,平均时间仍然是一个异常。统计不能解释步进行为,代码错误。 – BlamKiwi

+0

Idk,男人。如果有效,我不会说它有问题。你怎么做一个未对齐的商店btw? – CoffeeandCode

回答

2

这样的性能听起来像你的数据可能是有时穿越页面线,而不仅仅是缓存线。如果您正在测试许多不同矩阵的缓冲区,而不是重复使用同一个小矩阵,也许其他CPU核心上运行的其他程序正在将缓冲区从L3中推出?

在你的代码的性能问题(不解释因子的-20的变化,这些应始终缓慢。):

_mm_set1_ps(m_matrix[3])等将是一个问题。需要pshufdmovaps + shufps来广播一个元素。不过,我认为这对于matmuls来说是不可避免的。

存储最后3个元素而没有写入结尾:尝试PALIGNR将最后一行的前一行的最后一个元素存入到reg中。然后你可以做一个单独的未对齐的商店,它与前面的商店重叠。这是很少洗牌,并且可能比movss/extractps/extractps更快。

如果您想尝试用更少的未对齐16B店的东西,尝试movss,洗牌或由4个字节(psrldq又名_mm_bsrli_si128),然后movqmovsd右移的最后8个字节存储一气呵成。 (字节方式转变是相同的执行端口洗牌上,不像每个元素位移)

为什么你做 _mm_shuffle_psshufps)?对于最后一行的第一列,低位元素已经是您想要的元素。无论如何,我认为extractps比shuffle + store更快,在non-AVX中,保留来源不被shufps破坏的源码需要采取行动。 pshufd将工作。)

+0

另一件需要牢记的事情是,它确实取决于*哪些指令集可以使用。只支持SSE/SSE2是有限制的,但具有广泛的兼容性 - 例如,x64 native需要它,所以支持x64的所有CPU必须支持它。但是,还有很多[有用的指令集](http://blogs.msdn.com/b/chuckw/archive/2012/09/11/directxmath-sse-sse2-and-arm-neon.aspx) SSE2。 –

+0

是的,'shufps'的AVX 3操作数版本可以更有效地播放单个元素。你可以做2 16B负载和左边的8B负载,所以'm_matrix []'在3个寄存器中。尽管使用AVX,内存中的VBROADCASTSS只是一个单独的uop,根本不需要shuffle端口。 '_mm_set1_ps'用gcc编译成AVX。 FMA也会减少uops的数量。 –

+0

@PeterCordes我会记住这一点,但我会放弃这个实现并尝试另一种方法。 – BlamKiwi