【问题标题】:read/write to large array using large loop - execution time concerns使用大循环读取/写入大型数组 - 执行时间问题
【发布时间】:2021-04-11 17:09:29
【问题描述】:

所以最近我遇到了一个我认为很有趣但无法完全解释的问题。我在以下代码中强调了问题的性质:

#include <cstring>
#include <chrono> 
#include <iostream> 

#define NLOOPS 10

void doWorkFast(int total, int *write, int *read)
{
    for (int j = 0; j < NLOOPS; j++) {
        for (int i = 0; i < total; i++) {
            write[i] = read[i] + i;
        }
    }
}

void doWorkSlow(int total, int *write, int *read, int innerLoopSize)
{
    for (int i = 0; i < NLOOPS; i++) {
        for (int j = 0; j < total/innerLoopSize; j++) {
            for (int k = 0; k < innerLoopSize; k++) {
                write[j*k + k] = read[j*k + k] + j*k + k;
            }
        }
    }
}


int main(int argc, char *argv[])
{
    int n = 1000000000;
    
    int *heapMemoryWrite = new int[n];
    int *heapMemoryRead = new int[n];
    

    for (int i = 0; i < n; i++)
    {
        heapMemoryRead[i] = 1;
    }

    std::memset(heapMemoryWrite, 0, n * sizeof(int));   

    auto start1 = std::chrono::high_resolution_clock::now();

    doWorkFast(n,heapMemoryWrite, heapMemoryRead);
    

    auto finish1 = std::chrono::high_resolution_clock::now();  
    auto duration1 = std::chrono::duration_cast<std::chrono::microseconds>(finish1 - start1); 

    for (int i = 0; i < n; i++)
    {
        heapMemoryRead[i] = 1;
    }

    std::memset(heapMemoryWrite, 0, n * sizeof(int));

    auto start2 = std::chrono::high_resolution_clock::now();
    
    doWorkSlow(n,heapMemoryWrite, heapMemoryRead, 10);


    auto finish2 = std::chrono::high_resolution_clock::now();  
    auto duration2 = std::chrono::duration_cast<std::chrono::microseconds>(finish2 - start2); 

    std::cout << "Small inner loop:" << duration1.count() << " microseconds.\n" << 
                 "Large inner loop:" << duration2.count() << " microseconds." << std::endl; 

    delete[] heapMemoryWrite;
    delete[] heapMemoryRead;
}

查看两个 doWork* 函数,对于每次迭代,我们读取相同的地址添加相同的值并写入相同的地址。我知道在doWorkSlow 实现中,我们正在执行一两个以上的操作来解决j*k + k,但是,我认为相对于为内存读取和执行加载/存储所需的时间而言,假设它是合理安全的。写,这些操作的时间贡献可以忽略不计。

尽管如此,在我的 i7-3700 上使用 g++ --version 7.5.0 时,doWorkSlow 所用的时间(46.8 秒)大约是 doWorkFast(25.5 秒)的两倍。虽然想到了缓存预取和分支预测之类的东西,但我没有很好的解释为什么doWorkFastdoWorkSlow 快得多。有人有见解吗?

谢谢

【问题讨论】:

  • 你的编译标志是什么?
  • 我没有使用任何标志,但是当我使用 -o3 -o2 和 -o1 进行编译时,我得到了相似的结果(即慢需要 ~2x)。
  • 我的猜测是差异来自数组访问,doWorkFast 是一个接一个地访问数组元素(递增 1),但 doWorkSlow 将索引递增 j+1,这使得缓存预置更加困难。
  • 如果不进行优化,您可能会在每次迭代时计算total/innerLoopSizej*k+k 也是如此。尝试计算一次 j*k+k 并将其存储在一个临时(常量)变量中,在 write 语句之前,例如const int index = j * k + k; write[index] = ...;

标签: c++ arrays loops optimization heap-memory


【解决方案1】:

查看两个 doWork* 函数,对于每次迭代,我们读取相同的地址添加相同的值并写入相同的地址。

这不是真的!

doWorkFast 中,您以增量方式索引每个整数,如array[i]

array[0]
array[1]
array[2]
array[3]

doWorkSlow 中,您将每个整数索引为array[j*k + k],它会跳来跳去并重复。

例如,当 j 为 10 时,您从 0 开始迭代 k,您正在访问

array[0]    // 10*0+0
array[11]   // 10*1+1
array[22]   // 10*2+2
array[33]   // 10*3+3

这将阻止您的优化器使用可以同时对许多相邻整数进行操作的指令。

【讨论】:

  • 太棒了!感谢您指出了这一点。在这个例子中,以这种方式编码索引是一个实现缺陷(应该是j*innerLoopSize + k,然而,在解决问题时,我看到执行时间是相似的。不过,我看到的主要教训是我们应该是否尽可能使用 SIMD 进行连续内存访问?
猜你喜欢
  • 2020-02-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-07-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多