缓存一致性 伪共享

volatile并不是处理缓存一致,而是强制实时push cpu处理缓存一致

非volatile也有可能产生伪共享,只是概率比volatile小的多,因为cpu很可能在空闲时处理缓存更新,既然空闲了,伪共享的影响就大大降低了

虽然非volatile访问主存的概率降低了,但是仍然有很大概率导致多核处理器访问访问相同缓存行的串行化

 

 

 

1

https://blog.csdn.net/lyndon_li/article/details/126068426

 

 

 

https://blog.csdn.net/lyndon_li/article/details/131340763

# ./perf stat -e cache-references -e cache-misses ./cacheline_not_fill.out

 Performance counter stats for './cacheline_not_fill.out':

       12005744527      cache-references
         986698086      cache-misses              #    8.219 % of all cache refs

      15.095276549 seconds time elapsed

      29.822868000 seconds user
       0.000000000 seconds sys

  

# ./perf stat -e cache-references -e cache-misses ./cacheline_fill.out

 Performance counter stats for './cacheline_fill.out':

       12005381835      cache-references
             63555      cache-misses              #    0.001 % of all cache refs

      13.942023631 seconds time elapsed

      27.839129000 seconds user
       0.000000000 seconds sys

  

没有缓存行填充的代码,cache-misses 达 8.219%,运行时长为 15s;

 

 

 

 

https://blog.csdn.net/Orwell_VII/article/details/124175518

使用perf 来检测程序执行期间由此造成的cache miss的命令是perf stat -e cache-misses ./exefilename,另外,检测cache miss事件需要取消内核指针的禁用(/proc/sys/kernel/kptr_restrict设置为0)。

C语言中,对于二维数组,同一行的数据是相邻的,同一列的数据是不相邻的。

 

 

 

https://www.cnblogs.com/zhujiwei/p/14726848.html

  1. 多个核心对同一缓存行的高频修改会导致严重的性能开销,影响多核的可扩展性。由于缓存一致性协议同一时刻只允许一个核心独占修改该缓存行,会造成多核执行流串行化,无法充分发挥出多核的性能优势;此外,多个核心对于同一缓存行的高频修改还会导致高速互联总线中产生大量缓存一致性流量,从而造成性能瓶颈。
  2. 伪共享(False Sharing)。伪共享是指本身无需在多核之间共享的内容被错误地划分到同一个缓存行中,并引起了多核环境下对于单一缓存行的竞争,从而导致无谓的性能开销。

 

为了使得两个线程跑在不同的核上,我们需要设置CPU亲和性

将num0和num1放到不同的缓存行,可以通过如下方式:

int num0;
int num[1000];
int num1;

再运行并行代码,运行时间如图所示,可以看到只需要0.18s,接近串行的二分之一时间,和预料中一致。

 

 

 

https://www.51cto.com/article/777615.html

2个处理器写到不同的物理地址,但是2个物理地址映射到同一个缓存行,这种情况称为缓存行伪共享(False Sharing),

 

 

 

https://cloud.tencent.com/developer/article/1186380

CPU在读主存的时候,会先将主存的一块数据加载到缓存上,然后在缓存上读取。当CPU写主存的时候,它会首先写缓存,在未来的某个时间点再一次性将缓存的数据全部刷回主存,这样就可以提高写操作的性能。因为计算机程序数据操作的局部性,CPU连续的指令倾向于访问相邻地址空间的数据,所以后续的读写操作有很大的概率可以直接在缓存上拿到数据。如果缓存上不存在,那就再去主存上加载进来。

普通变量不需要保证线程之间的读写的可见性,CPU对缓存修改后不需要立即回写内存,不存在写操作缓存穿透现象。而读操作也不需要总是重新从内存加载,那这个效率几乎完全就是缓存访问的效率,而对volatile变量的读写操作则接近内存访问的效率,差距自然如此明显。不对,普通变量也有可能碰巧一个缓存行,虽然伪共享概率低,但是多核串行化概率高

你也许会问,知道这些有什么蛋用!

确是没什么蛋用,因为在现实世界,大部分操作都涉及到IO操作。根据水桶效应,其它环节优化到了极致,也无法提升整体的质量。

 
著名的disruptor框架正是使用了缓存行填充技术,才使得它的环形数组队列能如此高效。看wiki上的性能报告,disruptor的RingBuffer相比Java内置的ArrayBlockingQueue在OPS上高出近一个数量级,在队列延迟上则低了接近3个数量级。
 
 
 
 
https://blog.csdn.net/zwh1zwh/article/details/148051123

class Data {
volatile long x; // 线程1频繁修改
volatile long y; // 线程2频繁修改
}

