【问题标题】:Why is this SSE2 code performing inconsistently?为什么此 SSE2 代码执行不一致?
【发布时间】:2015-07-19 15:28:46
【问题描述】:

作为一项学习练习,我正在尝试在各种架构上使用 SIMD 加速矩阵乘法代码。我的 SSE2 3D 矩阵乘法代码有一个奇怪的问题,它的性能在两个极端之间跳跃,大约 5 毫秒(预期)或大约 100 毫秒(100 万次操作)。

这段代码唯一“坏”的事情是未对齐的存储/加载以及最后将向量存储到内存中而没有第 4 个元素践踏内存的 hack。这可以解释一些性能差异,但性能差异如此之大的事实让我怀疑我错过了一些重要的东西。

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

见下面的代码。 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)));
}

【问题讨论】:

  • 做100万次测试几次,得到平均时间。只做一次测试会得到不可靠的结果。
  • @CoffeeandCode 我已经将平均值作为测试台的一部分。与具有 SSE2 的 2D 和 4D 矩阵相比,平均时间仍然是异常的。踩踏行为无法用统计数据解释,代码有问题。
  • Idk,伙计。如果它有效,我不会说它有问题。顺便说一句,你怎么会做一个未对齐的商店?
  • @CoffeeandCode 3x3 矩阵的行的步长为 12 字节,但 SSE 中的对齐指令要求地址在 16 字节边界上对齐。所以在第一行之后,我们不能使用对齐的加载/存储。
  • 这就是为什么我建议只使用 4x4 或 3x4 矩阵的原因,除非您真的受限于空间。在另一个领域:您是否在其他线程接触相同数据的情况下运行这些测试?当然,这会引发许多其他问题,例如什么架构等……为了缩小范围,请尝试对适当大小的数据进行操作的仅对齐变体。

标签: c++ sse intrinsics icc


【解决方案1】:

这样的性能下降听起来像是您的数据有时可能会越过页面行,而不仅仅是缓存行。如果您正在测试多个不同矩阵的缓冲区,而不是重复地测试同一个小矩阵,那么在另一个 CPU 内核上运行的其他东西可能会将您的缓冲区推出 L3?

代码中的性能问题(不能解释 20 倍方差。这些应该总是很慢):

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

存储最后 3 个元素而不写入末尾:尝试使用PALIGNR 将前一行的最后一个元素放入包含最后一行的 reg 中。然后你可以做一个未对齐的存储,它与前面的存储重叠。这比 movss / extractps / extractps 要快得多。

如果您想尝试较少未对齐的 16B 存储,请尝试 movss、随机播放或右移 4 个字节(psrldq aka _mm_bsrli_si128),然后使用 movq 或 movsd 存储最后一个一口气8个字节。 (与每个元素的位移不同,逐字节移位与随机播放在同一个执行端口上)

你为什么要三个 _mm_shuffle_ps (shufps)?对于最后一行的第一列,低元素已经是您想要的元素。无论如何,我认为extractps 比 shuffle + store 更快,在非 AVX 上保护源不被shufps 破坏采取了行动。 pshufd 会起作用。)

【讨论】:

  • 要记住的另一件事是,它实际上取决于确切地您可以使用哪些指令集。只坚持 SSE/SSE2 是有限制的,但具有广泛的兼容性——例如 x64 本机需要它,因此所有支持 x64 的 CPU 都必须支持它。但是,SSE2 之外还有很多useful instructions sets。
  • 是的,shufps 的 AVX 3 操作数版本将提高单个元素的广播效率。您可以在左侧执行 2 个 16B 加载和 8B 加载,因此 m_matrix[] 在 3 个寄存器中。尽管使用 AVX,但内存中的 VBROADCASTSS 只是一个微指令,甚至根本不需要随机播放端口。 _mm_set1_ps 在 gcc 上使用 AVX 编译为。 FMA 也会减少微指令的数量。
  • @PeterCordes 我会记住这一点,但我将放弃这个实现并尝试另一种方法。
  • 可能是个好电话。最近这里有很多关于短向量/矩阵的问题,其中一些有有趣的答案。我忘记了细节,因为当时它与我没有直接关系!
  • 无论如何,我还是将其标记为答案,它对处理实现有很好的建议(即使它有缺陷)
猜你喜欢
  • 2023-04-02
  • 2016-02-01
  • 2017-01-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多