CPU写缓存策略
CPU写缓存策略主要用于管理数据在CPU缓存和主存之间的写操作。以下是三种常见的CPU写缓存策略的介绍:
1. Write Through(直写)
Write Through策略是指每次数据写入时,数据同时写入缓存和主存。其特点和优缺点如下:
特点:
- 当CPU修改缓存中的某一数据时,立即将该数据同步写入主存。
- 保持主存中的数据与缓存中的数据一致性。
优点:
- 数据一致性好。因为数据被同时写入缓存和主存,所以无论何时访问主存,数据总是最新的。
- 实现简单,硬件设计容易。
缺点:
- 写操作的速度较慢。因为每次写入缓存数据时都必须同时写入主存。
- 对于写操作密集的应用性能较差,因为每次写入都需要访问较慢的主存。
2. Write Back(回写)
Write Back策略是指数据写入缓存时仅修改缓存中的数据,而不立即写入主存。只有当缓存行被替换时,才将数据写入主存。其特点和优缺点如下:
特点:
- 当CPU修改缓存中的数据时,数据只写入缓存,而不是立即写入主存。
- 只有当缓存行被替换(即从缓存中移除)时,才会将修改后的数据写回主存。
优点:
- 写操作速度较快,因为数据只需写入缓存,主存写操作延迟较少。
- 适合写操作密集的应用,因为减少了主存的访问次数。
缺点:
- 数据一致性较差。因为主存中的数据可能滞后于缓存中的数据,其他处理器或设备读取主存时可能得到过时的数据。
- 硬件实现复杂。需要额外的机制来管理缓存行的状态,以决定何时将数据写回主存。
3. Write Allocate(写分配)
Write Allocate策略通常与Write Back策略结合使用。其特点和优缺点如下:
特点:
- 当CPU尝试写入一个不在缓存中的数据时,先将数据块从主存加载到缓存,然后再进行写操作。
- 与Write No-Allocate(不写分配)相反,后者是在不命中缓存时直接将数据写入主存而不加载到缓存。
优点:
- 提高缓存命中率。因为将数据加载到缓存中,后续对同一数据的写操作可以直接命中缓存。
- 性能较好。结合Write Back策略时,减少了对主存的写操作次数。
缺点:
- 初始写操作延迟较大。因为需要先将数据块加载到缓存中。
- 可能导致缓存污染。即某些不经常使用的数据也被加载到缓存中,占用宝贵的缓存空间。
综合对比
- Write Through:数据一致性好,但写操作速度慢,适用于对数据一致性要求高的场景。
- Write Back:写操作速度快,但数据一致性较差,适用于写操作频繁且可以容忍数据一致性延迟的场景。
- Write Allocate:提高缓存命中率和整体性能,适用于需要频繁访问特定数据块的场景。
选择哪种策略主要取决于具体应用场景的需求和系统架构设计的考虑。
MESI
MESI(Modified, Exclusive, Shared, Invalid)是一个用于管理多核处理器中缓存一致性的协议。这种协议用于确保多个处理器核心在访问共享内存时能够保持数据一致性。MESI协议定义了每个缓存行的四种状态,并通过这些状态来协调不同处理器对同一内存地址的访问。以下是MESI协议的四种状态及其含义:
MESI 四种状态
-
Modified(M,修改) :
- 缓存行包含已被修改但尚未写回主存的数据。
- 该缓存行在所有缓存中唯一有效(独占),其他缓存都无此数据。
- 处理器可以读取和写入此缓存行而不需要与其他处理器通信。
-
Exclusive(E,独占) :
- 缓存行包含与主存相同的数据,且只有此处理器的缓存中有此数据。
- 数据未被修改,因此与主存一致。
- 处理器可以读取和写入此缓存行,但如果要写入,则需将状态变为Modified。
-
Shared(S,共享) :
- 缓存行包含与主存相同的数据,且可能存在于多个处理器的缓存中。
- 数据未被修改,因此与主存一致。
- 处理器可以读取但不能写入此缓存行,如果要写入,需先发出无效信号,使其他缓存中的此行无效,并将状态变为Modified。
-
Invalid(I,无效) :
- 缓存行不包含有效数据,或者数据已经被其他处理器修改。
- 处理器不能读取或写入此缓存行,必须从主存或其他缓存中获取最新数据。
MESI协议的工作机制
MESI协议通过以下机制保持缓存一致性:
-
读取操作(Read) :
- 如果缓存行状态为Modified或Exclusive,处理器直接读取数据。
- 如果缓存行状态为Shared,处理器可以读取数据,但其他处理器也可能有相同的数据。
- 如果缓存行状态为Invalid,处理器需要从主存或其他缓存中读取数据,并将缓存行状态更新为Shared或Exclusive。
-
写入操作(Write) :
- 如果缓存行状态为Modified,处理器直接写入数据。
- 如果缓存行状态为Exclusive,处理器可以写入数据,并将状态变为Modified。
- 如果缓存行状态为Shared,处理器必须先将其他缓存中的该行无效化,然后将状态变为Modified,再写入数据。
- 如果缓存行状态为Invalid,处理器需要从主存或其他缓存中读取数据,将状态变为Exclusive或Modified,再写入数据。
-
缓存一致性消息:
- 无效信号(Invalidate) :当处理器要写入一个Shared状态的缓存行时,会向其他处理器发送无效信号,使其缓存中的该行变为Invalid。
- 读取请求(Read Request) :当处理器要读取一个Invalid状态的缓存行时,会向其他处理器或主存请求数据。
- 读取响应(Read Response) :收到读取请求的处理器或主存返回数据。
MESI协议的优势
- 保持数据一致性:通过定义和管理缓存行的状态,MESI协议确保多核处理器环境下数据的一致性。
- 减少主存访问:允许处理器从其缓存中读取数据而不必频繁访问主存,从而提高系统性能。
- 提高并行性:通过管理缓存行状态,MESI协议允许多个处理器并行处理数据,减少了数据冲突和通信开销。
小结
MESI协议是多处理器系统中常用的缓存一致性协议,通过管理缓存行的四种状态(Modified、Exclusive、Shared、Invalid),有效地保持数据一致性,并提高系统性能。理解MESI协议对于设计高效的多处理器系统和优化并行计算应用非常重要。
Cache line
为什么缓存是line不是单字节
- 为单字节标记状态不经济
- 利用空间局部性
- 总线都是批量存取内存
Cache Miss
在计算机系统中,缓存未命中(Cache Miss)是指处理器试图从缓存中读取数据而缓存中没有该数据的情况。根据缓存未命中的原因,可以将其分为以下几种类型:
1. Conflict Miss(冲突未命中)
冲突未命中是指即使缓存总容量足够存储所需的数据,但是由于缓存分配和映射策略(如直接映射或组相联映射)的限制,不同的数据块被映射到相同的缓存位置,从而导致缓存未命中。
原因:
- 由于缓存中的多个数据块被映射到相同的缓存行,导致频繁替换。
- 通常发生在直接映射缓存或低组相联度缓存中。
解决方法:
- 增加缓存的相联度(如使用全相联缓存或高组相联度缓存)。
- 改善缓存替换策略,使得常用数据块尽可能保留在缓存中。
- 优化程序代码以减少数据块映射冲突。
2. Capacity Miss(容量未命中)
容量未命中是指由于缓存容量不足,导致缓存无法存放所有需要的数据块,从而导致未命中。
原因:
- 缓存的总容量小于需要存储的数据集的总大小,导致频繁的缓存替换和未命中。
- 常见于需要处理大量数据的应用程序,如大规模科学计算或数据处理。
解决方法:
- 增加缓存容量,以容纳更多的数据块。
- 优化算法和数据结构,以提高缓存利用率。
- 使用分层缓存架构(如L1、L2、L3缓存)来缓解容量限制。
3. Communication Miss(通信未命中)
通信未命中主要出现在多处理器或多核系统中,指由于多个处理器或核心之间的数据通信导致的缓存未命中。这种类型的未命中与缓存一致性协议和数据共享模式有关。
原因:
- 多个处理器或核心之间共享数据时,需要保持缓存一致性,导致缓存行被频繁无效化或替换。
- 当一个处理器修改了共享的数据,其他处理器需要获取最新数据,导致未命中。
解决方法:
- 使用高效的缓存一致性协议,如MESI(Modified, Exclusive, Shared, Invalid)协议,来减少通信开销。
- 设计程序时尽量减少共享数据的修改,使用适当的锁和同步机制。
- 优化数据布局和访问模式,减少跨处理器的数据共享。
小结
缓存未命中是影响计算机系统性能的重要因素,不同类型的未命中有不同的成因和解决方法:
- Conflict Miss:由于数据块映射冲突引起,解决方法包括增加缓存相联度和优化代码。(多个内存映射同一块缓存)
- Capacity Miss:由于缓存容量不足引起,解决方法包括增加缓存容量和优化算法。
- Communication Miss:由于多处理器间数据通信引起,解决方法包括使用高效的缓存一致性协议和优化数据访问模式。
通过理解和分析不同类型的缓存未命中,可以针对性地采取措施,优化系统性能。
cache line取址和映射方式
Direct Mapped Cache(直接映射缓存)
地址含义:
- 地址0、1位:表示cacheline的data中的偏移地址(单位:字节),一个line 4子节,两位就够了
- 地址2~11位:表示哪条line(用于定位缓存行的位置)。
- 12~31位:用于标识内存块的标识符,当缓存行的标签与内存地址的标签一致时,表示命中。
优点
- 简单高效:由于每个内存块只能映射到一个特定的缓存行,因此查找和更新操作非常快速。
- 硬件实现简单:不需要复杂的硬件逻辑进行选择和替换,只需要简单的索引计算。
缺点
- 冲突未命中(Conflict Miss) :当多个频繁访问的内存块被映射到相同的缓存行时,会导致频繁的替换,从而增加缓存未命中率。这种现象称为冲突未命中,是直接映射缓存的一大缺点。
- 低缓存利用率:在一些应用程序中,某些缓存行可能被频繁替换,而其他缓存行则可能空闲,导致缓存空间利用率不高。
两路组相联缓存
两路组相联缓存(Two-Way Set Associative Cache)是一种介于直接映射缓存和全相联缓存之间的缓存组织方式。它结合了两者的优点,通过将缓存分成多个集合(sets),每个集合包含多个缓存行(ways),从而在提高缓存命中率的同时保持查找效率。
工作原理
在两路组相联缓存中,内存地址被分解成三部分:标签(Tag)、索引(Index)和块内偏移(Block Offset)。内存地址中的索引部分用于选择特定的缓存集合,标签部分用于在该集合内的多路中找到匹配的缓存行。
地址分解示例
假设有一个两路组相联缓存,缓存总大小为8 KB,每个块的大小为64字节,总共有64个缓存集合。每个集合包含两条缓存行。
- 块内偏移(Block Offset) :6位(因为64字节块,26=642^6 = 6426=64)
- 索引(Index) :6位(因为有64个集合,26=642^6 = 6426=64)
- 标签(Tag) :剩余的位数
缓存结构
- 标签(Tag) :用于识别内存块。
- 索引(Index) :用于选择特定的缓存集合。
- 块内偏移(Block Offset) :用于定位块内的具体字节。
操作步骤
读取操作(Read Operation)
- 计算索引:从内存地址中提取索引部分,定位到特定的缓存集合。
- 匹配标签:在该集合的两条缓存行中查找匹配的标签。
- 读取数据:如果找到匹配的标签且有效,则读取缓存中的数据。如果没有匹配的标签,则发生缓存未命中,需要从主存读取数据并将其加载到缓存中。
写入操作(Write Operation)
- 计算索引:从内存地址中提取索引部分,定位到特定的缓存集合。
- 匹配标签:在该集合的两条缓存行中查找匹配的标签。
- 写入数据:如果找到匹配的标签且有效,则直接写入缓存。如果没有匹配的标签,则需要根据替换策略选择一个缓存行进行替换,并将新数据写入缓存。
替换策略
在两路组相联缓存中,当一个集合的所有缓存行都已被占用且发生未命中时,需要替换一个缓存行。常见的替换策略包括:
- LRU(Least Recently Used) :替换最近最少使用的缓存行。
- FIFO(First In First Out) :替换最早进入缓存的缓存行。
- Random:随机替换一个缓存行。
优点
- 减少冲突未命中:相比于直接映射缓存,两路组相联缓存通过增加缓存行的选择范围,减少了冲突未命中的概率。
- 性能折中:比全相联缓存的硬件实现更简单,同时比直接映射缓存的命中率更高,提供了性能和复杂度的折中。
缺点
- 硬件复杂度增加:相比于直接映射缓存,两路组相联缓存的硬件实现更复杂,因为需要在每个集合中查找多个缓存行。
- 访问延迟略有增加:由于需要比较多个标签,访问时间可能比直接映射缓存稍长。
示例
假设有一个两路组相联缓存,总大小为8 KB,每个块大小为64字节,总共有128个缓存块,分成64个集合,每个集合包含2个缓存行。
- 内存地址:
0x12345678
- 64字节块(6位块内偏移)
- 64个集合(6位索引)
内存地址分解:
- Tag:
0x12345
- Index:
0x16
(二进制010110
) - Block Offset:
0x78
(二进制01111000
)
通过索引0x16
找到对应的缓存集合,然后在该集合的两条缓存行中查找标签0x12345
,如果找到并有效,则读取数据;否则,发生未命中,需要从主存加载数据。
总结
两路组相联缓存是一种平衡了性能和复杂度的缓存组织方式,通过增加缓存集合中的缓存行数,减少了冲突未命中的概率,同时保持了相对简单的硬件实现。了解这种缓存组织方式对于优化系统性能和设计高效的计算机体系结构非常重要。
N-way Set Associative Cache(N路组相连)
Fully Associative Cache全组相连
False Sharing(伪共享)
False Sharing(伪共享)是多线程编程中的一种性能问题,它发生在多个线程频繁地写入位于同一个缓存行中的不同变量时。虽然这些变量在逻辑上是独立的,但因为它们共享一个缓存行,所以一个线程对某个变量的写操作会导致其他线程对其他变量的缓存无效化,进而引发性能下降。
原理和原因
缓存行:缓存是以缓存行为单位进行存储的,典型大小为64字节。当多个线程访问和修改属于同一个缓存行的不同数据时,即使这些数据在逻辑上没有关系,也会因为共享同一个缓存行而产生伪共享。
示例
假设有两个线程分别操作两个变量x
和y
,这两个变量恰好位于同一个缓存行中。
// 线程1
for (int i = 0; i < 1000; i++) {
x++;
}
// 线程2
for (int i = 0; i < 1000; i++) {
y++;
}
即使线程1和线程2分别操作不同的变量,由于x
和y
共享同一个缓存行,线程1对x
的修改会使得线程2对y
所在的缓存行失效,反之亦然。这导致每次写操作后,缓存一致性协议(如MESI协议)需要频繁地在不同缓存之间进行通信,从而严重影响性能。
影响
- 缓存一致性流量增加:由于频繁的缓存失效和刷新,处理器之间需要大量通信来保持缓存一致性。
- 性能下降:伪共享会导致缓存命中率下降,增加内存访问延迟,整体性能下降。
解决方法
-
数据对齐与填充:通过插入填充字节(padding)来将独立的变量放置在不同的缓存行中,避免伪共享。例如,可以在每个变量之间插入一个64字节大小的填充,使每个变量独占一个缓存行。
c 复制代码 struct PaddedData { int x; char padding1[60]; // 填充到64字节(假设int为4字节) int y; char padding2[60]; };
-
优化数据布局:重新组织数据结构,使频繁访问的数据尽可能分散到不同的缓存行。
-
减少共享数据的写操作:通过优化算法和数据结构,尽量减少多个线程同时写入共享数据的情况。
示例改进
以下是改进的示例,通过填充避免伪共享:
struct PaddedData {
alignas(64) int x;
alignas(64) int y;
};
// 线程1
for (int i = 0; i < 1000; i++) {
paddedData.x++;
}
// 线程2
for (int i = 0; i < 1000; i++) {
paddedData.y++;
}
在这个改进的示例中,x
和y
都被对齐到64字节的边界,确保它们位于不同的缓存行中,避免了伪共享的问题。
小结
伪共享是多线程编程中的常见性能陷阱,它由于缓存行的共享而导致不必要的缓存一致性流量。通过适当的数据对齐和填充、优化数据布局以及减少共享数据的写操作,可以有效地避免伪共享,从而提高程序的性能。了解和解决伪共享对于编写高效的多线程程序至关重要。