【问题标题】:How is array of pair<double,double> 2 times faster than two arrays of double C++pair<double,double> 的数组如何比两个双 C++ 数组快 2 倍
【发布时间】:2020-11-08 04:53:29
【问题描述】:
#include <iostream>
#include <chrono>
#include <random>
#include <time.h>
using namespace std;

typedef pair<double,double> pd;
#define x first
#define y second
#define cell(i,j,w) ((i)*(w) + (j))

class MyTimer
{
private:
    std::chrono::time_point<std::chrono::steady_clock> starter;
    std::chrono::time_point<std::chrono::steady_clock> ender;

public:
    void startCounter() {
        starter = std::chrono::steady_clock::now();
    }

    long long getCounter() {
        ender = std::chrono::steady_clock::now();
        return std::chrono::duration_cast<std::chrono::milliseconds>(ender - starter).count();
    }
};
        
int main()
{
    const int n = 5000;
    int* value1 = new int[(n + 1) * (n + 1)];
    int* value2 = new int[(n + 1) * (n + 1)];
    double* a = new double[(n + 1) * (n + 1)];
    double* b = new double[(n + 1) * (n + 1)];
    pd* packed = new pd[(n + 1) * (n + 1)];
    MyTimer timer;

    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= n; j++) {
            value1[cell(i, j, n + 1)] = rand() % 5000;
            value2[cell(i, j, n + 1)] = rand() % 5000;
        }

    for (int i = 1; i <= n; i++) {
        a[cell(i, 0, n + 1)] = 0;
        a[cell(0, i, n + 1)] = 0;
        b[cell(i, 0, n + 1)] = 0;
        b[cell(0, i, n + 1)] = 0;
        packed[cell(i, 0, n + 1)] = pd(0, 0);
        packed[cell(0, i, n + 1)] = pd(0, 0);
    }

    for (int tt=1; tt<=5; tt++)
    {
        timer.startCounter();
        for (int i=1; i<=n; i++)
            for (int j = 1; j <= n; j++) {
                // packed[i][j] = packed[i-1][j] + packed[i][j-1] - packed[i-1][j-1] + value1[i][j]
                packed[cell(i, j, n + 1)].x = packed[cell(i - 1, j, n + 1)].x + packed[cell(i, j - 1, n + 1)].x - packed[cell(i - 1, j - 1, n + 1)].x + value1[cell(i, j, n + 1)];
                packed[cell(i, j, n + 1)].y = packed[cell(i - 1, j, n + 1)].y + packed[cell(i, j - 1, n + 1)].y - packed[cell(i - 1, j - 1, n + 1)].y + value1[cell(i, j, n + 1)] * value1[cell(i, j, n + 1)];
            }
        cout << "Time packed  = " << timer.getCounter() << "\n";

        timer.startCounter();
        for (int i=1; i<=n; i++)
            for (int j = 1; j <= n; j++) {
                // a[i][j] = a[i-1][j] + a[i][j-1] - a[i-1][j-1] + value2[i][j];
                // b[i][j] = b[i-1][j] + b[i][j-1] - b[i-1][j-1] + value2[i][j] * value2[i][j];
                a[cell(i, j, n + 1)] = a[cell(i - 1, j, n + 1)] + a[cell(i, j - 1, n + 1)] - a[cell(i - 1, j - 1, n + 1)] + value2[cell(i, j, n + 1)];
                b[cell(i, j, n + 1)] = b[cell(i - 1, j, n + 1)] + b[cell(i, j - 1, n + 1)] - b[cell(i - 1, j - 1, n + 1)] + value2[cell(i, j, n + 1)] * value2[cell(i, j, n + 1)];
            }
        cout << "Time separate = " << timer.getCounter() << "\n\n";
    }

    delete[] value1;
    delete[] value2;
    delete[] a;
    delete[] b;
    delete[] packed;
}

所以我正在计算一个 2D 前缀表(总面积表)。我注意到标题中的属性。

使用命令行或 Visual Studio 发布模式使用 CUDA nvcc 编译器(带 -O2)时,结果快 2 倍(单独需要 200 毫秒,打包需要 100 毫秒)第一次运行,但仅后续运行速度提高 25%(这是因为 value2[] 在第一个循环之后被缓存)。在我的具有更多计算步骤的实际程序中(计算 SAT 只是第 1 步),它总是快 2 倍,因为 value1[] 和 value2[] 肯定已从缓存中逐出。

我知道打包数组更快,因为现代 Intel CPU 一次将 32-64 字节读取到缓存中。因此,通过将两个数组打包在一起,它可以在 1 个主内存 (RAM) 访问中读取两个数据,而不是 2 个。但是为什么提速这么高?除了内存访问,CPU 还必须在每个循环中执行 6 次加法、2 次减法和 1 次乘法。将内存访问减半后的 2 倍加速是 100% 的改进效率(Amdahl 定律),就像那些加/乘操作不存在一样。怎么可能?

我确信它与 CPU 流水线有关,但无法更彻底地解释。任何人都可以在指令延迟/内存访问延迟/汇编方面进一步解释这一点吗?谢谢。

代码不使用任何 GPU,因此任何其他好的编译器都应该提供与 nvcc 相同的 2 倍加速。在 g++ 9.3.0 (g++ file.cpp -O2 -std=c++11 -o file.exe) 上,它也是 2 倍加速。 CPU是Intel i7-7700

我已经使用命令行参数-O2 -std=c++11 运行了这个程序here 和here2,它还显示了1.5-2 倍的加速。 使用 n = 3000,更大,它不会运行(毕竟免费的 VM 服务)。所以这不仅仅是我的电脑

【问题讨论】:

  • 您是否在启用编译器优化的情况下编译您的代码?如果没有,请不时执行此操作,然后再次进行基准测试。对未优化的调试版本进行基准测试毫无意义。
  • 是的,它带有 -O2 以及释放模式。我已经用 nvcc 和 g++ 9.3 对其进行了测试,在这两种情况下,结果都快了 2 倍
  • 你的代码中哪里使用了pair?
  • @S.M. -- 在这里:typedef pair&lt;double,double&gt; pd; -- 但这并不是你的错 -- 这是我最近从很多海报中看到的那些疯狂的宏。不仅类型被宏隐藏,它还被两个字母的小写宏隐藏。
  • 得到了不同的基准Here(但必须减小大小以避免超时)

标签: c++ caching memory cpu-registers cpu-cache


【解决方案1】:

答案在于不同级别内存的访问延迟,从 L1 缓存 -> 主内存 (RAM)。

L1 缓存中的数据需要 ~~5 个周期才能访问,而 RAM 中的数据需要 50-100 个周期。同时,add/sub/mult 操作需要 3-5 个周期。

因此,性能的主要限制因素是主内存访问。所以通过将主存请求的数量减少一半,性能几乎翻倍

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多