为什么使用 Eigen 密集动态矩阵的 setZero 比静态矩阵更快?
Why is setZero faster with Eigen dense dynamic matrix than static matrix?
我编译了以下代码进行测试
#include <iostream>
#include <Eigen/Dense>
#include <chrono>
int main()
{
constexpr size_t t = 10000;
constexpr size_t size = 100;
auto t1 = std::chrono::steady_clock::now();
Eigen::Matrix<double, Eigen::Dynamic, Eigen::Dynamic> m1;
for (size_t i = 0; i < t; ++i)
{
m1.setZero(size, size);
}
auto t2 = std::chrono::steady_clock::now();
Eigen::Matrix<double, size, size> m2;
for (size_t i = 0; i < t; ++i)
{
m2.setZero();
}
auto t3 = std::chrono::steady_clock::now();
double t12 = static_cast<std::chrono::duration<double>>(t2 - t1).count();
double t23 = static_cast<std::chrono::duration<double>>(t3 - t2).count();
std::cout << "dynamic: " << t12 << " static: " << t23 << std::endl;
return 0;
}
我发现动态矩阵总是比静态矩阵快
编译为 -O0
dynamic: 2.34759 static: 4.29692
编译为 -O3
dynamic: 0.0170274 static: 0.0363988
凭直觉,setZero
用动态矩阵分配新内存不会导致内存分配开销吗?
TL;DR: 性能结果强烈依赖于目标平台和编译器。
首先,这个断言并不总是正确的。实际上,动态版本在我的机器上更快,因为我得到了结果 dynamic: 0.128093 static: 0.142624
(使用 -std=c++20 -O3 -DNDEBUG
和 GCC 10.2.1)。原因是 GCC/Clang 在动态版本中生成调用 memset
的代码,而它们在静态情况下生成许多直接归零 SIMD 指令(更具体地说是 128 位上交所说明)。在某些情况下,memset
实现比主流编译器(例如 GCC 和 Clang)生成的代码更快。
确实,如果 libc 针对目标机器进行了优化,memset
可以使用更宽的 SIMD 指令,而 GCC 可能不会使用它。这可能是 x86-64 平台的情况,其中 AVX-256 和 AVX-512 可用,但主流编译器不会自动启用,因为生成的二进制文件需要 复古兼容性 在所有 x86-64 平台上。当启用平台上可用的最先进的 SIMD 指令集时,静态版本的结果往往要好得多,但这并不总是如此。在我的机器上,我得到 dynamic: 0.105638 static: 0.0929698
添加了 -march=native
。
深入分析发现我的libc(GNU libc 2.31)没有直接使用SIMD指令,而是rep stos
指令。现代处理器可以优化此指令并执行比 1 字节宽得多的存储。但是,它也有很大的启动开销。有关此指令及其性能的更多信息,请参阅 。
其他参数对于比较两个实现很重要:size/kind 的 CPU 缓存和 RAM 的速度以及矩阵大小。事实上,如果矩阵不适合 CPU 缓存,那么 memset
实现通常更快,因为 非临时存储 (NTS)。在使用 write-allocate 缓存的处理器(如 Intel 处理器)上尤其如此,因为这样的处理器将在没有 NTS 指令的情况下将零写入 RAM 之前从 RAM 读取矩阵(此过程最多 2比使用 NTS 指令直接写入内存要慢)。
事情是 GCC/Clang 假设静态矩阵足够小,因此它可能适合 CPU 缓存(并且可能在缓存中,因为矩阵应该存储在堆栈中在实践中),因此在大量展开的循环中使用经典存储指令可能比仅使用 memset
更快,后者使用多个版本和 select 一个关于数据大小和目标平台的好版本。希望这通常是正确的,因此您可以获得性能结果。请注意,当大小是动态的时,很难做出这种假设(没有配置文件引导优化),因为它在编译时是未知的。
请注意,其他参数可能很重要,例如执行顺序(因为频率缩放)和内存对齐(尽管它们在我的机器上似乎并不重要)。
我编译了以下代码进行测试
#include <iostream>
#include <Eigen/Dense>
#include <chrono>
int main()
{
constexpr size_t t = 10000;
constexpr size_t size = 100;
auto t1 = std::chrono::steady_clock::now();
Eigen::Matrix<double, Eigen::Dynamic, Eigen::Dynamic> m1;
for (size_t i = 0; i < t; ++i)
{
m1.setZero(size, size);
}
auto t2 = std::chrono::steady_clock::now();
Eigen::Matrix<double, size, size> m2;
for (size_t i = 0; i < t; ++i)
{
m2.setZero();
}
auto t3 = std::chrono::steady_clock::now();
double t12 = static_cast<std::chrono::duration<double>>(t2 - t1).count();
double t23 = static_cast<std::chrono::duration<double>>(t3 - t2).count();
std::cout << "dynamic: " << t12 << " static: " << t23 << std::endl;
return 0;
}
我发现动态矩阵总是比静态矩阵快
编译为 -O0
dynamic: 2.34759 static: 4.29692
编译为 -O3
dynamic: 0.0170274 static: 0.0363988
凭直觉,setZero
用动态矩阵分配新内存不会导致内存分配开销吗?
TL;DR: 性能结果强烈依赖于目标平台和编译器。
首先,这个断言并不总是正确的。实际上,动态版本在我的机器上更快,因为我得到了结果 dynamic: 0.128093 static: 0.142624
(使用 -std=c++20 -O3 -DNDEBUG
和 GCC 10.2.1)。原因是 GCC/Clang 在动态版本中生成调用 memset
的代码,而它们在静态情况下生成许多直接归零 SIMD 指令(更具体地说是 128 位上交所说明)。在某些情况下,memset
实现比主流编译器(例如 GCC 和 Clang)生成的代码更快。
确实,如果 libc 针对目标机器进行了优化,memset
可以使用更宽的 SIMD 指令,而 GCC 可能不会使用它。这可能是 x86-64 平台的情况,其中 AVX-256 和 AVX-512 可用,但主流编译器不会自动启用,因为生成的二进制文件需要 复古兼容性 在所有 x86-64 平台上。当启用平台上可用的最先进的 SIMD 指令集时,静态版本的结果往往要好得多,但这并不总是如此。在我的机器上,我得到 dynamic: 0.105638 static: 0.0929698
添加了 -march=native
。
深入分析发现我的libc(GNU libc 2.31)没有直接使用SIMD指令,而是rep stos
指令。现代处理器可以优化此指令并执行比 1 字节宽得多的存储。但是,它也有很大的启动开销。有关此指令及其性能的更多信息,请参阅
其他参数对于比较两个实现很重要:size/kind 的 CPU 缓存和 RAM 的速度以及矩阵大小。事实上,如果矩阵不适合 CPU 缓存,那么 memset
实现通常更快,因为 非临时存储 (NTS)。在使用 write-allocate 缓存的处理器(如 Intel 处理器)上尤其如此,因为这样的处理器将在没有 NTS 指令的情况下将零写入 RAM 之前从 RAM 读取矩阵(此过程最多 2比使用 NTS 指令直接写入内存要慢)。
事情是 GCC/Clang 假设静态矩阵足够小,因此它可能适合 CPU 缓存(并且可能在缓存中,因为矩阵应该存储在堆栈中在实践中),因此在大量展开的循环中使用经典存储指令可能比仅使用 memset
更快,后者使用多个版本和 select 一个关于数据大小和目标平台的好版本。希望这通常是正确的,因此您可以获得性能结果。请注意,当大小是动态的时,很难做出这种假设(没有配置文件引导优化),因为它在编译时是未知的。
请注意,其他参数可能很重要,例如执行顺序(因为频率缩放)和内存对齐(尽管它们在我的机器上似乎并不重要)。