素手烹茶|探讨缓存行与伪共享

最近项目中有个需求 , 需要用到有界队列对访问请求量进行流量削峰请求 , 同时作为一个缓冲层对请求处理进行后续处理 , Java 内置有界队列 ArrayBlockingQueue 可以满足这方面的需求 , 但是性能上并不满足 , 于是使用了 Disruptor , 它是英国外汇交易公司 LMAX 开发的一个高性能队列 , 了解到它内部解决伪共享问题 , 今天就和大家一起学习缓存行与伪共享相关的知识 。
缓存行(Cache line)对计算机组成原理相对熟悉的小伙伴都知道 , CPU 的速度比内存的速度高了几个数量级 , 为了 CPU 更快从内存中读取数据 , 设置了多级缓存机制 , 如下图所示:
素手烹茶|探讨缓存行与伪共享当 CPU 运算时 , 首先会从 L1 缓存查找所需要的数据 , 如果没有找到 , 再去 L2 缓存中去找 , 以此类推 , 直到从内存中获取数据 , 这也就意味着 , 越长的调用链 , 所耗费的执行时间也越长 。 那是不是可以从主内存拿数据的时候 , 顺便多拿一些呢?这样就可以避免频繁从主内存中获取数据了 。 聪明的计算机科学家已经想到了这个法子 , 这就是缓存行的由来 。 缓存是由多个缓存行组成的 , 而每个缓存行大小通常来说 , 大小为 64 字节 , 并且每个缓存行有效地引用主内存中的一块儿地址 , CPU 每次从主内存中获取数据时 , 会将相邻的数据也一同拉取到缓存行中 , 这样当 CPU 执行运算时 , 就大大减少了与主内存的交互 。
下面我用一个例子让大家体会一下用缓存行和不用缓存行在性能上的差异:
// 以下源码例子来源:public class CacheLineEffect {//考虑一般缓存行大小是64字节 , 一个 long 类型占8字节static long[][] arr;public static void main(String[] args) {int size = 1024 * 1024;arr = new long[size][];for (int i = 0; i < size; i++) {arr[i] = new long[8];for (int j = 0; j < 8; j++) {arr[i][j] = 0L;}}long sum = 0L;long marked = System.currentTimeMillis();for (int i = 0; i < size; i++) {for (int j = 0; j < 8; j++) {sum = arr[i][j];}}System.out.println("[cache line]Loop times:" + (System.currentTimeMillis() - marked) + "ms");marked = System.currentTimeMillis();for (int i = 0; i < 8; i += 1) {for (int j = 0; j < size; j++) {sum = arr[j][i];}}System.out.println("[no cache line]Loop times:" + (System.currentTimeMillis() - marked) + "ms");}}我使用的测试运行环境配置如下:
素手烹茶|探讨缓存行与伪共享运行后结果如下:
素手烹茶|探讨缓存行与伪共享可以看到 , 使用缓存行比没有使用缓存行的性能提升了将近 4 倍 。
伪共享问题当 CPU 执行完后 , 还需要将数据回写到内存上 , 以便于别的线程可以从主内存中获取最新的数据 。 假设两个线程都加载了相同的 Cache line 数据 , 会产生什么样的影响呢?下面我用一张图解释:
素手烹茶|探讨缓存行与伪共享数据 A、B、C 被加载到同一个 Cache line , 假设线程 1 在 core1 中修改 A , 线程 2 在 core2 中修改 B 。
线程 1 首先对 A 进行修改 , 这时 core1 会告知其它 CPU 核 , 当前引用同一地址的 Cache line 已经无效 , 随后 core2 发起修改 B , 会导致 core1 将数据回写到主内存中 , core2 这时会重新从主内存中读取该 Cache line 数据 。


推荐阅读