CPU缓存伪共享

midoll 284 2024-02-02

CPU缓存伪共享(False Sharing)

是指多个处理器核心在并行计算时错误地认为它们需要访问相同的数据,而这实际上导致了不必要的数据传输和性能下降。这种现象发生在多线程应用程序中,当多个线程修改相互独立的变量却恰好位于同一个缓存行(Cache Line)内时。

现代CPU使用缓存来减少访问主内存的时间。缓存通常被组织成多个层级(L1、L2、L3等),每个层级又被分割成称为“缓存行”的小块,缓存行是缓存和内存之间传输数据的基本单位,通常大小是64字节。

当一个核心需要读取或写入一个变量时,它会加载包含该变量的整个缓存行到它的本地缓存中。如果另一个核心修改了同一个缓存行中的另一个变量,即使两个变量是完全独立的,第一个核心的缓存行就会失效,因为缓存一致性协议会保证所有核心看到的是一致的内存状态。失效的缓存行必须从内存中重新加载,即使大部分数据可能没有发生变化。

伪共享可以显著降低并行程序的性能,因为它增加了缓存同步的开销,导致频繁的无效缓存行和不必要的数据传输。

为了避免伪共享,可以采取以下措施:

  • 将经常一起使用的数据放在同一个缓存行中。
  • 将独立使用的数据分散到不同的缓存行中。
  • 使用填充(Padding)技术,即在数据结构中添加额外的空间,以确保不同的线程操作的数据位于不同的缓存行。
  • 利用编译器提供的优化选项或特定的语言构造来告知编译器关于数据对齐和布局的信息。

理解和优化伪共享问题是高性能计算和并行编程中的一个重要方面。

java里面如何实现将独立使用的数据分散到不同的缓存行中

在Java中,要将独立使用的数据分散到不同的缓存行中,可以借助填充(Padding)技术来减少或避免伪共享。这通常涉及到在数据结构中人为地增加一些未使用的变量,以确保频繁修改的变量不会位于同一个缓存行上。

由于大多数现代CPU的缓存行大小是64字节,你可以通过添加额外的长整型(long)字段来作为填充,因为一个long占用8字节。如果你有一个类,它的实例会被多个线程频繁访问,可以这样做:

public class PaddedCounter {
    private volatile long counter = 0;

    // 填充字段, 一个long类型是8字节,这里添加7个long类型字段,
    // 加上counter自身占用的8字节,总共占用64字节,即一个缓存行的大小。
    private long p1, p2, p3, p4, p5, p6, p7; 

    public long getCounter() {
        return counter;
    }

    public void increment() {
        counter++;
    }
}

在这个例子中,我们添加了七个未使用的long变量p1到p7作为填充,以防止counter与其他变量共享缓存行。这样,当counter被一个核心修改时,不太可能影响到其他核心的缓存行,因为counter周围的填充保证了它占据了完整的缓存行。

从Java 8开始,可以使用@Contended注解来自动实现这种填充。这个注解可以帮助开发者标记那些可能会受到伪共享影响的字段。但是注意,使用@Contended注解默认是禁用的,需要在JVM启动时通过添加-XX:-RestrictContended参数来启用。

使用@Contended的例子:

import sun.misc.Contended;

public class ContendedCounter {
    @Contended
    private volatile long counter = 0;

    public long getCounter() {
        return counter;
    }

    public void increment() {
        counter++;
    }
}

在这个例子中,@Contended注解告诉JVM,counter字段可能会受到伪共享的影响,并且JVM应该尝试将其放置在一个单独的缓存行中。

请注意,sun.misc.Contended是一个内部API,它在Java 9及之后的版本中@Contended注解被移到了jdk.internal.vm.annotation包下
使用时报: java: 程序包 jdk.internal.vm.annotation 不存在
使用IDEA的Alt+Enter即可修复此问题
相当于在Setting - Build,Execution,Deployment - Compiler - Java Compiler - Override compiler parameters per-module中增加了如下设置:

--add-exports=java.base/jdk.internal.vm.annotation=ALL-UNNAMED 
--add-exports=java.base/sun.net=ALL-UNNAMED 
--add-exports=java.base/sun.net.util=ALL-UNNAMED 
--add-exports=java.base/jdk.internal.misc=ALL-UNNAMED 
--add-exports=java.base/sun.net.www=ALL-UNNAMED

image-1706855179372


# java # cpu