代码之家  ›  专栏  ›  技术社区  ›  ead

在一级缓存中缓存2KB数据时内存带宽崩溃的原因

  •  27
  • ead  · 技术社区  · 7 年前

    在一个自学项目中,我通过以下代码测量内存的带宽(此处转述,整个代码在问题末尾):

    unsigned int doit(const std::vector<unsigned int> &mem){
       const size_t BLOCK_SIZE=16;
       size_t n = mem.size();
       unsigned int result=0;
       for(size_t i=0;i<n;i+=BLOCK_SIZE){           
                 result+=mem[i];
       }
       return result;
    }
    
    //... initialize mem, result and so on
    int NITER = 200; 
    //... measure time of
       for(int i=0;i<NITER;i++)
           resul+=doit(mem)
    

    BLOCK_SIZE g++ -O3

    通过改变向量的大小,我可以观察到L1(*)、L2-、L3缓存和RAM内存的预期带宽:

    enter image description here

    然而,有一个影响我真的很难解释:一级缓存的测量带宽在2 kB左右时崩溃,这里的分辨率稍高一些:

    enter image description here

    我可以在我访问过的所有机器上(有英特尔Broadwell和英特尔Haswell处理器)复制结果。

    我的问题: 内存大小在2 KB左右时性能崩溃的原因是什么?

    编辑 :当选择内部for循环中的步长为

    • 8(而不是16)坍塌发生在1KB

    可以用中所示的分支未命中进行解释 @user10605163's great answer .


    用于复制结果的列表

    bandwidth.cpp :

    #include <vector>
    #include <chrono>
    #include <iostream>
    #include <algorithm>
    
    
    //returns minimal time needed for one execution in seconds:
    template<typename Fun>
    double timeit(Fun&& stmt, int repeat, int number)
    {  
       std::vector<double> times;
       for(int i=0;i<repeat;i++){
           auto begin = std::chrono::high_resolution_clock::now();
           for(int i=0;i<number;i++){
              stmt();
           }
           auto end = std::chrono::high_resolution_clock::now();
           double time = std::chrono::duration_cast<std::chrono::nanoseconds>(end-begin).count()/1e9/number;
           times.push_back(time);
       }
       return *std::min_element(times.begin(), times.end());
    }
    
    
    const int NITER=200;
    const int NTRIES=5;
    const size_t BLOCK_SIZE=16;
    
    
    struct Worker{
       std::vector<unsigned int> &mem;
       size_t n;
       unsigned int result;
       void operator()(){
            for(size_t i=0;i<n;i+=BLOCK_SIZE){           
                 result+=mem[i];
            }
       }
    
       Worker(std::vector<unsigned int> &mem_):
           mem(mem_), n(mem.size()), result(1)
       {}
    };
    
    double PREVENT_OPTIMIZATION=0.0;
    
    
    double get_size_in_kB(int SIZE){
       return SIZE*sizeof(int)/(1024.0);
    }
    
    double get_speed_in_GB_per_sec(int SIZE){
       std::vector<unsigned int> vals(SIZE, 42);
       Worker worker(vals);
       double time=timeit(worker, NTRIES, NITER);
       PREVENT_OPTIMIZATION+=worker.result;
       return get_size_in_kB(SIZE)/(1024*1024)/time;
    }
    
    
    int main(){
    
       int size=BLOCK_SIZE*16;
       std::cout<<"size(kB),bandwidth(GB/s)\n";
       while(size<10e3){
           std::cout<<get_size_in_kB(size)<<","<<get_speed_in_GB_per_sec(size)<<"\n";
           size=(static_cast<int>(size+BLOCK_SIZE)/BLOCK_SIZE)*BLOCK_SIZE;
       }
    
       //ensure that nothing is optimized away:
       std::cerr<<"Sum: "<<PREVENT_OPTIMIZATION<<"\n";
    }
    

    create_report.py :

    import sys
    import pandas as pd
    import matplotlib.pyplot as plt
    
    input_file=sys.argv[1]
    output_file=input_file[0:-3]+'png'
    data=pd.read_csv(input_file)
    
    labels=list(data)    
    plt.plot(data[labels[0]], data[labels[1]], label="my laptop")
    plt.xlabel(labels[0])
    plt.ylabel(labels[1])   
    plt.savefig(output_file)
    plt.close()
    

    生成/运行/创建报告:

    >>> g++ -O3 -std=c++11 bandwidth.cpp -o bandwidth
    >>> ./bandwidth > report.txt
    >>> python create_report.py report.txt
    # image is in report.png
    
    2 回复  |  直到 7 年前
        1
  •  18
  •   user10605163 user10605163    7 年前

    我稍微更改了值: NITER = 100000 NTRIES=1 以获得噪音更小的结果。

    我现在没有Broadwell,但是我在我的咖啡湖上试用了你的代码,性能下降了,不是2KB,而是4.5KB左右。此外,我还发现吞吐量在略高于2KB时表现出不稳定的行为。

    这里的红线是 perf stat -e branch-instructions,branch-misses ,给出未正确预测的分支的分数(百分比,右轴)。正如你所看到的,两者之间存在着明显的反相关性。

    研究更详细的 perf Worker::operator() . 如果环路分支的执行/未执行模式变得太长,分支预测器将无法跟踪它,因此内环的退出分支将被预测失误,导致吞吐量急剧下降。随着迭代次数的进一步增加,这种单一预测失误的影响将变得不那么显著,从而导致吞吐量的缓慢恢复。

    有关跌落前不稳定行为的更多信息,请参见下面@PeterCordes的评论。

    Worker::operator() ,例如:

    void operator()(){
        for(size_t i=0;i+3*BLOCK_SIZE<n;i+=BLOCK_SIZE*4){
             result+=mem[i];
             result+=mem[i+BLOCK_SIZE];
             result+=mem[i+2*BLOCK_SIZE];
             result+=mem[i+3*BLOCK_SIZE];
        }
    }
    

    展开2、3、4、6或8次迭代将得到以下结果。请注意,我没有更正向量末尾的块,这些块由于展开而被忽略。因此,应忽略蓝线中的周期性峰值,周期图案的下限基线是实际带宽。

    enter image description here enter image description here enter image description here enter image description here enter image description here

    正如您所看到的,分支预测失误的比例并没有真正改变,但由于分支总数因展开迭代次数的因素而减少,因此它们对性能的贡献将不再很大。

    如果循环展开,处理器可以更自由地按顺序进行计算,这还有另外一个好处。

    如果这是有实际应用的话,我建议尝试给热循环一个编译时固定的迭代次数或一些整除性的保证,这样(可能有一些额外的提示)编译器就可以决定展开的最佳迭代次数。

        2
  •  2
  •   Mario    7 年前

    可能与此无关,但您的Linux机器可能会使用CPU频率。我知道Ubuntu18有一个gouverner,它在功能和性能之间保持平衡。您还需要处理流程关联,以确保它在运行时不会迁移到不同的核心。