关于CPU Cache:程序猿需要知道的那些

天下没有免费的午餐,本文转载于:http://cenalulu.github.io/linux/all-about-cpu-cache/



先来看一张本文所有概念的一个思维导图:

为什么要有CPU Cache

随着工艺的提升最近几十年CPU的频率不断提升,而受制于制造工艺和成本限制,目前计算机的内存主要是DRAM并且在访问速度上没有质的突破。因 此,CPU的处理速度和内存的访问速度差距越来越大,甚至可以达到上万倍。这种情况下传统的CPU通过FSB直连内存的方式显然就会因为内存访问的等待, 导致计算资源大量闲置,降低CPU整体吞吐量。同时又由于内存数据访问的热点集中性,在CPU和内存之间用较为快速而成本较高的SDRAM做一层缓存,就 显得性价比极高了。

为什么要有多级CPU Cache

随着科技发展,热点数据的体积越来越大,单纯的增加一级缓存大小的性价比已经很低了。因此,就慢慢出现了在一级缓存(L1 Cache)和内存之间又增加一层访问速度和成本都介于两者之间的二级缓存(L2 Cache)。下面是一段从What Every Programmer Should Know About Memory中摘录的解释:

Soon after the introduction of the cache the system got more complicated. The speed difference between the cache and the main memory increased again, to a point that another level of cache was added, bigger and slower than the first-level cache. Only increasing the size of the first-level cache was not an option for economical rea- sons.

此外,又由于程序指令和程序数据的行为和热点分布差异很大,因此L1 Cache也被划分成L1i (i for instruction)和L1d (d for data)两种专门用途的缓存。
下面一张图可以看出各级缓存之间的响应时间差距,以及内存到底有多慢!

什么是Cache Line

Cache Line可以简单的理解为CPU Cache中的最小缓存单位。目前主流的CPU Cache的Cache Line大小都是64Bytes。假设我们有一个512字节的一级缓存,那么按照64B的缓存单位大小来算,这个一级缓存所能存放的缓存个数就是512/64 = 8个。具体参见下图:

为了更好的了解Cache Line,我们还可以在自己的电脑上做下面这个有趣的实验。

下面这段C代码,会从命令行接收一个参数作为数组的大小创建一个数量为N的int数组。并依次循环的从这个数组中进行数组内容访问,循环10亿次。最终输出数组总大小和对应总执行时间。

 1 #include "stdio.h"
 2 #include <stdlib.h>
 3 #include <sys/time.h>
 4
 5 long timediff(clock_t t1, clock_t t2) {
 6     long elapsed;
 7     elapsed = ((double)t2 - t1) / CLOCKS_PER_SEC * 1000;
 8     return elapsed;
 9 }
10
11 int main(int argc, char *argv[])
12 #*******
13 {
14
15     int array_size=atoi(argv[1]);
16     int repeat_times = 1000000000;
17     long array[array_size];
18     for(int i=0; i<array_size; i++){
19         array[i] = 0;
20     }
21     int j=0;
22     int k=0;
23     int c=0;
24     clock_t start=clock();
25     while(j++<repeat_times){
26         if(k==array_size){
27             k=0;
28         }
29         c = array[k++];
30     }
31     clock_t end =clock();
32     printf("%lu\n", timediff(start,end));
33     return 0;
34 }
 

如果我们把这些数据做成折线图后就会发现:总执行时间在数组大小超过64Bytes时有较为明显的拐点(当然,由于博主是在自己的Mac笔记本上测 试的,会受到很多其他程序的干扰,因此会有波动)。原因是当数组小于64Bytes时数组极有可能落在一条Cache Line内,而一个元素的访问就会使得整条Cache Line被填充,因而值得后面的若干个元素受益于缓存带来的加速。而当数组大于64Bytes时,必然至少需要两条Cache Line,继而在循环访问时会出现两次Cache Line的填充,由于缓存填充的时间远高于数据访问的响应时间,因此多一次缓存填充对于总执行的影响会被放大,最终得到下图的结果:

如果读者有兴趣的话也可以在自己的linux或者MAC上通过gcc cache_line_size.c -o cache_line_size编译,并通过./cache_line_size执行。

了解Cache Line的概念对我们程序猿有什么帮助?
我们来看下面这个C语言中常用的循环优化例子
下面两段代码中,第一段代码在C语言中总是比第二段代码的执行速度要快。具体的原因相信你仔细阅读了Cache Line的介绍后就很容易理解了。

for(int i = 0; i < n; i++) {
    for(int j = 0; j < n; j++) {
        int num;
        //code
        arr[i][j] = num;
    }
}

for(int i = 0; i < n; i++) {
    for(int j = 0; j < n; j++) {
        int num;
        //code
        arr[j][i] = num;
    }
}

