Missmiaom
4/21/2020 - 7:08 AM

CPU Cache Line

CPU Cache

L1,L2和L3缓存之间的性能差异

  • L1缓存访问延迟:4个周期
  • L2缓存访问延迟:11个周期
  • L3缓存访问延迟:39个周期
  • 主存储器访问延迟:107个周期

CPU Cache Line

Cache Line 是 Cache 和主存储器之间的数据传输单位。

通常,Cache Line 为 64 字节。当读取或写入64字节区域中的任何位置时,处理器将读取或写入整个缓存行。处理器还尝试通过分析线程的内存访问模式来预取高速缓存行。

CPU Cache 优化方法

1. 优先选择按行访问数组

  • 按行读取二维数组时,能够有效利用 Cache Line 载入带来的速度优势。
  • 按列读取二维数组时,则会因为缓存未命中,导致不停加载新的缓存行,访问速度急剧下降。

2. 优先选择按值存储的数组

  • 按值存储的数组相邻单位可以被同时载入到一个缓存行
  • 按指针存储的数组相邻单位在内存中相距很远,可能导致频繁的缓存未命中

3. 将标志位和数据分别放入不同的数组中

如果对象中包含标志位来判断该数据段是否有效:

  • 如果将标志位和数据保存在一个数组中,当有效数据较少时,将因为缓存行的原因导致载入较多的无效数据。
  • 如果将标志位和数据保存在不同数组中,当有效数据较少时,可以避免上述情况。

4. 使用缓冲行填充避免伪共享

如果一个缓存行大小内的数据被多个CPU Core读取,为了一致性,需要额外的手段保证 Cache 上的数据被刷新(volatile),但通常这是一个非常昂贵的操作,会显著的降低内存带宽。

使用缓冲行填充,可以避免一个缓冲行大小内的数据被多个CPU Core读取,能够避免上述问题。

5. 代码缓存

  • 短循环代码适合 L1 或 L2 缓存,由于不浪费CPU周期来重复从主存储器中提取相同的指令,因此可以加快程序执行速度。
  • 在循环中尽量使用内联函数,编译器会将不必要在循环中的代码(在循环中没有变化)放到循环外

Ref

  1. Why software developers should care about CPU caches
  2. False Sharing
  3. 深入分析volatile实现原理
  4. 伪共享分析以及volatile与缓存行填充的应用
  5. What every programmer should know about memory

伪共享性能测试

代码

#include <iostream>
#include <chrono>
#include <thread>
#include <vector>
#include <atomic>

enum 
{
    ITERATIONS = 500 * 1000 * 1000
};

struct Long
{
#ifdef PADDING
    int64_t p1, p2, p3, p4, p5, p6, p7;
    std::atomic<int64_t> value;
    //volatile int64_t value;
    int64_t p8, p9, p10, p11, p12, p13, p14;
#else
    std::atomic<int64_t> value;
    //volatile int64_t value;
#endif
};

Long* longs;

void run(size_t index)
{
    for (int i = 0; i < ITERATIONS; ++i)
    {
        longs[index].value++;
    }
}

int main(int argc, char** args)
{
    size_t threadNum = args[1][0] - '0';

    longs = new Long[threadNum];

    std::vector<std::thread*> trs;

    auto start = std::chrono::high_resolution_clock::now();
    for (int i = 0; i < threadNum; ++i)
    {
        std::thread* tr = new std::thread(run, i);
        trs.push_back(tr);
    }
    for (int i = 0; i < threadNum; ++i)
    {
        trs[i]->join();
        delete trs[i];
    }
    auto end = std::chrono::high_resolution_clock::now();

    auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start).count();
    std::cout << "thread number: " << threadNum << " duration: " << duration << " us" << std::endl;
}

结果分析

使用 atomic

伪共享造成的性能损失极大

使用 volatile

因为 volatile 只是进行了内存写入,避免 Cache 在其他线程中不可见,并没有复杂的同步保证,所以测试结果差距不大