什么数据会被缓存?

问题描述

书中对cache的描述总是很笼统。我是建筑学专业的学生。我想更详细地了解 cache 的行为。

在c/c++语言代码中,哪些数据会从内存加载到缓存?经常使用的时候会加载到缓存中吗?比如我用C语言写for循环的时候,经常用到变量i,j,and k。这些也会被加载到缓存中吗? C语言局部变量一般放在栈区,全局变量会放在数据区吗?这些在使用的时候会先加载到缓存中吗?数据是否必须经过缓存才能到达寄存器然后到达cpu

指针变量p存储数据的地址。如果我使用指针 *p 来访问一个变量。会先把p加载到缓存中,然后再加载*p吗?

解决方法

通常您的 C++ 程序使用的所有内存(代码和数据)都在可缓存内存中。

对任何 C++ 对象的任何访问(读或写)1 将导致包含它的缓存行变热,假设正常的 CPU 缓存:set-associative、write-back / write -allocate1,即使它以前不热。

最简单的设计是每一级缓存通过下一个外层取数据,所以在一次加载未命中后,数据在各级缓存中都是热的。但是您可以拥有不进行读取分配的外部缓存,并充当 victim caches。或者是内部缓存独占的外部级别,以避免浪费空间缓存相同的数据两次 (https://en.wikipedia.org/wiki/Cache_inclusion_policy)。 但是无论发生什么,在读取或写入之后,至少最内层(最接近 CPU 内核)的缓存级别都会使数据处于热状态,因此立即再次访问它(或同一层中的相邻项)缓存行)会很快。如果下一次访问是在一堆其他访问之后,不同的设计选择会影响线路仍然很热的机会。如果很热,您可能会在哪个级别的缓存中找到它。但最基础的是,任何内存编译器生成的代码都会接触到缓存。 CPU 缓存透明地缓存物理内存。

许多缓存线可以同时处于热状态,不会相互混淆。即缓存有很多集合。一些访问模式是悲观的,例如多个指针都相互偏移 4k,这将使所有访问别名在 L1d 缓存中设置相同,并且有时在 CPU 的内存消歧逻辑中具有额外的 4k 别名惩罚。 (假设 4k 页面大小,如 x86)。例如L1 memory bandwidth: 50% drop in efficiency using addresses which differ by 4096+64 bytes - 内存性能影响可能变得非常复杂。了解一些理论足以理解什么是好的,但确切的细节可能非常复杂。 (有时甚至是像带宽博士这样的真正专家,例如 this case)。

脚注 1:松散地说,对象是一个命名变量或由指针指向的动态分配的内存。

脚注 2:带有写分配策略的回写缓存对于现代 CPU 几乎是通用的,也是一种伪 LRU 替换策略;见wikipedia。一些设备的访问模式受益于仅在读取而不是写入时分配的缓存,但 CPU 受益于写入分配。现代 CPU 几乎总是具有多级缓存层次结构,每个级别都与某个级别的关联性设置关联。一些嵌入式 CPU 可能只有 1 级,甚至没有缓存,但如果您专门为这样的系统编写代码,您就会知道。

现代大型 L3 缓存有时会使用替换策略。


当然,优化可能意味着一些局部变量(尤其是循环计数器或数组指针)可以优化到寄存器中,而不存在于内存中。寄存器根本不是 CPU 缓存或内存的一部分,它们是一个单独的存储空间。人们通常将事物描述为“编译器缓存寄存器中的值”,但不要将其与 CPU 缓存混淆。 (相关:https://software.rajivprab.com/2018/04/29/myths-programmers-believe-about-cpu-caches/When to use volatile with multi threading?

如果您想了解编译器让 CPU 做什么,请查看编译器的 asm 输出。 How to remove "noise" from GCC/clang assembly output?。 asm 源中的每个内存访问都是计算机体系结构术语中的访问,因此您可以应用您对给定访问模式的缓存状态的了解来确定集合关联缓存会发生什么。

还相关:

,

通常,最近使用的缓存行会存储在缓存中。对于短循环,循环计数器变量通常存储在 CPU 寄存器中。对于较长的循环,循环计数器变量可能会存储在缓存中,除非一个循环迭代运行了很长时间,以至于循环计数器由于 CPU 执行其他工作而从缓存中被逐出。

大多数变量通常会在第一次访问后缓存(或者如果 cache prefetcher 做得很好,则事先缓存),无论它们使用的频率如何。较高的使用频率只会防止内存从缓存中被逐出,而不会影响它首先被缓存。但是,某些 CPU 架构提供所谓的 non-temporal read and write instructions,它绕过缓存。如果程序员事先知道一个内存位置只会被访问一次,因此不应该被缓存,那么这些指令很有用。但一般来说,不应使用这些说明,除非您确切地知道自己在做什么。

CPU 缓存并不关心变量是存储在堆上还是堆栈上。内存只是根据“最近使用”算法进行缓存,或者,更准确地说,缓存会根据“最近最少使用”算法逐出,只要新的内存访问需要缓存中的新空间。

在局部变量存储在堆栈中的情况下,由于程序最近将该堆栈缓存线用于其他用途,该变量的缓存线很有可能已经被缓存。因此,局部变量一般具有良好的缓存性能。此外,缓存预取器与堆栈配合得非常好,因为堆栈以线性方式增长。

指针变量p存储数据的地址。如果我使用指针 *p 来访问一个变量。会先把p加载到缓存中,然后再加载*p吗?

是的,首先,包含 p 的缓存行将被加载到缓存中(如果它尚未缓存或存储在 CPU 寄存器中)。然后,包含 *p 的缓存行将被加载到缓存中。