CPU Cache 是如何存放数据的

你会怎么设计Cache的存放规则

我们先来尝试回答一下那么这个问题:

假设我们有一块4MB的区域用于缓存,每个缓存对象的唯一标识是它所在的物理内存地址。每个缓存对象大小是64Bytes,所有可以被缓存对象的大小总和(即物理内存总大小)为4GB。那么我们该如何设计这个缓存?

如果你和博主一样是一个大学没有好好学习基础/数字电路的人 的话,会觉得最靠谱的的一种方式就是:Hash表。把Cache设计成一个Hash数组。内存地址的Hash值作为数组的Index,缓存对象的值作为数 组的Value。每次存取时,都把地址做一次Hash然后找到Cache中对应的位置操作即可。
这样的设计方式在高等语言中很常见,也显然很高效。因为Hash值得计算虽然耗时(10000个CPU Cycle左右),
但是相比程序中其他操作(上百万的CPU Cycle)来说可以忽略不计。而对于CPU Cache来说,本来其设计目标就是在几十CPU
Cycle内获取到数据。如果访问效率是百万Cycle这个等级的话,还不如到Memory直接获取数据。当然,更重要的原因是在硬件上要实现
Memory Address Hash的功能在成本上是非常高的。

为什么Cache不能做成Fully Associative

Fully Associative 字面意思是全关联。在CPU
Cache中的含义是:如果在一个Cache集内,任何一个内存地址的数据可以被缓存在任何一个Cache
Line里,那么我们成这个cache是Fully
Associative。从定义中我们可以得出这样的结论:给到一个内存地址,要知道他是否存在于Cache中,需要遍历所有Cache
Line并比较缓存内容的内存地址。而Cache的本意就是为了在尽可能少得CPU Cycle内取到数据。那么想要设计一个快速的Fully
Associative的Cache几乎是不可能的。

为什么Cache不能做成Direct Mapped

和Fully Associative完全相反,使用Direct Mapped模式的Cache给定一个内存地址,就唯一确定了一条Cache
Line。设计复杂度低且速度快。那么为什么Cache不使用这种模式呢?让我们来想象这么一种情况:一个拥有1M L2
Cache的32位CPU,每条Cache Line的大小为64Bytes。那么整个L2Cache被划为了1M/64=16384条Cache Line。我们为每条Cache Line从0开始编上号。同时32位CPU所能管理的内存地址范围是2^32=4G,那么Direct Mapped模式下,内存也被划为4G/16384=256K
小份。也就是说每256K的内存地址共享一条Cache Line。但是,这种模式下每条Cache
Line的使用率如果要做到接近100%,就需要操作系统对于内存的分配和访问在地址上也是近乎平均的。而与我们的意愿相反,为了减少内存碎片和实现便
捷,操作系统更多的是连续集中的使用内存。这样会出现的情况就是0-1000号这样的低编号Cache
Line由于内存经常被分配并使用,而16000号以上的Cache
Line由于内存鲜有进程访问,几乎一直处于空闲状态。这种情况下,本来就宝贵的1M二级CPU缓存,使用率也许50%都无法达到。

什么是N-Way Set Associative

为了避免以上两种设计模式的缺陷,N-Way Set Associative缓存就出现了。他的原理是把一个缓存按照N个Cache
Line作为一组(set),缓存按组划为等分。这样一个64位系统的内存地址在4MB二级缓存中就划成了三个部分(见下图),低位6个bit表示在
Cache Line中的偏移量,中间12bit表示Cache组号(set
index),剩余的高位46bit就是内存地址的唯一id。这样的设计相较前两种设计有以下两点好处:

  • 给定一个内存地址可以唯一对应一个set,对于set中只需遍历16个元素就可以确定对象是否在缓存中(Full Associative中比较次数随内存大小线性增加)
  • 2^18(256K)*16(way)=4M的连续热点数据才会导致一个set内的conflict(Direct Mapped中512K的连续热点数据就会出现conflict)

为什么N-Way Set Associative的Set段是从低位而不是高位开始的

下面是一段从How Misaligning Data Can Increase Performance 12x by Reducing Cache Misses摘录的解释:

The vast majority of accesses are close together, so moving the set
index bits upwards would cause more conflict misses. You might be able
to get away with a hash function that isn’t simply the least significant
bits, but most proposed schemes hurt about as much as they help while
adding extra complexity.

由于内存的访问通常是大片连续的,或者是因为在同一程序中而导致地址接近的(即这些内存地址的高位都是一样的)。所以如果把内存地址的高位作为
set index的话,那么短时间的大量内存访问都会因为set index相同而落在同一个set index中,从而导致cache
conflicts使得L2, L3 Cache的命中率低下,影响程序的整体执行效率。

