首页 > 解决方案 > 为什么使用 Eigen 密集动态矩阵的 setZero 比静态矩阵更快?

问题描述

我编译了以下代码进行测试

#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分配导致内存分配开销的新内存吗?

标签: c++performanceeigen

解决方案


TL;DR:性能结果强烈依赖于目标平台和编译器。

首先,这种说法并不总是正确的。实际上,动态版本在我的机器上更快,因为我得到了结果dynamic: 0.128093 static: 0.142624(使用-std=c++20 -O3 -DNDEBUG和 GCC 10.2.1)。原因是 GCC/Clangmemset在动态版本中生成代码调用,而它们在静态情况下生成许多直接归零SIMD 指令(更具体地说是 128 位 SSE 指令)。在某些情况下,memset实现可以比主流编译器(例如 GCC 和 Clang)生成的代码更快。

实际上,memset如果 libc 针对目标机器进行了优化,而 GCC 可能不使用它,则可以使用更广泛的 SIMD 指令。这可能是 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 字节宽得多的存储。但是,它也有很大的启动开销。有关此指令及其性能的更多信息,请参阅这篇非常详细的帖子

其他参数对于比较这两种实现很重要:CPU 缓存的大小/种类、RAM 的速度以及矩阵大小。实际上,如果矩阵不适合 CPU 缓存,那么由于非临时存储(NTS) ,memset实现通常会更快。这在使用写入分配缓存的处理器(如英特尔处理器)上尤其如此,因为这样的处理器将在没有 NTS 指令的情况下从 RAM 中读取矩阵,然后再将零写入其中(这个过程比使用 NTS 直接写入内存要慢 2 倍指示)。

问题是 GCC/Clang 假设静态矩阵足够小,因此它可能适合 CPU 缓存(并且可能在缓存中,因为矩阵实际上应该存储在堆栈中),因此在大量展开的循环可能比使用更快memset,后者使用多个版本并根据数据大小和目标平台选择一个好的版本。希望这通常是正确的,因此您获得的性能结果。请注意,当大小是动态的,因为它在编译时是未知的,所以很难做出这个假设(没有配置文件引导的优化)。

请注意,其他参数可能很重要,例如执行顺序(因为频率缩放)和内存对齐(尽管它们在我的机器上似乎并不重要)。


推荐阅读