在 C 中将静态大小的向量与静态大小的常量非方矩阵相乘的最快方法,生成 15 元素输出向量

Fastest way to multiply a static sized vector with a static sized constant non-square matrix in C, producing a 15-element output vector

我有一个很大的 float 矩阵,A,列长大约 100000,行长大约 15。

然后我们有一个 uint8_t 向量 X,其行大小为 100000。AX 都具有静态大小,永远不会改变大小。

X 可以更改其值,但 A 保持不变。

那么在 C 中计算 A*X 生成 15 元素乘积向量的最绝对最快的方法是什么? 写这样的东西而不是使用 for 循环是一种好方法吗?

 A(0,0)*X(0) + A(0,1)*X(1) + A(0,2)*X(2) + ... +  A(0,n)*X(n)
 A(1,0)*X(0) + A(1,1)*X(1) + A(1,2)*X(2) + ... +  A(1,n)*X(n)
 ......
 A(m,0)*X(0) + A(m,1)*X(1) + A(m,2)*X(2) + ... +  A(m,n)*X(n)

我假设你的矩阵是密集的,而不是稀疏的。 (主要是 0.0)。此外,它并非主要由 0.01.0 元素组成;如果是这种情况,请将其转换为位图,您可以将其用于矢量的屏蔽总和。


我假设这些是 floatdouble 值,并且您想 运行 在典型的现代机器上使用 SIMD,例如 x86-64 或 AArch64。循环可能 更好 因为您需要编译器自动矢量化以获得最佳性能,并且循环比完全展开的代码更有可能。

您可能希望将 X[] 的每个 SIMD 块与 4 个左右的 A[][] 数据块中的每一个一起使用,因此 X[] 总共只需要加载到寄存器中 4 次. A[][] 的每一行仅被读取一次,因此 A[][].

无法重复使用数据

缓存阻塞还可以将必须​​将 X[] 数据提取到 L1d 缓存中的次数减少到 1 次。但是您可能不想编写一个并行执行 15 次求和的循环。从 A 中获取 15 个流可能不是一个好主意,而 x86-64(没有 AVX512)只有 16 个 SIMD 寄存器,因此除非您小心地手动矢量化,否则编译器可能会将矢量累加器溢出到堆栈并引入存储转发瓶颈。


不要完全展开:代码缓存未命中比循环开销造成的伤害要大得多。

编译器通常不会将直线代码回滚到循环中,即使那样会更好。所以你会得到一个 huge 没有分支的 asm 块。 CPU 不是从 L1 指令缓存(或 uop 缓存或循环缓冲区)中重用相同的循环体,而是必须从内存中获取代码,从而占用与数据带宽竞争的带宽。


在实践中你应该调用一个 BLAS 库函数:它将使用 SIMD 对其安装的系统中的特定 CPU 进行大量优化。

还是不行,根据上次更新,Xuint8_t X[]。我怀疑是否存在可以从 uint8_t 动态转换为 float 的 BLAS 库,但这可能是您想要节省内存带宽的方式,而不是单独转换为 tmp float 向量。尽管这样做 + 调用一个好的 BLAS 函数仍然比糟糕的矢量化代码更好,如果你的编译器不能很好地处理你的纯 C 循环。

展开更多次使用 X 数据的每个 SIMD 向量会特别好,可以分摊更多次的转换开销。可能会在 8 点之前展开。