通过Async-Profiler观察缓存未命中事件(L1-dcache-load-misses),可发现伪共享导致的高缓存失效率。

  • 将高频修改的字段隔离到独立对象中。
  • 使用数组+线程ID分散写入位置(如Disruptor环形队列的设计)。

 

通过perf工具观察到L1-dcache-load-misses下降90%。

perf stat -e L1-dcache-load-misses,L1-dcache-loads java MyApp

L1-dcache-load-misses率可能暗示伪共享。

Java Object Layout (JOL)​​:
查看对象内存布局,确认字段是否相邻

System.out(ClassLayout.parseClass(Data.class).toPrintable());
 

​隔离写入热点​:分散变量到不同缓存行。

  • Disruptor:通过填充和缓存行对齐设计实现无锁高性能队列。
  • Agrona:提供DirectBufferAtomicBuffer避免伪共享。

 

 

 

https://blog.csdn.net/qq_45443475/article/details/131417090

一般来说,每级缓存的命中率大概都在80%左右,也就是说全部数据量的80%都可以在一级缓存中找到,只剩下20%的总数据量才需要从二级缓存、三级缓存或内存中读取,由此可见一级缓存是整个CPU缓存架构中最为重要的部分。

由于存放到Cache行的的是内存块而不是单个变量,所以可能会把多个变量存放到了一个cache行。当多个线程同时修改一个缓存行里面的多个变量时候,由于同时只能有一个线程操作缓存行,所以相比每个变量放到一个缓存行性能会有所下降,这就是伪共享。

我们可以在任何字段之间通过填充长整型的变量把热点变量隔离在不同的缓存行中

 
 
 
https://zhuanlan.zhihu.com/p/370648332
伪共享这一现象在内存队列高并发消费的情况下可能会出现,比如ArrayBlockingQueue中的字段putIndex、count、takeIndex(non volatile)往往容易被缓存在同一个cache line中,这样在多个消费线程的作用下,这些队列的元字段的产生了更新,可能就会一定程度上导致各个独立的消费者在CPU缓存的层面上产生相互的影响。
 
 
 
 
https://blog.csdn.net/zhaoyqcsdn/article/details/147198790
伪共享是指多个线程同时修改位于同一缓存行(Cache Line)的不同变量时,由于缓存一致性协议的影响,导致缓存行在不同核心之间频繁无效化和重新加载,从而引起性能下降的现象。

 

2

C++有此问题,由此可推得,并不是volatile专利

 

https://juejin.cn/post/7481604667589623847

有了MESI缓存一致性协议为什么还需要volatile?

但是我们可以清晰看到 MESI 的问题所在:

  1. 如果 CPU 0 发出了多个相互之间无依赖的指令,进行串行化操作阻塞就变得非常低效。
  2. 如果 CPU 1 发生阻塞,一直不返回 ACK,CPU 0 也会被动阻塞。

为了解决上述的问题,后来就引入了 Store Buffer 和 Invalidate Queue。

  • 有了 StoreBuffer,CPU 0 不等 invalidate ack:先写入 store buffer,然后继续做事。之后收到 invalidate ack 再更新 cache 的状态。因为最新的資料可能存在 store buffer,CPU 读资料的变为 store buffer → cache → memory。
  • 有了 Invalidate Queue,CPU1 可以立即回复 invalidate ack 消息给发出广播的 CPU 0,之后 invalidate queue 再异步执行将缓存行失效。

 

其实在这里我们也可以看出这是一个典型的 CAP 问题,通过 StoreBuffer 和 Invalidate Queue异步处理,这种设计牺牲了强一致性(Consistency)来保障可用性(Availability)和分区容忍性(Partition Tolerance),使得强一致变为了最终一致性
volatile使最终一致性升级为强一致性

volatile 还有一个关键的特性就是禁止指令重排序。
由于MESI的存在,即使没有volatile,也总有一天会刷主存,只是晚一点嘛;然后加上指令重排就跪了
 
而 volatile 的有序性,是将 store buffer 里的数据全都加载到cache中。

 

  • 写屏障(Store Barrier)保证屏障前的所有写操作完成后才能执行屏障后的写操作;storebuffer 内的数据刷新为 cache
  • 读屏障(Load Barrier)保证屏障后的读操作前先完成屏障前的所有读操作,让 invaildQueue 中数据全部得到处理
volatile ---- 强一致性
避免多核心操作多个变量导致的看似“重排序”情况。
 
 
 