了解N-Way Set Associative的存储模式对我们有什么帮助

了解N-Way Set的概念后,我们不难得出以下结论:2^(6Bits <Cache Line Offset> + 12Bits <Set Index>) = 2^18 = 256K
即在连续的内存地址中每256K都会出现一个处于同一个Cache
Set中的缓存对象。也就是说这些对象都会争抢一个仅有16个空位的缓存池(16-Way
Set)。而如果我们在程序中又使用了所谓优化神器的“内存对齐”的时候,这种争抢就会越发增多。效率上的损失也会变得非常明显。具体的实际测试我们可以
参考: How Misaligning Data Can Increase Performance 12x by Reducing Cache Misses 一文。
这里我们引用一张Gallery of Processor Cache Effects 中的测试结果图,来解释下内存对齐在极端情况下带来的性能损失。

该图实际上是我们上文中第一个测试的一个变种。纵轴表示了测试对象数组的大小。横轴表示了每次数组元素访问之间的index间隔。而图中的颜色表示
了响应时间的长短,蓝色越明显的部分表示响应时间越长。从这个图我们可以得到很多结论。当然这里我们只对内存带来的性能损失感兴趣。有兴趣的读者也可以阅
原文分析理解其他从图中可以得到的结论。

从图中我们不难看出图中每1024个步进,即每1024*4即4096Bytes,都有一条特别明显的蓝色竖线。也就是
说,只要我们按照4K的步进去访问内存(内存根据4K对齐),无论热点数据多大它的实际效率都是非常低的!按照我们上文的分析,如果4KB的内存对齐,那
么一个240MB的数组就含有61440个可以被访问到的数组元素;而对于一个每256K就会有set冲突的16Way二级缓存,总共有256K/4K=64个元素要去争抢16个空位,总共有61440/64=960个这样的元素。那么缓存命中率只有1%,自然效率也就低了。

除了这个例子,有兴趣的读者还可以查阅另一篇国人对Page Align导致效率低的实验:http://evol128.is-programmer.com/posts/35453.html

想要知道更多关于内存地址对齐在目前的这种CPU-Cache的架构下会出现的问题可以详细阅读以下两篇文章:


Cache淘汰策略

在文章的最后我们顺带提一下CPU Cache的淘汰策略。常见的淘汰策略主要有LRURandom两种。通常意义下LRU对于Cache的命中率会比Random更好,所以CPU Cache的淘汰策略选择的是LRU。当然也有些实验显示在Cache Size较大的时候Random策略会有更高的命中率

总结

CPU Cache对于程序猿是透明的,所有的操作和策略都在CPU内部完成。但是,了解和理解CPU Cache的设计、工作原理有利于我们更好的利用CPU Cache,写出更多对CPU Cache友好的程序

Reference

  1. Gallery of Processor Cache Effects
  2. How Misaligning Data Can Increase Performance 12x by Reducing Cache Misses
  3. Introduction to Caches
时间: 2024-11-10 07:17:53

关于CPU Cache:程序猿需要知道的那些的相关文章

关于CPU Cache -- 程序猿需要知道的那些事

/ 本文将介绍一些作为程序猿或者IT从业者应该知道的CPU Cache相关的知识 文章欢迎转载,但转载时请保留本段文字,并置于文章的顶部 作者:卢钧轶(cenalulu) 本文原文地址:http://cenalulu.github.io/linux/all-about-cpu-cache/ 先来看一张本文所有概念的一个思维导图 为什么要有CPU Cache 随着工艺的提升最近几十年CPU的频率不断提升,而受制于制造工艺和成本限制,目前计算机的内存主要是DRAM并且在访问速度上没有质的突破.因此,

做程序猿的老婆应该注意的一些事情

今天来说说一位女青年的老公以及他们的事儿.如有雷同,纯属巧合. 十一年前我和程序猿第一次见面,还是大一军训期间.我甚至不确定程序猿是否记得那是第一次见面.当时不小心装伪文艺参加了吉他社,想借一本吉他入门书,然后同在吉他社热心的海哥说他一同学有,就带着我去拿书了.于是我们第一次"见面"了,我只看见一个对着电脑,佝偻着背的背影--严重怀疑程序猿根本没抬头看一眼那个已经晒得像反转熊猫一样的妹子,因为他递过来书以后,又迅速投入到了电脑的怀抱,相对应的,我也不记得他长什么样了. 我从来没有读懂过

程序猿趣闻

