优化代码性能:利用CPU缓存原理
在计算机的世界里,有一场如同龟兔赛跑般的速度较量,主角便是 CPU 和内存 。龟兔赛跑的故事大家都耳熟能详,兔子速度飞快,乌龟则慢吞吞的。在计算机中,CPU 就如同那敏捷的兔子,拥有超高的运算速度,能够在极短的时间内完成大量复杂的计算任务;而内存则像是那步伐缓慢的乌龟,虽然它能够存储程序运行所需的数据和指令,但数据的读取和写入速度,与 CPU 相比,简直是天壤之别。
现代 CPU 的运行频率可以轻松达到数 GHz,这意味着它每秒能够执行数十亿次的操作。而内存的访问速度,虽然也在不断提升,但与 CPU 的速度相比,仍然存在着巨大的差距。这种速度上的不匹配,就好比是让一个短跑冠军和一个普通人进行接力赛跑,每次短跑冠军快速跑到终点后,都需要花费大量时间等待普通人慢悠悠地把接力棒送过来,这无疑会极大地影响整个系统的运行效率。
为了解决这个问题,计算机科学家们引入了一种名为 CPU Cache 的高速缓冲存储器,它就像是在 CPU 和内存之间搭建了一座 “高速桥梁”,让 CPU 能够更快地获取数据,从而提高计算机的整体性能 。接下来,就让我们一起深入了解一下 CPU Cache 的奥秘吧。
一、CPU Cache概述
你可能会好奇为什么有了内存,还需要 CPU Cache?根据摩尔定律,CPU 的访问速度每 18 个月就会翻倍,相当于每年增长 60% 左右,内存的速度当然也会不断增长,但是增长的速度远小于 CPU,平均每年只增长 7% 左右。于是,CPU 与内存的访问性能的差距不断拉大。
到现在,一次内存访问所需时间是 200~300 多个时钟周期,这意味着 CPU 和内存的访问速度已经相差 200~300 多倍了。为了弥补CPU和内存之间的性能差异,以便于能够真实变得把CPU的性能提升利用起来,而不是让它在那里空转,我们在现代CPU中引入了高速缓存。
从CPU Cache被加入到现有的CPU里开始,内存中的指令、数据,会被加载到L1-L3 Cache中,而不是直接从CPU访问内存中取拿。CPU从内存读取数据到CPU Cache的过程中,是一小块一小块来读取数据的,而不是按照单个数组元素来读取数据的。这样一小块一小块的数据,在CPU Cache里面,叫做Cache Line(缓存块)
在我们日常使用的 Intel 服务器或者 PC 里,Cache Line 的大小通常是 64 字节。
1.1Cache 的定义与角色
CPU Cache,即高速缓冲存储器,是位于 CPU 和内存之间的临时存储器 。它就像是一个数据 “中转站”,主要作用是为了加速 CPU 读取数据的速度 。由于 CPU 的运算速度极快,而内存的读写速度相对较慢,这就好比一个短跑冠军和一个慢跑者,两者的速度差距明显。
如果 CPU 直接从内存中读取数据,就需要花费大量时间等待,这无疑会降低整个计算机系统的运行效率 。而 CPU Cache 的出现,就很好地解决了这个问题。它的速度比内存快很多,能够提前将 CPU 可能需要的数据从内存中读取出来并存储起来,当 CPU 需要数据时,首先会在 Cache 中查找,如果找到了,就可以直接从 Cache 中快速获取,大大节省了读取数据的时间,提高了 CPU 的工作效率 。
1.2Cache 的层级结构
随着科技发展,热点数据的体积越来越大,单纯的增加一级缓存大小的性价比已经很低了;二级缓存就是一级缓存的缓冲器:一级缓存制造成本很高因此它的容量有限,二级缓存的作用就是存储那些CPU处理时需要用到、一级缓存又无法存储的数据。
CPU Cache 通常分为三级,分别是 L1 Cache(一级缓存)、L2 Cache(二级缓存)和 L3 Cache(三级缓存) 。这三级缓存就像是一个金字塔结构,从 L1 到 L3,速度逐渐变慢,容量逐渐增大 。
L1 Cache 是离 CPU 核心最近的缓存,它的速度最快,几乎可以与 CPU 的频率同步运行 。L1 Cache 又可以细分为数据缓存(L1 D - Cache)和指令缓存(L1 I - Cache),分别用于存储数据和指令 。
数据缓存就像是一个小型的数据仓库,专门存放 CPU 在运算过程中需要处理的数据;指令缓存则像是一个指令库,存储着 CPU 执行运算时所需要的指令 。由于 L1 Cache 与CPU 核心紧密相连,访问速度极快,但其容量相对较小,一般只有几十 KB 到几百 KB 。例如,英特尔酷睿 i7 - 13700K 处理器,其每个核心的 L1 数据缓存为 32KB,L1指令缓存也为 32KB 。
L2 Cache 位于 L1 Cache 之后,速度比 L1 Cache 稍慢,但容量比 L1 Cache 大 。它的作用是作为 L1 Cache的补充,当 L1 Cache 中没有找到 CPU 需要的数据或指令时,CPU 就会到L2 Cache 中查找 。L2 Cache 的容量一般在几百 KB 到几 MB 之间 。以刚才提到的酷睿i7 - 13700K 为例,其每个核心的L2缓存为 1MB 。
L3 Cache 是三级缓存中速度最慢但容量最大的缓存,它通常是多个 CPU 核心共享的 。当 L1 和 L2 Cache 都没有命中时,CPU 会访问 L3 Cache 。L3 Cache 的存在可以进一步提高数据的命中率,减少 CPU 访问内存的次数 。它的容量一般在几 MB 到几十 MB 之间 。酷睿 i7 - 13700K 的 L3 缓存为 30MB 。
在实际工作中,CPU 按照 L1 Cache、L2 Cache、L3 Cache 的顺序依次查找数据 。如果在某一级缓存中找到了所需的数据,就称为缓存命中;如果在三级缓存中都没有找到,才会去内存中查找,这就是缓存未命中 。缓存命中的概率越高,CPU 获取数据的速度就越快,计算机的性能也就越好 。
二、CPU Cache核心原理
2.1局部性原理
CPU Cache 能够提高数据访问性能,背后依赖的是局部性原理 。局部性原理又分为时间局部性和空间局部性 。
时间局部性,是指如果一个数据项在某个时刻被访问,那么在不久的将来它很可能再次被访问 。就好比我们看电视剧时,喜欢反复观看精彩的片段。在程序中,循环结构就是时间局部性的典型体现 。例如下面这段简单的 C 语言代码:
int sum = 0;
int arr[100] = {1, 2, 3,..., 100};
for (int i = 0; i < 100; i++) {sum += arr[i];
}
在这个循环中,变量sum会被多次访问,每次循环都要读取和更新它的值 。根据时间局部性原理,CPU Cache 会将sum的值缓存起来,这样在后续的循环中,CPU 就可以直接从 Cache 中快速获取sum的值,而不需要每次都从内存中读取 ,大大提高了访问效率 。
空间局部性,是指如果一个数据项被访问,那么与其相邻的数据项很可能在不久的将来也被访问 。这就像我们在书架上找书,找到一本后,往往会顺便看看它旁边的书 。在计算机中,内存中的数据通常是按顺序存储的 。比如数组,数组元素在内存中是连续存放的 。当 CPU 访问数组中的一个元素时,根据空间局部性原理,它很可能在接下来的操作中访问该元素附近的其他元素 。
还是以上面的代码为例,当 CPU 访问arr[i]时,Cache 会把arr[i]以及它附近的一些元素(比如arr[i - 1]、arr[i + 1]等,具体取决于 Cache Line 的大小,后面会详细介绍 Cache Line)一起缓存起来 。这样,当 CPU 访问下一个元素arr[i + 1]时,就有可能直接从 Cache 中获取,而无需再次访问内存 。
CPU Cache 正是利用了这两种局部性原理,提前将可能被访问的数据从内存中读取到 Cache 中 。当 CPU 需要数据时,首先在 Cache 中查找,如果命中(即找到所需数据),就可以快速获取数据,大大缩短了数据访问时间 。如果未命中,才会去内存中读取数据,并将读取到的数据及其相邻的数据块存入 Cache,以便后续访问 。通过这种方式,CPU Cache 有效地提高了数据访问性能,减少了 CPU 等待数据的时间,从而提升了整个计算机系统的运行效率 。
2.2缓存命中与未命中
在 CPU 访问数据的过程中,缓存命中和未命中是两个非常关键的概念,它们对 CPU 的数据读取流程有着重要的影响 。
当 CPU 需要读取某个数据时,它会首先在 Cache 中查找该数据 。如果该数据正好存在于 Cache 中,这就称为缓存命中 。缓存命中是一种非常理想的情况,因为 Cache 的速度比内存快很多,CPU 可以直接从 Cache 中快速获取数据,几乎不需要等待 。这就好比你在自己的书架上找一本书,一下子就找到了,马上就可以开始阅读 。
在缓存命中的情况下,CPU 的数据读取流程非常简单高效,直接从 Cache 中读取数据并进行后续的运算操作,大大提高了 CPU 的工作效率 。例如,在一个频繁访问数组元素的程序中,如果数组元素被缓存到 Cache 中,当 CPU 再次访问这些元素时,就会发生缓存命中,CPU 能够迅速获取数据,使得程序能够快速运行 。
然而,如果 CPU 在 Cache 中没有找到所需的数据,这就是缓存未命中 。缓存未命中的情况相对复杂,对 CPU 的性能影响也较大 。当缓存未命中时,CPU 不得不从速度较慢的内存中读取数据 。这就像你在自己的书架上没找到想要的书,只能去图书馆的书库中寻找,这无疑会花费更多的时间 。
在从内存读取数据的过程中,CPU 需要等待内存返回数据,这个等待时间可能会包含多个时钟周期 。而且,在从内存读取数据的同时,为了提高后续数据访问的命中率,CPU 会将读取到的数据以及该数据周围的一部分数据(以 Cache Line 为单位,后面会详细介绍)存入 Cache 中 。
如果此时 Cache 已满,还需要采用一定的替换算法(如最近最少使用 LRU 算法等),将 Cache 中不太常用的数据替换出去,为新的数据腾出空间 。例如,在一个处理大数据集的程序中,如果数据量超过了 Cache 的容量,就很容易出现缓存未命中的情况 。每次缓存未命中都需要 CPU 从内存读取数据,这会导致程序的运行速度明显下降,因为内存访问的延迟相对较高 。
缓存命中率是衡量 Cache 性能的重要指标,它表示缓存命中次数在总访问次数中所占的比例 。缓存命中率越高,说明 CPU 从 Cache 中获取数据的次数越多,也就意味着 CPU 等待数据的时间越短,计算机系统的性能也就越好 。因此,在计算机系统设计和程序优化中,提高缓存命中率是一个重要的目标 。通过合理地利用局部性原理、优化数据访问模式以及选择合适的 Cache 大小和替换算法等方法,可以有效地提高缓存命中率,减少缓存未命中的次数,从而提升计算机系统的整体性能 。
3.2Cache Line
Cache Line 是 CPU Cache 中的一个重要概念,它是 CPU 与内存之间进行数据传输的最小单位 。简单来说,当 CPU 需要从内存读取数据到 Cache 时,并不是以单个字节为单位进行读取,而是一次性读取一个固定大小的数据块,这个数据块就是一个 Cache Line 。
Cache Line 的大小通常是固定的,常见的 Cache Line 大小有 32 字节、64 字节等 。不同的 CPU 架构可能会有不同的 Cache Line 大小 。例如,在许多现代 x86 架构的 CPU 中,Cache Line 的大小一般为 64 字节 。Cache Line 的存在主要是为了利用空间局部性原理,提高数据读取的效率 。当 CPU 访问某个内存地址时,虽然它只需要该地址处的一个数据,但由于空间局部性,与该地址相邻的数据很可能也会被访问 。因此,将该地址所在的一整段数据(即一个 Cache Line)都读取到 Cache 中,可以减少后续数据访问时的缓存未命中次数 。
举个例子,假设有一个包含 100 个整数的数组,每个整数占 4 字节,数组在内存中是连续存储的 。如果 Cache Line 大小为 64 字节,那么一个 Cache Line 可以容纳 16 个整数(64 字节 / 4 字节 = 16) 。当 CPU 访问数组中的第 1 个元素时,内存会将包含第 1 个元素以及其后面 15 个元素的这 64 字节数据作为一个 Cache Line 读取到 Cache 中 。这样,当 CPU 接下来访问数组中的第 2 个到第 16 个元素时,就可以直接从 Cache 中获取,而不需要再次访问内存,大大提高了数据访问的效率 。
Cache Line 的大小对程序性能有着重要的影响 。如果 Cache Line 过小,虽然每次读取的数据量少,读取速度可能会快一些,但由于不能充分利用空间局部性,可能会导致缓存未命中次数增加;如果 Cache Line 过大,虽然能更好地利用空间局部性,减少缓存未命中次数,但每次读取的数据量过多,可能会导致 Cache 的利用率降低,而且读取时间也可能会变长 。因此,在设计 CPU Cache 时,需要综合考虑各种因素,选择合适的 Cache Line 大小,以达到最佳的性能 。
在编写程序时,了解 Cache Line 的概念也有助于优化程序性能 。例如,在处理数组时,可以通过合理地组织数据结构和访问顺序,使得数据访问能够更好地利用 Cache Line,提高缓存命中率 。比如,按行访问二维数组通常比按列访问更能利用 Cache Line,因为二维数组在内存中是按行存储的,按行访问时相邻元素更有可能在同一个 Cache Line 中 。
三、CPU Cache的数据写入策略
当 CPU 对 Cache 进行写操作时,为了确保 Cache 和内存之间的数据一致性以及提高系统性能,会采用不同的数据写入策略,其中最常见的是直写(Write Through)和写回(Write Back)策略 。
3.1直写(Write Through)
直写,也被称为写直通或写穿透 。其操作逻辑非常直观:当 CPU 要写入数据时,数据会同时被写入 Cache 和内存 。也就是说,只要有写操作发生,Cache 和内存中的数据都会立即被更新 。这就好比你在两个笔记本上同时记录同一件事情,一个笔记本是 Cache,另一个笔记本是内存 。
直写策略的优点是简单易懂,并且能够始终保证 Cache 和内存中的数据一致性 。因为每次写操作都同步更新了内存,所以在任何时刻,内存中的数据都是最新的 。这种一致性在一些对数据一致性要求极高的场景中非常重要,比如数据库系统中的关键数据存储 。在数据库事务处理中,需要确保数据的持久性和一致性,直写策略可以保证每次数据修改都能及时保存到内存中,避免了数据丢失或不一致的问题 。
然而,直写策略也存在明显的缺点 。由于每次写操作都要访问内存,而内存的访问速度相对较慢,这就导致了写操作的性能较低 。每次写操作都需要等待内存写入完成,这会增加 CPU 的等待时间,降低了系统的整体效率 。而且,频繁地访问内存还会增加总线的负载,因为每次写操作都需要通过总线与内存进行数据传输,可能会导致总线成为系统性能的瓶颈 。例如,在一个频繁进行数据写入的实时监控系统中,大量的写操作会使 CPU 花费大量时间等待内存写入,从而影响系统对其他任务的响应速度 。
3.2写回(Write Back)
写回策略的工作机制与直写策略有很大不同 。在写回策略中,当 CPU 进行写操作时,数据首先被写入 Cache,并不会立即写入内存 。只有当被修改的 Cache Line(缓存行,是 Cache 与内存之间数据交换的最小单位)要被替换出 Cache 时(比如 Cache 已满,需要腾出空间来存放新的数据),才会将其写回到内存中 。
为了实现这种延迟写入的机制,每个 Cache Line 都有一个脏标记位(Dirty Bit) 。当数据被写入 Cache 时,脏标记位会被设置,表示该 Cache Line 中的数据已经被修改,与内存中的数据不一致 。当 Cache Line 需要被替换时,系统会检查其脏标记位 。如果脏标记位被设置,说明该 Cache Line 中的数据是最新的,需要先将其写回到内存中,然后再进行替换操作;如果脏标记位未被设置,说明该 Cache Line 中的数据与内存中的数据一致,直接进行替换即可 。
写回策略的主要优点是性能较高 。由于大部分写操作只需要在 Cache 中进行,不需要频繁地访问内存,减少了内存访问的次数,从而提高了系统的整体性能 。这种策略尤其适用于那些存在大量写操作的应用场景,比如图形渲染、视频编码等 。在图形渲染过程中,GPU 会对大量的图形数据进行处理和修改,采用写回策略可以减少数据写入内存的次数,加快渲染速度 。
与直写策略相比,写回策略在性能上有明显的优势 。直写策略每次写操作都要访问内存,而写回策略只有在 Cache Line 被替换时才会访问内存,大大减少了内存访问的频率 。但是,写回策略的实现复杂度较高,因为它需要额外维护脏标记位,并且在 Cache Line 替换时需要进行更多的判断和操作 。
同时,由于数据不是立即写入内存,在系统突然断电或崩溃的情况下,可能会导致 Cache 中已修改但未写回内存的数据丢失,从而出现数据不一致的问题 。为了应对这种风险,一些系统会采用写缓冲区(Write Buffer)或日志记录(Logging)等机制来保证数据的一致性 。写缓冲区可以在数据写回内存之前临时存储数据,即使系统崩溃,也可以从写缓冲区中恢复数据;日志记录则可以记录数据的修改操作,以便在系统恢复时进行数据的一致性恢复 。
四、多核时代的挑战:缓存一致性问题
当 CPU 对 Cache 进行写操作时,为了确保 Cache 和内存之间的数据一致性以及提高系统性能,会采用不同的数据写入策略,其中最常见的是直写(Write Through)和写回(Write Back)策略 。
4.1缓存一致性问题的产生
在多核 CPU 的时代,每个 CPU 核心都拥有自己独立的缓存,这虽然进一步提高了数据访问的速度,但也带来了一个新的棘手问题 —— 缓存一致性问题 。
想象一下,有一个多核心的 CPU,其中核心 A 和核心 B 都需要访问内存中的同一个数据 X 。一开始,数据 X 被加载到核心 A 和核心 B 各自的缓存中 。当核心 A 对缓存中的数据 X 进行修改时,此时核心 A 缓存中的数据 X 已经更新,但核心 B 缓存中的数据 X 仍然是旧的 。如果在这个时候,核心 B 读取自己缓存中的数据 X,就会得到一个错误的、过时的值,这就导致了数据不一致的情况 。
以一个简单的银行转账程序为例,假设有两个线程分别在不同的 CPU 核心上执行转账操作 。这两个线程都需要读取账户余额,然后进行相应的加减操作 。如果存在缓存一致性问题,就可能出现这样的情况:第一个线程读取了账户余额为 1000 元,然后在自己的缓存中进行了减 100 元的操作,但还没有将更新后的数据写回内存 。
此时,第二个线程从自己的缓存中读取账户余额,由于其缓存中的数据没有更新,仍然是 1000 元,然后也进行了减 100 元的操作 。最后,两个线程都将各自缓存中的数据写回内存,结果账户余额就变成了 800 元,而实际上应该是 900 元 。这种数据不一致的情况会对程序的正确性产生严重影响,尤其是在涉及到金融、数据库等对数据准确性要求极高的领域 。
4.2解决缓存一致性的方案
要解决缓存一致性问题,首先要解决的是多个 CPU 核心之间的数据传播问题。最常见的一种解决方案呢,叫作总线嗅探。
⑴总线嗅探Bus Snooping
总线嗅探是一种解决缓存一致性问题的基本机制 。在这种机制下,每个 CPU 核心都通过总线与内存相连,并且每个核心都可以 “嗅探” 总线上的通信 。当一个 CPU 核心对自己缓存中的数据进行写操作时,它会向总线发送一个写请求,这个请求包含了被修改数据的地址等信息 。
总线上的其他 CPU 核心会监听这个请求,当它们发现请求中的地址与自己缓存中某数据的地址相同时,就会根据请求的类型(例如是写操作还是使缓存失效的操作),对自己缓存中的数据进行相应的处理 。如果是写操作,其他核心可能会选择将自己缓存中的该数据标记为无效,这样下次访问时就会从内存中重新读取最新的数据;如果是使缓存失效的操作,直接将对应缓存数据标记为无效即可 。
总线嗅探的优点是实现相对简单,不需要复杂的目录结构来记录缓存状态 。它能够快速地检测到其他核心对共享数据的修改,从而及时更新自己的缓存,保证数据的一致性 。然而,它也存在一些明显的缺点 。随着 CPU 核心数量的增加,总线上的通信量会大幅增加,因为每次写操作都要通过总线广播通知其他核心,这会导致总线带宽成为瓶颈,降低系统的整体性能 。而且,总线嗅探机制在处理复杂的多处理器系统时,可能会出现一些一致性问题,例如在多个核心同时进行读写操作时,可能会出现数据更新顺序不一致的情况 。
⑵MESI 协议
MESI 协议是一种广泛应用的缓存一致性协议,它是对总线嗅探机制的进一步优化和完善 。MESI 代表了缓存行的四种状态:Modified(已修改)、Exclusive(独占)、Shared(共享)和 Invalid(无效) 。
-
Modified(已修改,M):当一个 CPU 核心对缓存中的数据进行修改后,该数据所在的缓存行状态变为 M 。此时,缓存中的数据与内存中的数据不一致,并且只有这个核心拥有该数据的最新副本 。在该缓存行被写回内存之前,其他核心如果要访问这个数据,必须先等待该核心将数据写回内存 。
-
Exclusive(独占,E):当一个 CPU 核心从内存中读取数据到缓存时,如果其他核心的缓存中没有该数据的副本,那么该缓存行处于 E 状态 。此时,缓存中的数据与内存中的数据一致,并且这个核心独占该数据 。如果有其他核心也读取这个数据,该缓存行状态会变为 S 。
-
Shared(共享,S):当多个 CPU 核心的缓存中都存在同一个数据的副本时,这些缓存行都处于 S 状态 。此时,缓存中的数据与内存中的数据一致,各个核心都可以同时读取该数据,但如果有一个核心要对数据进行写操作,就需要先将其他核心缓存中该数据的副本失效,然后才能进行修改,修改后缓存行状态变为 M 。
-
Invalid(无效,I):当一个缓存行中的数据被其他核心修改,或者该缓存行被替换出缓存时,它的状态就变为 I 。处于 I 状态的缓存行中的数据是无效的,不能被使用 。
MESI协议中的运行机制
假设有三个CPU A、B、C,对应三个缓存分别是cache a、b、 c。在主内存中定义了x的引用值为0。
①单核读取,那么执行流程是:CPU A发出了一条指令,从主内存中读取x。从主内存通过bus读取到缓存中(远端读取Remote read),这是该Cache line修改为E状态(独享)。
②双核读取,那么执行流程是:
-
CPU A发出了一条指令,从主内存中读取x。
-
CPU A从主内存通过bus读取到 cache a中并将该cache line 设置为E状态。
-
CPU B发出了一条指令,从主内存中读取x。
-
CPU B试图从主内存中读取x时,CPU A检测到了地址冲突。这时CPU A对相关数据做出响应。此时x 存储于cache a和cache b中,x在chche a和cache b中都被设置为S状态(共享)。
③修改数据,那么执行流程是:
-
CPU A 计算完成后发指令需要修改x.
-
CPU A 将x设置为M状态(修改)并通知缓存了x的CPU B, CPU B将本地cache b中的x设置为I状态(无效)
-
CPU A 对x进行赋值。
④同步数据,那么执行流程是:
-
CPU B 发出了要读取x的指令。
-
CPU B 通知CPU A,CPU A将修改后的数据同步到主内存时cache a 修改为E(独享)
-
CPU A同步CPU B的x,将cache a和同步后cache b中的x设置为S状态(共享)。
MESI 协议通过状态转换机制来保证缓存一致性 。例如,当一个处于 S 状态的缓存行接收到一个写请求时,拥有该缓存行的核心会向总线发送一个 Invalidate 消息,通知其他核心将它们缓存中该数据的副本失效 。其他核心收到这个消息后,将自己缓存中对应的缓存行状态变为 I,并返回一个 Invalidate Acknowledge 消息 。当发起写请求的核心收到所有其他核心的确认消息后,它就可以将自己缓存中的数据修改,并将缓存行状态变为 M 。
MESI 协议的优势在于它能够有效地减少总线带宽的占用 。通过状态转换机制,只有在必要时才会进行总线通信,例如当一个核心要修改共享数据时才会通知其他核心使缓存失效,而不是像总线嗅探那样每次写操作都进行广播 。这大大提高了系统在多核环境下的性能和可扩展性 。同时,MESI 协议还能很好地保证数据的一致性,确保各个核心在任何时刻都能访问到正确的数据 。然而,MESI 协议的实现相对复杂,需要额外的硬件支持来维护缓存行的状态和进行状态转换 。而且,在一些极端情况下,例如大量核心同时进行读写操作时,MESI 协议的性能也会受到一定的影响 。
五、如何利用 CPU Cache 优化代码
5.1数据对齐
在计算机中,数据对齐是指将数据存储在内存地址是其自身大小整数倍的位置上 。比如,一个 4 字节的整数(如int类型),应该存储在地址能被 4 整除的地方;一个 8 字节的双精度浮点数(如double类型),应该存储在地址能被 8 整除的位置 。
数据对齐对 CPU Cache 访问效率有着重要的影响 。现代 CPU 在访问内存时,是以 Cache Line 为单位进行数据读取和写入的,常见的 Cache Line 大小为 64 字节 。当数据对齐时,它们更有可能完整地位于同一个 Cache Line 内 。例如,有一个包含多个int类型元素的数组,每个int占 4 字节 。如果数组元素是对齐存储的,那么连续的 16 个int元素就可以刚好存放在一个 64 字节的 Cache Line 中 。当 CPU 访问这个数组的某个元素时,就可以一次性将包含该元素以及相邻 15 个元素的 Cache Line 读入 Cache,后续访问相邻元素时就很可能直接从 Cache 中命中,大大提高了访问效率 。
相反,如果数据没有对齐,就可能出现一个数据跨越两个 Cache Line 的情况 。比如,一个int类型的数据本该存储在地址为 4 的倍数的位置,但却存储在了一个非 4 倍数的地址上,这就可能导致它的一部分在一个 Cache Line 中,另一部分在另一个 Cache Line 中 。当 CPU 访问这个数据时,就需要读取两个 Cache Line,增加了内存访问次数和时间,降低了 Cache 命中率和访问效率 。
下面通过一个简单的 C 语言代码示例来展示数据对齐前后的性能差异 。我们定义一个结构体,分别测试对齐和未对齐情况下对结构体数组的访问速度 。
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <time.h>// 未对齐的结构体
struct UnalignedStruct {char a;int b;char c;
};// 对齐的结构体,使用__attribute__((aligned(4)))确保结构体按4字节对齐
struct __attribute__((aligned(4))) AlignedStruct {char a;int b;char c;
};// 测试函数,计算对结构体数组的访问时间
void testAccessTime(void *arr, size_t numElements, size_t structSize) {clock_t start, end;double cpu_time_used;int i;start = clock();for (i = 0; i < numElements; i++) {// 模拟对结构体成员的访问char *ptr = (char *)arr + i * structSize;char temp = *((char *)ptr);temp = *((int *)(ptr + 1));temp = *((char *)(ptr + 5));}end = clock();cpu_time_used = ((double) (end - start)) / CLOCKS_PER_SEC;printf("Access time: %f seconds\n", cpu_time_used);
}int main() {size_t numElements = 1000000;struct UnalignedStruct *unalignedArr = (struct UnalignedStruct *)malloc(numElements * sizeof(struct UnalignedStruct));struct AlignedStruct *alignedArr = (struct AlignedStruct *)malloc(numElements * sizeof(struct AlignedStruct));if (unalignedArr == NULL || alignedArr == NULL) {perror("malloc failed");return 1;}printf("Testing unaligned struct:\n");testAccessTime(unalignedArr, numElements, sizeof(struct UnalignedStruct));printf("Testing aligned struct:\n");testAccessTime(alignedArr, numElements, sizeof(struct AlignedStruct));free(unalignedArr);free(alignedArr);return 0;
}
在这个示例中,UnalignedStruct是未对齐的结构体,AlignedStruct是通过__attribute__((aligned(4)))进行了 4 字节对齐的结构体 。testAccessTime函数用于测试对结构体数组的访问时间 。通过运行这个程序,可以发现对齐后的结构体数组访问时间明显比未对齐的要短,这直观地展示了数据对齐对 CPU Cache 访问效率和程序性能的提升作用 。在实际编程中,尤其是在对性能要求较高的场景下,合理地进行数据对齐是优化程序性能的重要手段之一 。
5.2循环优化
循环结构在程序中非常常见,它对 CPU Cache 命中率有着显著的影响 。当循环中的数据访问模式不合理时,很容易导致 Cache 未命中次数增加,从而降低程序的执行效率 。以下是一些优化循环以提高 CPU Cache 命中率的方法和建议 。
减少循环嵌套深度:嵌套循环会增加数据访问的复杂性,容易导致 Cache 命中率下降 。
例如,有一个双重嵌套循环:
for (int i = 0; i < N; i++) {for (int j = 0; j < M; j++) {// 访问数组a[i][j]data[i][j] = data[i][j] * 2;}
}
在这个例子中,如果N和M都比较大,那么在访问data[i][j]时,由于数组元素在内存中的存储顺序是按行优先的,当内层循环j不断变化时,可能会频繁地访问不同的 Cache Line,导致 Cache 未命中次数增多 。可以尝试将一些计算逻辑提取到外层循环,减少内层循环的复杂性,从而减少 Cache 未命中 。比如,如果内层循环中有一些与j无关的计算,可以将其移到外层循环:
// 假设存在一些与j无关的计算,这里简化为一个常量计算
int constant = someComplexCalculation();
for (int i = 0; i < N; i++) {// 与j无关的计算结果在每次i循环中可以复用int temp = someCalculationBasedOnI(i, constant);for (int j = 0; j < M; j++) {// 利用与j无关的计算结果进行内层循环操作data[i][j] = temp * data[i][j];}
}
合理安排循环顺序:根据数据在内存中的存储方式和访问的局部性原理,合理安排循环顺序可以提高 Cache 命中率 。以二维数组为例,二维数组在内存中是按行存储的 。
如果按行优先的顺序访问二维数组,更能利用空间局部性原理 。比如:
// 按行优先访问
for (int i = 0; i < ROWS; i++) {for (int j = 0; j < COLS; j++) {sum += matrix[i][j];}
}
这种访问方式下,当访问matrix[i][j]时,由于空间局部性,matrix[i][j + 1]很可能也在同一个 Cache Line 中,从而提高了 Cache 命中率 。而如果按列优先访问:
// 按列优先访问
for (int j = 0; j < COLS; j++) {for (int i = 0; i < ROWS; i++) {sum += matrix[i][j];}
}
在这种情况下,每次访问matrix[i][j]时,下一个访问的matrix[i + 1][j]很可能在不同的 Cache Line 中,导致Cache未命中次数增加,降低了访问效率 。所以,在大多数情况下,按行优先访问二维数组能更好地利用 CPU Cache,提高程序性能 。
在编写程序时,充分考虑循环结构对 CPU Cache 命中率的影响,并运用上述优化方法,可以显著提高程序的运行效率 。尤其是在处理大规模数据和对性能要求苛刻的应用场景中,循环优化是提升程序性能的关键环节之一 。
相关文章:
优化代码性能:利用CPU缓存原理
在计算机的世界里,有一场如同龟兔赛跑般的速度较量,主角便是 CPU 和内存 。龟兔赛跑的故事大家都耳熟能详,兔子速度飞快,乌龟则慢吞吞的。在计算机中,CPU 就如同那敏捷的兔子,拥有超高的运算速度࿰…...
Rust场景示例:为什么要使用切片类型
通过对比 不用切片 和 使用切片 的场景,说明切片类型在 Rust 中的必要性: 场景:提取字符串中的单词 假设我们需要编写一个函数,从一个句子中提取第一个单词。我们将分别展示 不用切片 和 使用切片 的实现,并对比二者的…...
ubuntu直接运行arm环境qemu-arm-static
qemu-arm-static 嵌入式开发有时会在ARM设备上使用ubuntu文件系统。开发者常常会面临这样一个问题,想预先交叉编译并安装一些应用程序,但是交叉编译的环境配置以及依赖包的安装十分繁琐,并且容易出错。想直接在目标板上进行编译和安装&#x…...
HTTP 黑科技
🧑 博主简介:CSDN博客专家,历代文学网(PC端可以访问:https://literature.sinhy.com/#/literature?__c1000,移动端可微信小程序搜索“历代文学”)总架构师,15年工作经验,…...
【Vite + Vue + Ts 项目三个 tsconfig 文件】
Vite Vue Ts 项目三个 tsconfig 文件 为什么 Vite Vue Ts 项目会有三个 tsconfig 文件?首先我们先了解什么是 tsconfig.json ? 为什么 Vite Vue Ts 项目会有三个 tsconfig 文件? 在使用 Vite 创建 vue-ts 模板的项目时,会发现除了 ts…...
Mac怎么彻底卸载软件,简单彻底的卸载方式
一个阳光明媚的下午,咖啡厅里,珂珂正在和他的好友帅帅讨论如何优化他们的Mac电脑,特别是谈到Mac怎么彻底卸载软件的时候,帅帅就特别感同身受,因为他近期就遇到了这个的麻烦,并且找了很久才找到号的解决方案…...
15 刚体变换模块(rigid.rs)
rigid.rs是一个表示三维刚体变换(Rigid Transformation)的结构体定义,用于在计算机图形学、机器人学以及物理模拟等领域中表示物体在三维空间中的旋转和平移。在这个定义中,所有长度在变换后都保持不变,这是刚体变换的…...
springboot使用rabbitmq
使用springboot创建rabbitMQ的链接。 整个项目结构如下: 1.maven依赖 <dependency><groupId>com.rabbitmq</groupId><artifactId>amqp-client</artifactId><version>3.4.1</version> </dependency>application.y…...
【Linux】一文带你入门了解线程和虚拟地址空间中页表映射的秘密(内附手绘底层逻辑图 通俗易懂)
绪论 每日激励:“努力去做自己该做的,但是不要期待回报,不是付出了就会有回报的,做了就不要后悔,不做才后悔。—Jack” 绪论: 本章是LInux中非常重要的线程部分,通过了解线程的基本概念&am…...
高并发、高可用的消息队列(MQ)设计与实战
目录 背景与历史消息队列的核心功能高并发、高可用的业务场景消息队列的实用性企业规模与消息队列的选择Java实战案例:基于RabbitMQ的高并发、高可用消息队列 6.1 环境准备6.2 RabbitMQ的安装与配置6.3 Java客户端集成6.4 生产者与消费者实现6.5 高并发处理6.6 高可…...
nginx 新手指南
文章来源:https://nginx.cadn.net.cn/beginners_guide.html 本指南对 nginx 进行了基本的介绍,并描述了一些 可以用它完成的简单任务。 假设 nginx 已经安装在阅读器的机器上。 如果不是,请参阅 安装 nginx 页面。 本指南介绍如何启动和停止…...
7-4 西安距离
小明来到了古都西安,想去参观大唐西市! 西安的道路可以看做是与x轴或y轴垂直的直线,小明位于(a,b),而目的地位于(c,d),问最少几步可以到达。 输入格式: 一行中四个整数,a,b,c,d,表示坐标为(a…...
VScode+Latex (Recipe terminated with fatal error: spawn xelatex ENOENT)
使用VSCode编辑出现Recipe terminated with fatal error: spawn xelatex ENOENT问题咋办? 很好解决,大概率的原因是因为latex没有添加到系统环境变量中,所有设置的编译工具没有办法找到才出现的这种情况。 解决方法: winR 然后输…...
使用 Elastic Cloud Hosted 优化长期数据保留:确保政府合规性和效率
作者:来自 Elastic Jennie Davidowitz 在数字时代,州和地方政府越来越多地承担着管理大量数据的任务,同时确保遵守严格的监管要求。这些法规可能因司法管辖区而异,通常要求将数据保留较长时间 —— 有时从一年到七年不等。遵守刑事…...
51单片机 02 独立按键
一、独立按键控制LED亮灭 轻触按键:相当于是一种电子开关,按下时开关接通,松开时开关断开,实现原理是通过轻触按键内部的金属弹片受力弹动来实现接通和断开。 #include <STC89C5xRC.H> void main() { // P20xFE;while(1){…...
海外问卷调查渠道查,具体运营的秘密
相信只要持之以恒并逐渐掌握技巧,每一位调查人在踏上征徐之时都会非常顺利的。并在日后的职业生涯中拥有捉刀厮杀的基本技能!本文会告诉你如何做好一个优秀的海外问卷调查人。 在市场经济高速发展的今天,众多的企业为了自身的生存和发展而在…...
Vue.js 的介绍与组件开发初步
Vue.js 的介绍与组件开发初步 Vue.js 的介绍与组件开发初步引言第一部分:Vue.js 基础入门1.1 什么是 Vue.js?1.2 搭建 Vue.js 开发环境安装 Node.js 和 npm安装 Vue CLI创建新项目运行示例 1.3 第一个 Vue.js 示例 第二部分:Vue.js 组件开发基…...
Shadow DOM举例
这东西具有隔离效果,对于一些插件需要append一些div倒是不错的选择 <!DOCTYPE html> <html lang"zh-CN"> <head> <meta charset"utf-8"> <title>演示例子</title> </head> <body> <style&g…...
kamailio-Core 说明书 版本:Kamailio SIP Server v6.0.x(稳定版)
Core 说明书 版本:Kamailio SIP Server v6.0.x(稳定版) 概述 本教程收集了 Kamailio 导出的函数和参数 core 添加到配置文件中。 注意:此页面上的参数不按字母顺序排列。 结构 kamailio.cfg 的结构可以看作是三个部分ÿ…...
PHP XML操作指南
PHP XML操作指南 引言 随着互联网的快速发展,数据交换和共享变得越来越重要。XML(可扩展标记语言)作为一种灵活的标记语言,被广泛应用于各种数据交换场景。PHP作为一种流行的服务器端脚本语言,具有强大的XML处理能力…...
一文了解DeepSeek
1. DeepSeek 的起源 创立时间:DeepSeek 于 2023 年由中国的梁文锋创立。 V3 模型训练成本:最终训练成本为 600 万美元。 开源:DeepSeek 提供开源版本。 流行度:DeepSeek R1 模型成为 Apple 应用商店中下载量最高的应用。 2. …...
三角形的最大周长(976)
976. 三角形的最大周长 - 力扣(LeetCode) 可以一起总结的题目:三数之和(15)-CSDN博客 官方解法: class Solution { public://官方解法int largestPerimeter(vector<int>& nums) {sort(nums.be…...
10:预处理
预处理 1、宏替换2、头文件包含3、条件编译4、typedef和#define的区别5、#define中的注意点5.1、使用do....while(0)5.2、#和##的含义 C语言编译器在编译程序之前,会先使用预处理器(预处理器)处理代码,代码经过预处理之后再送入编译器进行编译。预处理器…...
一文讲解HashMap线程安全相关问题(上)
HashMap不是线程安全的,主要有以下几个问题: ①、多线程下扩容会死循环。JDK1.7 中的 HashMap 使用的是头插法插入元素,在多线程的环境下,扩容的时候就有可能导致出现环形链表,造成死循环。 JDK 8 时已经修复了这个问…...
C++泛型编程指南04-(对默认调用参数的类型推断)
文章目录 问题描述解决方案示例代码 关键点解释进一步改进:结合概念约束 你提到的情况确实是一个常见的问题:在C中,类型推断不适用于默认调用参数。这意味着如果你希望函数模板能够通过默认参数来实例化,你需要为模板参数提供一个…...
Python爬虫:1药城店铺爬虫(完整代码)
⭐️⭐️⭐️⭐️⭐️欢迎来到我的博客⭐️⭐️⭐️⭐️⭐️ 🐴作者:秋无之地 🐴简介:CSDN爬虫、后端、大数据领域创作者。目前从事python爬虫、后端和大数据等相关工作,主要擅长领域有:爬虫、后端、大数据…...
浅谈知识蒸馏技术
最近爆火的DeepSeek 技术,将知识蒸馏技术运用推到我们面前。今天就简单介绍一下知识蒸馏技术并附上python示例代码。 知识蒸馏(Knowledge Distillation)是一种模型压缩技术,它的核心思想是将一个大型的、复杂的教师模型࿰…...
【人工智能】使用Python和Hugging Face构建情感分析应用:从模型训练到Web部署
《Python OpenCV从菜鸟到高手》带你进入图像处理与计算机视觉的大门! 解锁Python编程的无限可能:《奇妙的Python》带你漫游代码世界 情感分析是自然语言处理(NLP)中的重要任务,它通过分析文本来判断情绪或观点的倾向性。近年来,预训练语言模型如BERT、GPT等在情感分析任…...
【R语言】函数
一、函数格式 如下所示: hello:函数名;function:定义的R对象是函数而不是其它变量;():函数的输入参数,可以为空,也可以包含参数;{}:函数体,如果…...
python leetcode 笔记
只为记录一些python相关的特殊写法 无穷大,无穷小,NAN float(inf), float(-inf), float(nan) 判断字符的类型 isdigit(x) isspace(x) 字符串拼接 /.join([a,b,c]) # a/b/c 格式转换,字符转整形 ord(a) # 97 chr(97) # a 进制转…...
基于SpringBoot的青年公寓服务平台的设计与实现(源码+SQL脚本+LW+部署讲解等)
专注于大学生项目实战开发,讲解,毕业答疑辅导,欢迎高校老师/同行前辈交流合作✌。 技术范围:SpringBoot、Vue、SSM、HLMT、小程序、Jsp、PHP、Nodejs、Python、爬虫、数据可视化、安卓app、大数据、物联网、机器学习等设计与开发。 主要内容:…...
深入剖析 HTML5 新特性:语义化标签和表单控件完全指南
系列文章目录 01-从零开始学 HTML:构建网页的基本框架与技巧 02-HTML常见文本标签解析:从基础到进阶的全面指南 03-HTML从入门到精通:链接与图像标签全解析 04-HTML 列表标签全解析:无序与有序列表的深度应用 05-HTML表格标签全面…...
kamailio的kamctl的使用
kamctl 是 Kamailio SIP 服务器的管理工具,用于执行各种管理任务,如启动、停止、重启 Kamailio 进程,管理用户、ACL、路由、信任的 IP 地址等。以下是对 kamctl 命令的解释及举例说明: 1. 启动、停止、重启 Kamailio start: 启动…...
[创业之路-270]:《向流程设计要效率》-2-企业流程架构模式 POS架构(规划、业务运营、支撑)、OES架构(业务运营、使能、支撑)
目录 一、POS架构 二、OES架构 三、POS架构与OES架构的差异 四、各自的典型示例 POS架构典型示例 OES架构典型示例 示例分析 五、各自的典型企业 POS架构典型企业 OES架构典型企业 分析 六、各自典型的流程 POS架构的典型流程 OES架构的典型流程 企业流程架构模式…...
【leetcode100】路径总和Ⅲ
1、题目描述 给定一个二叉树的根节点 root ,和一个整数 targetSum ,求该二叉树里节点值之和等于 targetSum 的 路径 的数目。 路径 不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点…...
用结构加法3ax+1预测第4点的分布
有1个点在19*19的平面上在某种力的作用下运动,轨迹为 共移动了90步,按照(0,1,2,3),(1,2,3,4),…,&…...
CTF-web: Python YAML反序列化利用
PyYAML存在以下几个特殊标签,如果这些标签被不安全的解析,会造成解析漏洞 从 PyYaml 版本 6.0 开始,load 的默认加载器已切换到 SafeLoader,以降低远程代码执行的风险。更新后易受攻击的是 yaml.unsafe_load 和 yaml.load(input, Loaderyaml.UnsafeLoade…...
JDK-1.8.0_432安装(CentOS7)
目录 1、卸载系统自带JDK 2、下载安装包并解压 3、赋予可执行权限 4、设置环境变量 5、刷新环境变量 6、查看JDK版本 1、卸载系统自带JDK # 查询出自带的jdk rpm -qa | grep jdk rpm -qa | grep java # 将上述命令列出的包依次删除 rpm -e --nodeps xxxxxxx 2、下载…...
OpenGL学习笔记(五):Textures 纹理
文章目录 纹理坐标纹理环绕方式纹理过滤——处理纹理分辨率低的情况多级渐远纹理Mipmap——处理纹理分辨率高的情况加载与创建纹理 ( <stb_image.h> )生成纹理应用纹理纹理单元练习1练习2练习3练习4 通过上一篇着色部分的学习,我们可以…...
【Pytorch和Keras】使用transformer库进行图像分类
目录 一、环境准备二、基于Pytorch的预训练模型1、准备数据集2、加载预训练模型3、 使用pytorch进行模型构建 三、基于keras的预训练模型四、模型测试五、参考 现在大多数的模型都会上传到huggface平台进行统一的管理,transformer库能关联到huggface中对应的模型&am…...
2025年Android开发趋势全景解读
文章目录 一、界面开发:从"手写代码"到"智能拼装"1.1 Jetpack Compose实战进化1.2 淘汰XML布局的三大信号 二、AI融合开发:无需炼丹的普惠智能2.1 设备端AI三大杀手级应用2.2 成本对比:设备端VS云端AI 三、跨平台演进&am…...
Python NumPy(12):NumPy 字节交换、NumPy 副本和视图、NumPy 矩阵库(Matrix)
1 NumPy 字节交换 在几乎所有的机器上,多字节对象都被存储为连续的字节序列。字节顺序,是跨越多字节的程序对象的存储规则。 大端模式:指数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址中,这样的…...
【Vaadin flow 实战】第5讲-使用常用UI组件绘制页面元素
vaadin flow官方提供的UI组件文档地址是 https://vaadin.com/docs/latest/components这里,我简单实战了官方提供的一些免费的UI组件,使用案例如下: Accordion 手风琴 Accordion 手风琴效果组件 Accordion 手风琴-测试案例代码 Slf4j PageT…...
第三篇:模型压缩与量化技术——DeepSeek如何在边缘侧突破“小而强”的算力困局
——从算法到芯片的全栈式优化实践 随着AI应用向移动终端与物联网设备渗透,模型轻量化成为行业核心挑战。DeepSeek通过自研的“算法-编译-硬件”协同优化体系,在保持模型性能的前提下,实现参数量与能耗的指数级压缩。本文从技术原理、工程实…...
搜索与图论复习2最短路
单源最短路---所有边权是正数(Dijkstra算法O(n^2)--稠密图(邻接矩阵)和堆优化的Dijkstra算法O(mlogn)--稀疏图(邻接表)) 或存在负边权(Bellman-ford贝尔曼福特算法O(nm)和SPFA一般O(m) 最坏O(nm) ) 多源最短路---Floyd算法O(n^3) 一、迪杰斯特拉算法(Dijkstra):1…...
redis集群理论详解
一. Redis集群发展历程 本片文章只介绍集群理论知识,不包含Redis集群搭建教程 教程文章请点击docker搭建redis集群(三主三从) 阶段一:单机版Redis 优点: 简单:易于部署和使用,适合小型项目或初期…...
本地缓存~
前言 Caffeine是使用Java8对Guava缓存的重写版本,在Spring Boot 2.0中取而代之,基于LRU算法实现,支持多种缓存过期策略。 以下摘抄于https://github.com/ben-manes/caffeine/wiki/Benchmarks-zh-CN 基准测试通过使用Java microbenchmark ha…...
SpringBoot 整合 SpringMVC:SpringMVC的注解管理
分类: 中央转发器(DispatcherServlet)控制器视图解析器静态资源访问消息转化器格式化静态资源管理 中央转发器: 中央转发器被 SpringBoot 自动接管,不需要我们在 web.xml 中配置: <servlet><servlet-name>chapter2&l…...
YOLO11/ultralytics:环境搭建
前言 人工智能物体识别行业应该已经饱和了吧?或许现在并不是一个好的入行时候。 最近看到了各种各样相关的扩展应用,为了理解它,我不得不去尝试了解一下。 我选择了git里非常受欢迎的yolo系列,并尝试了最新版本YOLO11或者叫它ultr…...
扩散模型(三)
相关阅读: 扩散模型(一) 扩散模型(二) Latent Variable Space 潜在扩散模型(LDM;龙巴赫、布拉特曼等人,2022 年)在潜在空间而非像素空间中运行扩散过程,这…...