https://cloud.tencent.com/developer/news/762462
  • 写屏障与(StoreStore、StoreLoad)屏障的关系:在volatile变量写之前加入StoreStore屏障保证了volatile写之前,写缓冲器中的内容已全部刷回告诉缓存,防止前面的写操作和volatile写操作之间发生指令重排,在volatile写之后加入StoreLoad屏障,保证了后面的读/写操作与volatile写操作发生指令重排,所以写屏障同时具有StoreStore与StoreLoad的功能
  • 读屏障与(LoadLoad、LoadStore)屏障的关系:在volatile变量读之后加入LoadLoad屏障保证了后面其他读操作的无效队列中无效消息已经被刷回到了高速缓存,在volatile变量读操作后加入LoadStore屏障,保证了后面其他写操作的无效队列中无效消息已经被刷回高速缓存。读屏障同时具有了LoadLoad,LoadStore的功能。

 

 

 

https://baijiahao.baidu.com/s?id=1806898723094891990&wfr=spider&for=pc&searchword=%E7%BC%93%E5%AD%98%E4%B8%80%E8%87%B4%E6%80%A7%20volatile

  1. 编程语言抽象:Java等高级编程语言提供了丰富的抽象和并发控制机制,如volatile、synchronized等,这些机制为开发者提供了更直观、更易用的并发编程方式。

  2. 性能优化:volatile关键字允许JVM进行更细粒度的优化,而不仅仅是依赖硬件的缓存一致性协议。例如,JVM可以通过内存屏障和锁细化等技术来减少不必要的同步开销。

  3. 跨平台兼容性:Java设计之初就考虑到了跨平台的需求,volatile关键字为JVM提供了一种标准化的方式来处理跨平台的缓存一致性问题。

 

 

 

https://cloud.tencent.com/developer/article/2435505

伪共享发生在多个线程访问不同变量,但这些变量位于同一缓存行中时。由于缓存行是缓存的最小单位,当一个线程修改了缓存行中的一个变量时,整个缓存行都会被标记为无效。这意味着其他线程需要重新从主内存加载整个缓存行

#include <iostream>
#include <emmintrin.h>
struct alignas(64) AlignedData {
    int value;
    char padding[60];
};
int main() {
    AlignedData data;
    data.value = 42;
    std::cout << "Value: " << data.value << std::endl;
    return 0;
}

使用alignas(64)来确保AlignedData结构体对齐到64字节边界,即一个缓存行的大小。这样,即使我们在多个线程中访问不同的AlignedData实例,它们也不会共享相同的缓存行,

 

因为缓存行是内存的mapping,两个>=64字节的对象无论如何不会mapping到同一个缓存行

 

 

 

https://blog.csdn.net/weixin_41165867/article/details/147485229

MESI 协议在保证一致性的同时,也引入了额外的性能开销。为了优化这一过程,现代处理器引入了写缓冲区(Write Buffer)和失效队列(Invalid Queue)等机制,将强一致性模型转变为最终一致性模型。这种优化虽然提升了性能,但也带来了指令重排和缓存不一致等新问题。为了解决这些问题,内存屏障(Memory Barrier)技术被引入,用于在特定场景下保证强一致性。

CPUB先从主内存中读取到变量x,此时CPUB的本地变量x是独占状态。
CPUA试图从主内存中读取x时,CPUB通过总线检测到了地址冲突。此时x存储于cache a和cache b中,x在chche a和cache b中都被设置为共享状态。
CPUA修改x的时候需要CPUA中的本地变量x是独享状态,如果不是会通过总线广播发出失效命令,让CPUB的高速缓存对应本地变量x的状态变为失效状态。
CPUA收到CPUB的ACK后让本地的变量x变为独占状态后才能够修改并变成修改状态。
CPUA随后会将修改后的数据写回到主内存(如果需要)。

 

后续 CPU 会在空闲时对失效队列中的消息进行处理,将对应的 CPU 缓存失效。

写缓存区允许处理器将写操作暂存,不必等待数据真正写入主存或被其他处理器读取,从而导致写操作和随后的读操作在执行顺序上发生重排。
 

5.2 Invalid Queue导致缓存不一致问题
CPU1 将 X 的值设置为 1,并向 CPU2 发送失效请求。失效请求被放入 CPU2 的失效队列。
CPU1 立即读取 X 的值,由于缓存一致性协议,CPU1 会读取最新的值 1,因此 r1 = 1。
在 CPU2 处理失效请求之前,CPU2 直接读取 X,由于失效请求还在队列中尚未处理,CPU2 可能读取到缓存中的旧值 0,因此 r2 = 0。
尽管 CPU1 已经向 CPU2 发送了失效请求,但由于失效请求在 CPU2 的失效队列中尚未处理,CPU2 读取到了过期的数据,导致缓存不一致。

 

3 perf

https://zhuanlan.zhihu.com/p/674094948

 

posted on 2025-06-01 18:03  silyvin  阅读(15)  评论(0)    收藏  举报