Redian新闻
>
改了一行代码,数组遍历耗时从10.3秒降到了0.5秒!

改了一行代码,数组遍历耗时从10.3秒降到了0.5秒!

公众号新闻

本专题除讲解常见性能问题以及分析、调优手段外,还会重点讲解一些对系统性能至关重要,却又经常被忽视的高级话题,如Cache、指令流水线、Superscalar、SIMD、分支预测、内存屏障等。此外,还会涉及到编译器、操作系统等话题。

两个简单的测试程序

定义一个同样大小的二维数组,然后循环遍历,对数组元素赋值。

  • • array1.c 对数组按行进行访问

  • • array2.c 对数组按列进行访问

编译运行,并用time命令统计一下运行时间:

array1用时0.528秒,array2用时10.310秒。array2耗时居然是array1的将近20倍!

有没有被这个结果震惊到?为什么会有如此之大的性能差异呢?

重要说明

要想真正理解这个问题,必须要先补充一些关于现代计算机存储系统相关的背景知识,这也是理解这个问题的关键所在。为方便大家理解,我会尽量以白话的形式进行讲解,尽可能避免枯燥无味的纯理论描述。

存储金字塔

相信不少人都听过“存储金字塔”这个词,或者至少见过类似下面这张图:

这张图很直观地描述了现代计算机系统的分级存储模型。

可以认为CPU就位于金字塔的顶点上,越靠近塔顶,离CPU越近,访问速度越快,但生产成本越高,相应的容量也就越小。在所有存储器中,寄存器直接内嵌在CPU中,访问速度最快,容量也最小,一般CPU上也就最多几十个通用寄存器供应用程序使用。

反之,越往下靠近塔底,访问速度越慢,生产成本越低,相应的容量也就越大。比如图中最底部的网络存储设备,相对其他存储设备而言是访问速度最慢的,但其容量却几乎可以认为是无限制的。

那么,这种金字塔式结构中,不同层级的存储设备之间究竟是如何协调工作的呢?

用一句话概括:高一级的存储设备,往往是作为低一级存储设备的缓存来使用的。

简单来说,系统运行时,为了提升数据访问效率,把程序中最近最经常访问的数据,尽可能放到访问速度更快的高一级存储器中。这样一来,每次访问数据时,从金字塔的顶端开始,都先尝试在高一级存储器中查找,如果被访问的数据存在且有效,则直接访问,否则,就逐级到更低级的存储器中去查找。

这种金字塔式的分级存储模型之所以能够以近乎完美的方式运行,实际上都是建立在现代计算机科学中的一个非常重要的理论基础之上:程序的局部性原理

局部性原理

一个程序的局部性,包含两个维度:时间局部性和空间局部性。

  • • 时间局部性。如果一个数据在某个时间点被CPU访问了,那么在接下来很短的一段时间内,这个数据很有可能会再次被CPU访问到。

  • • 空间局部性。如果一个数据在某个时间点被CPU访问了,那么与这个数据临近的其他数据,很有可能也会很快被CPU访问到。

高速缓存 - Cache

根据常识,我们知道,程序要想被CPU正常执行,必须要先被加载到内存中。(当然也有例外,有童鞋知道哪些程序不是在内存中运行的吗?欢迎添加作者微信CreCoding讨论!)

但是,内存的访问速度与CPU运行速度相比,要慢好几个数量级,可以想象一下,如果CPU每次都直接从内存中存取数据,会造成大量的计算资源浪费,程序性能严重受损,假如真是这样的话,你还能像现在这样愉快的吃鸡吗?

为了解决CPU和内存之间速度严重不匹配的问题,在CPU和内存之间设计了高速缓存,即Cache。

目前,主流CPU一般都有三级(或更多级)的Cache,依次是L1 Cache、L2 Cache、L3 Cache。其中L1 Cache速度最快,几乎可以和内嵌在CPU中的寄存器媲美,容量也最小,而L3 Cache速度最慢(但仍然比内存快很多),但容量最大。

CPU读取数据时,会在L1、L2、L3Cache中逐级查找,如果找到,就从Cache直接读取,找不到再从内存读取,并且把数据存放到Cache中,以便提高下次访问的效率。

