计算混合实复数矩阵向量乘积的最快方法是什么?
What is the fastest way to compute mixed real-complex matrix-vector products?
我面临着尽快计算矩阵-向量乘积的问题,其中矩阵是严格实数而向量是复数。
更准确地说,实矩阵是(反)对称的,并且具有非零密集块的稀疏结构。
到目前为止,我的策略是将向量拆分为实部和虚部,并为每个密集块计算实部和虚部的矩阵向量积。由于矩阵是(反)对称的,我打算同时计算一个块及其转置的乘积,所以我可以重新使用矩阵所在的缓存行。所以对于每个块,我计算 4 个矩阵- 块的矢量乘积及其每个实部和虚部的转置。
我为单个块计算这 4 个产品的代码最终如下所示:
#define no_alias __restrict__
template <typename VecType, typename MatType>
void trans_mul( const VecType* const no_alias re_in,
VecType* const no_alias re_out,
const VecType* const no_alias im_in,
VecType* const no_alias im_out,
const VecType* const no_alias re_tin,
VecType* const no_alias re_tout,
const VecType* const no_alias im_tin,
VecType* const no_alias im_tout,
const MatType* no_alias mat, // the matrix block
const int rows,
const int cols)
{
for(int j = 0; j < cols; ++j) {
for(int i = 0; i < rows; ++i) {
const auto m = *mat++; // this is mat[i, j]
re_tout[j] += m * re_tin[i]; // transposed
im_tout[j] += m * im_tin[i]; // transposed
re_out[i] -= m * re_in[j];
im_out[i] -= m * im_in[j];
}
}
}
典型的矩阵大小为 10^2 阶。
我使用带有 -Ofast -march=native
的 GCC 9.2.1 编译我的代码。从assembly output,我可以看到编译器正在自动矢量化并使用 SIMD 指令。
我正在与用 Fortran 编写的类似代码竞争,它仍然 运行 快大约 25%。当然,我的代码非常幼稚,但我仍然想不出比这更快的方法,因为积极的优化似乎非常有效。我还尝试使用四个 cblas_dgemv
调用,但这比我天真的方法要慢得多。还有什么我可以做的吗?或者有什么有用的 BLAS 例程可以适合我的情况吗?
对于相当大的矩阵(例如 >=1k),您可以使用寄存器阻塞来提高性能(与算术运算相比减少了内存数量 load/store)。
对于小矩阵,很难做到比原代码更好的了。
这是带有寄存器阻塞的结果代码:
#define no_alias __restrict__
#define mini(a, b) (((a)<(b)) ? (a) : (b))
template <typename VecType, typename MatType>
void trans_mul_v2( const VecType* const no_alias re_in,
VecType* const no_alias re_out,
const VecType* const no_alias im_in,
VecType* const no_alias im_out,
const VecType* const no_alias re_tin,
VecType* const no_alias re_tout,
const VecType* const no_alias im_tin,
VecType* const no_alias im_tout,
const MatType* no_alias mat, // the matrix block
const int rows,
const int cols)
{
// Block size (tuned for Clang/GCC on Intel Skylake processors)
const int si = 16;
const int sj = 8;
for(int bj = 0; bj < cols; bj+=sj) {
for(int bi = 0; bi < rows; bi+=si) {
if(bi+si <= rows && bj+sj <= cols)
{
// The underlying loops are expected to be unrolled by the compiler
for(int j = bj; j < bj+sj; ++j) {
for(int i = bi; i < bi+si; ++i) {
const auto m = mat[j*rows+i]; // Assume a column major ordering
re_tout[j] += m * re_tin[i];
im_tout[j] += m * im_tin[i];
re_out[i] -= m * re_in[j];
im_out[i] -= m * im_in[j];
}
}
}
else
{
// General case (borders)
for(int j = bj; j < mini(bj+sj,cols); ++j) {
for(int i = bi; i < mini(bi+si,rows); ++i) {
const auto m = mat[j*rows+i];
re_tout[j] += m * re_tin[i];
im_tout[j] += m * im_tin[i];
re_out[i] -= m * re_in[j];
im_out[i] -= m * im_in[j];
}
}
}
}
}
}
请注意,si
和 sj
的值对执行时间有很大影响。最佳值取决于编译器和底层架构。您可能应该为目标机器调整它(如果您想要性能可移植性,请将它们保持较小,尽管性能可能不是最佳的)。
结果如下(GCC 9 使用双精度类型):
With a row=100 and cols=100:
trans_mul_v1: 2.438 us
trans_mul_v2: 2.842 us
With a row=1k and cols=1k:
trans_mul_v1: 452 us
trans_mul_v2: 296 us
With a row=10k and cols=10k:
trans_mul_v1: 71.2 ms
trans_mul_v2: 35.9 ms
我面临着尽快计算矩阵-向量乘积的问题,其中矩阵是严格实数而向量是复数。 更准确地说,实矩阵是(反)对称的,并且具有非零密集块的稀疏结构。
到目前为止,我的策略是将向量拆分为实部和虚部,并为每个密集块计算实部和虚部的矩阵向量积。由于矩阵是(反)对称的,我打算同时计算一个块及其转置的乘积,所以我可以重新使用矩阵所在的缓存行。所以对于每个块,我计算 4 个矩阵- 块的矢量乘积及其每个实部和虚部的转置。
我为单个块计算这 4 个产品的代码最终如下所示:
#define no_alias __restrict__
template <typename VecType, typename MatType>
void trans_mul( const VecType* const no_alias re_in,
VecType* const no_alias re_out,
const VecType* const no_alias im_in,
VecType* const no_alias im_out,
const VecType* const no_alias re_tin,
VecType* const no_alias re_tout,
const VecType* const no_alias im_tin,
VecType* const no_alias im_tout,
const MatType* no_alias mat, // the matrix block
const int rows,
const int cols)
{
for(int j = 0; j < cols; ++j) {
for(int i = 0; i < rows; ++i) {
const auto m = *mat++; // this is mat[i, j]
re_tout[j] += m * re_tin[i]; // transposed
im_tout[j] += m * im_tin[i]; // transposed
re_out[i] -= m * re_in[j];
im_out[i] -= m * im_in[j];
}
}
}
典型的矩阵大小为 10^2 阶。
我使用带有 -Ofast -march=native
的 GCC 9.2.1 编译我的代码。从assembly output,我可以看到编译器正在自动矢量化并使用 SIMD 指令。
我正在与用 Fortran 编写的类似代码竞争,它仍然 运行 快大约 25%。当然,我的代码非常幼稚,但我仍然想不出比这更快的方法,因为积极的优化似乎非常有效。我还尝试使用四个 cblas_dgemv
调用,但这比我天真的方法要慢得多。还有什么我可以做的吗?或者有什么有用的 BLAS 例程可以适合我的情况吗?
对于相当大的矩阵(例如 >=1k),您可以使用寄存器阻塞来提高性能(与算术运算相比减少了内存数量 load/store)。 对于小矩阵,很难做到比原代码更好的了。
这是带有寄存器阻塞的结果代码:
#define no_alias __restrict__
#define mini(a, b) (((a)<(b)) ? (a) : (b))
template <typename VecType, typename MatType>
void trans_mul_v2( const VecType* const no_alias re_in,
VecType* const no_alias re_out,
const VecType* const no_alias im_in,
VecType* const no_alias im_out,
const VecType* const no_alias re_tin,
VecType* const no_alias re_tout,
const VecType* const no_alias im_tin,
VecType* const no_alias im_tout,
const MatType* no_alias mat, // the matrix block
const int rows,
const int cols)
{
// Block size (tuned for Clang/GCC on Intel Skylake processors)
const int si = 16;
const int sj = 8;
for(int bj = 0; bj < cols; bj+=sj) {
for(int bi = 0; bi < rows; bi+=si) {
if(bi+si <= rows && bj+sj <= cols)
{
// The underlying loops are expected to be unrolled by the compiler
for(int j = bj; j < bj+sj; ++j) {
for(int i = bi; i < bi+si; ++i) {
const auto m = mat[j*rows+i]; // Assume a column major ordering
re_tout[j] += m * re_tin[i];
im_tout[j] += m * im_tin[i];
re_out[i] -= m * re_in[j];
im_out[i] -= m * im_in[j];
}
}
}
else
{
// General case (borders)
for(int j = bj; j < mini(bj+sj,cols); ++j) {
for(int i = bi; i < mini(bi+si,rows); ++i) {
const auto m = mat[j*rows+i];
re_tout[j] += m * re_tin[i];
im_tout[j] += m * im_tin[i];
re_out[i] -= m * re_in[j];
im_out[i] -= m * im_in[j];
}
}
}
}
}
}
请注意,si
和 sj
的值对执行时间有很大影响。最佳值取决于编译器和底层架构。您可能应该为目标机器调整它(如果您想要性能可移植性,请将它们保持较小,尽管性能可能不是最佳的)。
结果如下(GCC 9 使用双精度类型):
With a row=100 and cols=100:
trans_mul_v1: 2.438 us
trans_mul_v2: 2.842 us
With a row=1k and cols=1k:
trans_mul_v1: 452 us
trans_mul_v2: 296 us
With a row=10k and cols=10k:
trans_mul_v1: 71.2 ms
trans_mul_v2: 35.9 ms