在 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。A
和 X
都具有静态大小,永远不会改变大小。
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.0
和 1.0
元素组成;如果是这种情况,请将其转换为位图,您可以将其用于矢量的屏蔽总和。
我假设这些是 float
或 double
值,并且您想 运行 在典型的现代机器上使用 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 进行大量优化。
还是不行,根据上次更新,X
是uint8_t X[]
。我怀疑是否存在可以从 uint8_t
动态转换为 float
的 BLAS 库,但这可能是您想要节省内存带宽的方式,而不是单独转换为 tmp float
向量。尽管这样做 + 调用一个好的 BLAS 函数仍然比糟糕的矢量化代码更好,如果你的编译器不能很好地处理你的纯 C 循环。
展开更多次使用 X
数据的每个 SIMD 向量会特别好,可以分摊更多次的转换开销。可能会在 8 点之前展开。
我有一个很大的 float
矩阵,A
,列长大约 100000,行长大约 15。
然后我们有一个 uint8_t
向量 X
,其行大小为 100000。A
和 X
都具有静态大小,永远不会改变大小。
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.0
和 1.0
元素组成;如果是这种情况,请将其转换为位图,您可以将其用于矢量的屏蔽总和。
我假设这些是 float
或 double
值,并且您想 运行 在典型的现代机器上使用 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 进行大量优化。
还是不行,根据上次更新,X
是uint8_t X[]
。我怀疑是否存在可以从 uint8_t
动态转换为 float
的 BLAS 库,但这可能是您想要节省内存带宽的方式,而不是单独转换为 tmp float
向量。尽管这样做 + 调用一个好的 BLAS 函数仍然比糟糕的矢量化代码更好,如果你的编译器不能很好地处理你的纯 C 循环。
展开更多次使用 X
数据的每个 SIMD 向量会特别好,可以分摊更多次的转换开销。可能会在 8 点之前展开。