在这个过程中,如果在Cache中找到所需数据,称为Cache命中(Cache Hit), 找不到称为Cache未命中(Cache Miss)

不难看出,L1 Cache命中的时候,读取数据最快,性能最好,而当L1、L2、L3 Cache全部未命中时,就必须要直接从内存中读取数据,性能最差!

Cache Line

Cache Line 是 Cache和内存之间进行数据传输的最小单位。

根据上文讲解的程序的局部性原理,如果一个数据被CPU访问了,那么这个数据相邻的其他数据也很快会被访问到。因此,为了提高内存数据的读取效率,并且最大化利用CPU资源,数据在Cache和内存之间传输时,不是一个字节一个字节进行传输的,而是以缓存行(Cache Line)为单位进行传输的。

不同CPU的Cache line大小可能不同,典型的CPU Cache line大小是64个字节。

我们通过下面一个简单的例子,加深一下理解。

Cache Line 实例讲解

在一个Cache Line大小为64字节的机器上,定义个数组:

int a[16] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16};

我们假设数组a的起始地址是Cache Line对齐的,可以简单理解为a的地址能被64整除。假设,数组a还从来没有被访问过,如果此时需要访问a中的一个元素a[5],如:

int x = a[5];

由于在此之前数组a没有被访问过,所以理论上讲,数组a应该只存在于内存中,并未被加载到Cache中。因此,此时CPU在Cache中找不到a[5],触发Cache Miss,然后需要从内存中读取数据,并更加载到Cache中。前面提到,Cache和内存之间是以Cache Line为单位进行数据传输的,因此,这里会把一个Cache line大小(64字节)的数据从内存读取出来加载到Cache中。由于a的起始地址恰巧是Cache line对齐的,所以CPU会把整个数组(64个字节,刚好一个Cache Line)都加载到Cache中。

紧接着,如果再访问数组a的元素,如:

int y = a[10];

此时,整个数组都在Cache中,所以CPU在访问时,触发Cache Hit,直接从Cache读取数据即可,不需要再从内存中读取。

了解了Cache的背景知识,现在来分析下array1.c和array2.c为什么会存在这么巨大的性能差异。

按行、列访问的真正差异 - Cache

首先,必须要知道一点:C语言的数组,所有元素是存放在地址连续的内存中的,此外,C语言的多维数组,是按行进行存储的

array1.c按行对数组进行访问,也就是从数组起始地址开始,一直连续访问到数组的最后一个元素的地址处。第一次访问一个Cache Line的首个元素时,触发Cache Miss,与该元素临近的几个元素会组成一个Cache Line,被一起加载到Cache中。如此,在访问下一个元素的时候,就会Cache Hit,直接从Cache读取数据即可。

而array2.c按列对数组进行访问,因此并不是按照连续地址进行访问的,而是每次间隔10240 * 4个字节进行访问。第一次访问一个Cache Line的首个元素时,假设地址为x,尽管该元素临近的一个Cache Line大小的元素也会被一起加载进Cache中,但是程序接下来访问的并不是临近的元素,而是地址为x + 10240 * 4处的元素,因此会再次触发Cache Miss。而当程序回过头来访问x + 4地址处的元素时,这个Cache Line可能已经被其他数据冲刷掉了。因为,尽管Cache会尽量缓存最近访问过的数据,但毕竟大小有限,当Cache被占满时,一些旧的数据就会被冲刷替换掉。

可以看出,无论是时间局部性还是空间局部性,array1.c都要比array2.c好很多!相比array1.c,array2.c会触发大量的Cache Miss,这也是为什么array2的性能会如此之差!

下面我们用perf来对比下这两个程序的性能指标。

用perf观测性能指标

perf是Linux提供的一个功能强大的实用性能调优工具,它可以用来观测几乎所有CPU相关的性能指标,但perf工具本身,不是本文重点,以后会有专门文章详细介绍perf的使用。

先获取array1.c的性能指标:

再看下array2.c的性能指标:

