在现代多处理器架构中,每个 CPU 核心通常有自己的缓存(L1,L2 甚至 L3 cache),以减少对主内存(main memory)的访问延迟。处理器的缓存是以缓存行(cache line) 为单位来存储和传递数据的,常见的缓存行大小为 64 bytes。
当多个线程在多核处理器上并行执行时,如果两个或多个线程访问的不同变量存储在同一缓存行中,就可能发生 false sharing
。如果一个线程对这些变量中的一个进行写操作,它可能会导致其他线程的缓存行无效,从而迫使它们在读取变量时重新从主内存中加载整个缓存行。这种不必要的缓存无效化和缓存行传输会导致显著的性能下降,特别是当这些操作在紧密循环中反复发生时。
假共享问题在并行计算中尤为严重。当运行计算密集型或内存密集型应用程序时,访问内存的速度可能会成为程序性能的瓶颈。在这种情况下,由于缓存行无效化所导致的额外内存访问延迟可能会大大降低程序的吞吐量和响应时间。
在数据结构中添加额外的填充字节,以确保共享数据不会位于同一缓存行中。 如,在 C++ 中,可以使用对齐指定符来确保struct 和 class 的实例被对齐到缓存行边界:
struct alignas(64) PaddedCounter {
volatile long long value;
char padding[64 - sizeof(long long)];
};
在某些情况下,可以通过改变数据结构的布局来避免假共享。例如,如果有一个数组,其中的每个元素都经常被不同的线程访问,可以将这些元素分散到多个数组中,以减少或避免假共享。
一些现代编译器和运行时环境提供了避免假共享的高级特性。例如,Java 8 引入了sun.misc.Contended
注解来帮助避免假共享,但需要 JVM 启动时指定-XX:-RestrictContended
标志。
一些语言和库提供了设计用来避免假共享的专用并发数据结构,例如 Java 中的java.util.concurrent
包含了一些设计用来减少并发访问开销的数据结构。
STM 是一种同步机制,它可以帮助开发者写出无锁的并发代码。STM 系统可以自动检测内存访问冲突,并在运行时尝试减少由此产生的开销,包括假共享引起的。
使用性能分析工具(如 Intel vtune, Linux perf 或其他平台特定的工具)可以帮助识别程序中的假共享问题。这些工具可以显示 high cache miss 的代码区域,可能是假共享导致的。
- 访问模式重构:如果可能,重构代码以改变线程对数据的访问模式,使得每个线程都在其自己的数据集上工作,从而减少访问共享缓存行的机会。
- 数据局部性优化:尽可能地增加数据的局部性。经常一起使用的数据在内存中放在一起。
- 避免频繁的写操作:减少线程写操作的频率也可以减少假共享的影响,因为缓存失效通常是由写操作引起的。
- 使用最新的语言和库特性:像 C++11 及更高版本提供的原子操作库和内存模型,可以帮助开发者更有效地编写并发程序,并减少假共享问题。 避免和解决假共享问题是提升并发程序性能的关键。需要开发者在写并发代码时考虑处理器的缓存设计,并采取策略来减少不必要的缓存行无效化。这可能包括对数据进行适当的对齐和填充,遵循编写无锁数据结构的最佳实践,以及使用高级同步机制和性能分析工具。