为什么此 SSE2 代码执行不一致?
Why is this SSE2 code performing inconsistently?
作为一项学习练习,我正在尝试在各种体系结构上使用 SIMD 加速矩阵乘法代码。我的 SSE2 的 3D 矩阵乘法代码有一个奇怪的问题,它的性能在两个极端之间跳跃,即 ~5ms(预期)或 ~100ms 进行 100 万次操作。
此代码所做的唯一一件事 "bad" 是未对齐的 stores/loads 和末尾的 hack,用于将向量存储到内存中,而第 4 个元素不会破坏内存。这可以解释一些性能差异,但性能差异如此之大这一事实让我怀疑我遗漏了一些重要的东西。
我已经尝试了一些方法,但睡一觉后我会再尝试一下。
见下面的代码。 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)));
}
像这样的性能下降听起来您的数据有时可能会越过页面行,而不仅仅是缓存行。如果您在许多不同矩阵的缓冲区上进行测试,而不是重复测试同一个小矩阵,也许另一个 CPU 内核上的其他东西 运行 正在将您的缓冲区推出 L3?
代码中的性能问题(不能解释 20 倍方差。这些应该总是很慢):
_mm_set1_ps(m_matrix[3])
等等都会出问题。它需要 pshufd
或 movaps + shufps
来广播一个元素。不过,我认为这对于 matmuls 来说是不可避免的。
存储最后 3 个元素而不写入末尾:尝试 PALIGNR
将前一行的最后一个元素放入具有最后一行的 reg 中。然后你可以做一个单独的未对齐的商店,它与前面的商店重叠。这比 movss
/ extractps
/ extractps
.
少很多洗牌
如果您想尝试使用较少的未对齐 16B 存储,请尝试 movss
、随机播放或右移 4 个字节(psrldq
又名 _mm_bsrli_si128
),然后 movq
或 movsd
一次性存储最后 8 个字节。 (与按元素移位不同,逐字节移位与随机播放在同一执行端口上)
你为什么要做三个_mm_shuffle_ps
(shufps
)?对于最后一行的第一列,低元素已经是您想要的元素。无论如何,我认为 extractps
在非 AVX 上比 shuffle + store 更快,在非 AVX 上保护源不被 shufps
破坏需要采取行动。 pshufd
会起作用。)
作为一项学习练习,我正在尝试在各种体系结构上使用 SIMD 加速矩阵乘法代码。我的 SSE2 的 3D 矩阵乘法代码有一个奇怪的问题,它的性能在两个极端之间跳跃,即 ~5ms(预期)或 ~100ms 进行 100 万次操作。
此代码所做的唯一一件事 "bad" 是未对齐的 stores/loads 和末尾的 hack,用于将向量存储到内存中,而第 4 个元素不会破坏内存。这可以解释一些性能差异,但性能差异如此之大这一事实让我怀疑我遗漏了一些重要的东西。
我已经尝试了一些方法,但睡一觉后我会再尝试一下。
见下面的代码。 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)));
}
像这样的性能下降听起来您的数据有时可能会越过页面行,而不仅仅是缓存行。如果您在许多不同矩阵的缓冲区上进行测试,而不是重复测试同一个小矩阵,也许另一个 CPU 内核上的其他东西 运行 正在将您的缓冲区推出 L3?
代码中的性能问题(不能解释 20 倍方差。这些应该总是很慢):
_mm_set1_ps(m_matrix[3])
等等都会出问题。它需要 pshufd
或 movaps + shufps
来广播一个元素。不过,我认为这对于 matmuls 来说是不可避免的。
存储最后 3 个元素而不写入末尾:尝试 PALIGNR
将前一行的最后一个元素放入具有最后一行的 reg 中。然后你可以做一个单独的未对齐的商店,它与前面的商店重叠。这比 movss
/ extractps
/ extractps
.
如果您想尝试使用较少的未对齐 16B 存储,请尝试 movss
、随机播放或右移 4 个字节(psrldq
又名 _mm_bsrli_si128
),然后 movq
或 movsd
一次性存储最后 8 个字节。 (与按元素移位不同,逐字节移位与随机播放在同一执行端口上)
你为什么要做三个_mm_shuffle_ps
(shufps
)?对于最后一行的第一列,低元素已经是您想要的元素。无论如何,我认为 extractps
在非 AVX 上比 shuffle + store 更快,在非 AVX 上保护源不被 shufps
破坏需要采取行动。 pshufd
会起作用。)