这里,我们只关注一个最重要的性能指标:L1 Data Cache的Miss率。

对比一下,array1.c的L1-dcache-load-misses率只有3.10%, 而按列访问的array2.c,则达到了惊人的93.03%!这就是两者性能差异如此巨大的真相!

小结和下篇预告

Cache是CPU内部最为重要的部件之一,也是影响程序性能的最重要因素之一,但由于各种原因,经常被程序员忽视。在进行性能调优时,除了系统架构、算法等方面的考量之外,不妨也从CPU的角度去思考一下,比如Cache,有时会对程序性能产生决定性影响。

推荐阅读  点击标题可跳转

1、比Wget、Curl更强大的下载工具!

2、Linux中内存管理详解

3、简单地了解一些计算机基础知识

微信扫码关注该文公众号作者

戳这里提交新闻线索和高质量文章给我们。
相关阅读
两行代码解决大模型对话局限,港中文贾佳亚团队联合 MIT 发布超长文本扩展技术用一个月重构了同事写的烂代码,我总结了8条重写烂代码的经验!公司新来一个同事:为什么 HashMap 不能一边遍历一边删除?一下子把我问懵了!苹果、AMD和高通GPU被爆存在漏洞!只需十行代码即可窃取数据,数百万台苹果设备或将受到影响看Vivian 转的视频和为人父转的柴静《和圣战分子对话》视频Redis创始人开源最小聊天服务器,仅200行代码贾佳亚韩松团队新作:两行代码让大模型上下文窗口倍增 | GitHub热榜集中和分散的思考,如何看待遍历性?指针没用好,一行代码让公司损失6000万美元6014 血壮山河之随枣会战 南昌之战 7GPT-4惨遭削弱,偷懒摸鱼绝不多写一行代码,OpenAI已介入调查网传男子杀害妻子和丈母娘后,被警方抓捕时从14楼跳下当场身亡!细节曝光第十一章第二节 金融资本和货币政策网友600行代码做出翻版“愤怒的小鸟”,却没有一行是自己写的!一行代码提高大模型10%性能,开发者:免费午餐現在的網路世界有點冷清华为贡献 88.75%,开源鸿蒙 OpenHarmony 在 2023 年累计新增 1620 万行代码不到 600 行代码实现了《愤怒的小鸟》翻版,GPT-4+DALL·E 3+Midjourney 撼动游戏圈!约270万行代码!我国自主研发,今日发布夏婳:两情难相知(十三)一场67万行代码的应用重构不到1000行代码,PyTorch团队让Llama 7B提速10倍为啥余华成了00后的“知心小狗”?2行代码,「三体」一次读完!港中文贾佳亚团队联手MIT发布超长文本扩展技术,打破LLM遗忘魔咒倒计时,一晚地震1100次!居民排队出逃,随时从地图上抹去简单有效!Direct Inversion:三行代码提升基于扩散的图像编辑效果只改了五行代码接口吞吐量提升了 10 多倍字节家属:老公面临被优化,压力大准备卖房。从110多万降到82万,还没卖掉洛杉矶县核桃市一华人快递被盗,小偷作案过程不到5秒!两行代码解决大语言模型对话局限!港中文贾佳亚团队联合 MIT 发布超长文本扩展技术程前跟董宇辉之间,差了0.9999个董宇辉简单有效!Direct Inversion: 三行代码提升基于Diffusion的图像编辑效果AI读脑成真,延迟仅0.25秒!Meta里程碑新研究:MEG实时解码大脑图像,LeCun转赞由近20亿个可调参数组成,AI制药新锐Deep Genomics设计出首个用于RNA疗法开发的人工智能基础模型郭艺芳教授:低密度脂蛋白降到了1.2,咋办?降到了2.2,咋办?
logo
联系我们隐私协议©2024 redian.news
Redian新闻
Redian.news刊载任何文章,不代表同意其说法或描述,仅为提供更多信息,也不构成任何建议。文章信息的合法性及真实性由其作者负责,与Redian.news及其运营公司无关。欢迎投稿,如发现稿件侵权,或作者不愿在本网发表文章,请版权拥有者通知本网处理。