1.程序员找不女朋友的原因 程序员问禅师:“大师,我身体健康,思想端正,各方面都不错,为何就是找不到女朋友呢?” 禅师浅笑,答:“原因很简单,不过若想我告诉你,你需先写一段java代码.” 青年略一沉吟,写完了. “再写一段C#” 写完了, “再写一段php” 写完了, “再写一段js”,写完了, “这就是原因” . 2.技术宅男如何把妹? 1.自制系统盘内置马儿随时知道MM在做什么,计划任务定期搞崩系统从而保持用户粘性: 2.给MM推荐自制APP,随时知道mm位置给她惊喜: 3.经过一段时间你

一篇让Java程序猿随时可以翻看的Oracle总结

一篇让Java程序猿随时可以翻看的Oracle总结 前言:Oracle学习也有十几天了,但是呢,接下来还要学习许多其他的东西,并不能提步不前,所以在此总结了以下Oracle中常用的命令和语句,没有语法都是实例,以便以后工作的时候随时翻看,毕竟是自己的东西,一看就懂.  有关的语句和操作基本都是按照实战中的顺序来总结的,比如创建用户,建表,序列初始化,插入数据的顺序呢. 这篇文章的基表是大家最为熟知的Scott用户下的emp员工表,dept部门表以及salgrade薪水等级表,一切的语句都是围绕它

要嫁就嫁程序猿

一. 程序猿问科比:“你为什么这么成功? ”科比:“你知道洛杉矶凌晨四点是什么样子吗? ”程序猿:“知道,一般那个时候我还在写代码,怎么了?”科比:“额…….” 二. 女神:你能让这个论坛的人都吵起来,我今晚就跟你走.程序猿:PHP语言是最好的语言!论坛炸锅了,各种吵架.女神:服了你了,我们走吧,你想干啥都行.程序猿:今天不行,我一定要说服他们,PHP语言是最好的语言. 三. 我是一个苦b的程序员,今晚加班到快通宵了,困得快睁不开眼了,女上司很关心,问我要不要吃宵夜.我没好气地说,宵夜就算了,能

疯狂Java学习笔记(72)-----------大话程序猿面试

大话程序猿面试 10个我最喜欢问程序猿的面试问题 程序猿面试不全然指南 10个经典的C语言面试基础算法及代码 程序猿的10大成功面试技巧 程序猿选择公司的8个标准 编程开发 8个值得关注的PHP安全函数 简析TCP的三次握手与四次分手 10分钟掌握XML.JSON及其解析 高效的jQuery代码编写技巧总结 编译器的工作过程和原理 CPU空暇时在"忙"什么 5个强大的Java分布式缓存框架推荐 架构设计 趣味漫画:云计算的起源 负载均衡调度算法大全 程序人生 程序猿不不过写代码 201

程序猿媳妇儿注意事项

程序猿媳妇儿注意事项 作者: Arale  来源: 简书  发布时间: 2016-08-11 15:42  阅读: 40944 次  推荐: 225   原文链接   [收藏] 十一年前我和程序猿第一次见面,还是大一军训期间.我甚至不确定程序猿是否记得那是第一次见面.当时不小心装伪文艺参加了吉他社,想借一本吉他入门书,然后同在吉他社热心的海哥说他一同学有,就带着我去拿书了.于是我们第一次“见面”了,我只看见一个对着电脑,佝偻着背的背影......严重怀疑程序猿根本没抬头看一眼那个已经晒得像反转熊

OSChina 娱乐弹弹弹——要嫁就嫁程序猿

程序猿问科比:"你为什么这么成功? "科比:"你知道洛杉矶凌晨四点是什么样子吗? "程序猿:"知道,一般那个时候我还在写代码,怎么了?" 科比:"额......." 女神:你能让这个论坛的人都吵起来,我今晚就跟你走. 程序猿:PHP语言是最好的语言! 论坛炸锅了,各种吵架. 女神:服了你了,我们走吧,你想干啥都行. 程序猿:今天不行,我一定要说服他们,PHP语言是最好的语言. 我是一个苦b的程序员,今晚加班到快通宵了,困得快睁

要嫁就嫁程序猿——钱多话少死的早!

1.程序猿问科比:“你为什么这么成功? ” 科比:“你知道洛杉矶凌晨四点是什么样子吗? ” 程序猿:“知道,一般那个时候我还在写代码,怎么了?” 科比:“额…….” 2.女神:你能让这个论坛的人都吵起来,我今晚就跟你走. 程序猿:PHP语言是最好的语言! 论坛炸锅了,各种吵架. 女神:服了你了,我们走吧,你想干啥都行. 程序猿:今天不行,我一定要说服他们,PHP语言是最好的语言. 3.我是一个苦b的程序员,今晚加班到快通宵了,困得快睁不开眼了,女上司很关心,问我要不要吃宵夜.我没好气地说,宵夜就