Cpu Cache 简单介绍

Posted by w@hidva.com on April 8, 2016

嗯,…,真的是很简单的介绍咯 @_@

CPU Cache 为何引入

  • 一张比较直观的时间图:

    • execute typical instruction; 1 ns;
    • L1 cache reference,从 L1 cache 中取数据; 0.5 ns
    • L2 cache reference,从 L2 cache 中取数据; 7 ns
    • Main memory reference,从主存中取数据; 100 ns;
  • 所以有必要引入 CPU Cache

CPU Cache 实现

这里真是很简单的一点点内容 @_@

  • CPU Cache 模型;参考 MESI protocol.
  • CPU Cache 如何工作;

    • 对于 CPU 而言,数据并没有类型,有无符号这些概念;对于 CPU 而言,这些都是一块一块的数据.
    • 在 CPU 需要读取主存上的数据时,其会把该数据周围数据(一般是 Cache line 大小,64 byte)先 一同加载到 CPU Cache 中.这个应该是根据程序的局部性原理决定的.
  • Cache associativity;缓存关联性,指定了 CPU Cache 与 Memory 之间如何关联,如何映射.

  • Cache 一致性;参见MESI protocol协议.

  • 原子操作与 CPU Cache;原子操作仅能保证操作具有原子性,即不会被其他 CPU 看到操作的中间状态, 在某个 CPU 执行原子操作之后其还应该按照 MESI 协议规范执行对应的内存屏障指令才能确保原子操 作的副作用被其他 CPU 可见.

    • 按照我的理解,没有必要了解的太详细,只需要按照语言标准提供的内存模型来编程即可.

CPU Cache 友好的习惯

  • 其实,我觉得这些亲近硬件的东西应该是编译器的锅,她应该根据源码行为以及目标平台然后生成缓存友好的 代码.

避免 false sharing

  • false sharing 是啥? 先看下面一种情景:

    1. 内存中的数据是以 cache line 为单位从内存中读到 CPU cache 中的,比如有两个变量 X,Y;在 内存中他们俩非常近,那么很有可能在读 X 的时候,Y 也被放到了相同 cache line 中.
    2. Thread1 在一个循环中不停的写 X;Thread2 不停的写 Y;那么在 Thread1 写 X 的时候,需要 Invalid 其他 cache 中对应的 cache line,Thread2 写 Y 的时候也要做同样的事情.

    这样就会碰到 MESI 中 2 个耗时的操作:

    • 写入到处于 Invalid 状态的 cache line.
    • 将一个 cache line 的状态更改为 Invalid.
  • 如何解决 false sharing;很简单,增大 X,Y 之间的距离;确保 X,Y 不会被加载到同一 Cache line 中;在 Facebook/folly 项目中, X,Y 的距离是 128 byte!

演示 false sharing

void thread_main(unsigned char *addr,const char *thread_name)
{
    auto time_use = ExecuteTimeGet(7,300000000U,[&]{
        unsigned char var = *addr;
        ++var;
        *addr = var;
    });
    PP_QQ_LOG_D("thread: %s;time_use: %lu",thread_name,time_use);// 单位:纳秒
}

/* 若定义了 ENABLE_FALSE_SHARING,此时 2 个线程之间会 false sharing;
 * 否则 2 个线程之间不会 false sharing.
 */
#if defined(ENABLE_FALSE_SHARING)
unsigned char data[2];
#else
unsigned char data[256];
#endif

int
main(int ,char **)
{
    std::thread thread1 {thread_main,&data[0],"thread1"};
    std::thread thread2 {thread_main,&data[sizeof(data) / sizeof(unsigned char) - 1],"thread2"};
    thread1.join();
    thread2.join();
    return 0;
}
  • 很显然,这里只是代码片段,完整见;

  • 当开启了 ENABLE_FALSE_SHARING 时的运行结果.

    $ ./bin/false_sharing_test
    thread: thread2;time_use: 4160599200
    thread: thread1;time_use: 3372905584
    $ ./bin/false_sharing_test
    thread: thread2;time_use: 4162006784
    thread: thread1;time_use: 3354563091
    $ ./bin/false_sharing_test
    thread: thread1;time_use: 3746164897
    thread: thread2;time_use: 2814324090
    $ ./bin/false_sharing_test
    thread: thread2;time_use: 4013732999
    thread: thread1;time_use: 3944717713
    
  • 当不开启 ENABLE_FALSE_SHARING 时运行结果

    $ ./bin/false_sharing_test
    thread: thread2;time_use: 1364076924
    thread: thread1;time_use: 1375531495
    $ ./bin/false_sharing_test
    thread: thread2;time_use: 1362867454
    thread: thread1;time_use: 1373039588
    $ ./bin/false_sharing_test
    thread: thread2;time_use: 1363529034
    thread: thread1;time_use: 1374240938
    $ ./bin/false_sharing_test
    thread: thread2;time_use: 1365391755
    thread: thread1;time_use: 1369760771
    

    可以看出,由于此时没有了 false sharing,所以运行时间还是比较大的.

参考

转载请注明出处!谢谢