什么是CPU缓存
在计算机系统中,CPU高速缓存是用于减少处理器访问内存所需平均时间的部件。在金字塔式存储体系中它位于自顶向下的第二层,仅次于CPU寄存器。其容量远小于内存,但速度却可以接近处理器的频率。
当处理器发出内存访问请求时,会先查看缓存内是否有请求数据。如果存在(命中),则不经访问内存直接返回该数据;如果不存在(失效),则要先把内存中的相应数据载入缓存,再将其返回处理器。
为什么需要CPU缓存
随着工艺的提升,最近几十年 CPU 的频率不断提升,而受制于制造工艺和成本限制,目前计算机的内存在访问速度上没有质的突破。因此,CPU 的处理速度和内存的访问速度差距越来越大,甚至可以达到上万倍。这种情况下传统的 CPU 直连内存的方式显然就会因为内存访问的等待,导致计算资源大量闲置,降低 CPU 整体吞吐量。同时又由于内存数据访问的热点集中性,在 CPU 和内存之间用较为快速而成本较高(相对于内存)的介质做一层缓存,就显得性价比极高了。
为什么需要有CPU多级缓存
大致可以得出结论,缓存层级越接近于 CPU core,容量越小,速度越快,当 CPU 执行运算的时候,它先去 L1 查找所需的数据,再去 L2,然后是 L3,最后如果这些缓存中都没有,所需的数据就要去主内存拿。走得越远,运算耗费的时间就越长。
什么是缓存行
缓存行 (Cache Line) 便是 CPU Cache 中的最小单位,CPU Cache 由若干缓存行组成,一个缓存行的大小通常是 64 字节(这取决于 CPU),并且它有效地引用主内存中的一块地址。一个 Java 的 long 类型是 8 字节,因此在一个缓存行中可以存 8 个 long 类型的变量。
猜一下下面代码的执行时间:
- public class ArrayLoop {
- public static void main(String[] args) {
- long[][] arr = new long[1024 * 1024][8];
- long sum = 0;
- //横向遍历
- long start = System.currentTimeMillis();
- for (int i = 0; i < 1024 *1024; i++) {
- for (int j = 0; j < 8; j++) {
- sum += arr[i][j];
- }
- }
- System.out.println("横向遍历耗时:" + (System.currentTimeMillis() - start) + "ms");
- //纵向遍历
- start = System.currentTimeMillis();
- for (int i = 0; i < 8; i++) {
- for (int j = 0; j < 1024 * 1024; j++) {
- sum += arr[j][i];
- }
- }
- System.out.println("纵向遍历耗时:" + (System.currentTimeMillis() - start) + "ms");
- }
- }
在我电脑上的执行时间为:
横向遍历耗时:32ms
纵向遍历耗时:88ms
在程序运行的过程中,缓存每次更新都从主内存中加载连续的 64 个字节。因此,如果访问一个 long 类型的数组时,当数组中的一个值被加载到缓存中时,另外 7 个元素也会被加载到缓存中。
什么是伪共享
如果多个线程的变量共享了同一个 CacheLine,任意一方的修改操作都会使得整个 CacheLine 失效(因为 CacheLine 是 CPU 缓存的最小单位),也就意味着,频繁的多线程操作,CPU 缓存将会彻底失效,降级为 CPU core 和主内存的直接交互。
如何避免伪共享
使用了字节填充技术(空间换时间)解决伪共享;
- public final class FalseSharing implements Runnable {
- public final static int NUM_THREADS = 4; // change
- public final static long ITERATIONS = 500L * 1000L * 1000L;
- private final int arrayIndex;
- private static VolatileLong[] longs = new VolatileLong[NUM_THREADS];
- static {
- for (int i = 0; i < longs.length; i++) {
- longs[i] = new VolatileLong();
- }
- }
- public FalseSharing(final int arrayIndex) {
- this.arrayIndex = arrayIndex;
- }
- public static void main(final String[] args) throws Exception {
- final long start = System.currentTimeMillis();
- runTest();
- System.out.println("duration = " + (System.currentTimeMillis() - start));
- }
- private static void runTest() throws InterruptedException {
- Thread[] threads = new Thread[NUM_THREADS];
- for (int i = 0; i < threads.length; i++) {
- threads[i] = new Thread(new FalseSharing(i));
- }
- for (Thread t : threads) {
- t.start();
- }
- for (Thread t : threads) {
- t.join();
- }
- }
- public void run() {
- long i = ITERATIONS + 1;
- while (0 != --i) {
- longs[arrayIndex].value = i;
- }
- }
- public final static class VolatileLong {
- public volatile long value = 0L;
- public long p1, p2, p3, p4, p5, p6; // 填充,可以注释后对比测试
- }
- }
Java8 中实现字节填充
- @Retention(RetentionPolicy.RUNTIME)
- @Target({ElementType.FIELD, ElementType.TYPE})
- public @interface Contended {
- String value() default "";
- }
注意需要同时开启 JVM 参数:-XX:-RestrictContended=false
@Contended 注解会增加目标实例大小,要谨慎使用。默认情况下,除了 JDK 内部的类,JVM 会忽略该注解。要应用代码支持的话,要设置 -XX:-RestrictContended=false,它默认为 true(意味仅限 JDK 内部的类使用)。当然,也有个 –XX: EnableContented 的配置参数,来控制开启和关闭该注解的功能,默认是 true,如果改为 false,可以减少 Thread 和 ConcurrentHashMap 类的大小。参加《Java性能权威指南》210 页。
ConcurrentHashMap 中,使用 @sun.misc.Contended 对静态内部类 CounterCell 进行修饰。另外还包括并发容器 Exchanger 也有相同的操作。
- /* ---------------- Counter support -------------- */
- /**
- * A padded cell for distributing counts. Adapted from LongAdder
- * and Striped64. See their internal docs for explanation.
- */
- @sun.misc.Contended static final class CounterCell {
- volatile long value;
- CounterCell(long x) { value = x; }
- }
Thread 线程类的源码中,使用 @sun.misc.Contended 对成员变量进行修饰。
- // The following three initially uninitialized fields are exclusively
- // managed by class java.util.concurrent.ThreadLocalRandom. These
- // fields are used to build the high-performance PRNGs in the
- // concurrent code, and we can not risk accidental false sharing.
- // Hence, the fields are isolated with @Contended.
- /** The current seed for a ThreadLocalRandom */
- @sun.misc.Contended("tlr")
- long threadLocalRandomSeed;
- /** Probe hash value; nonzero if threadLocalRandomSeed initialized */
- @sun.misc.Contended("tlr")
- int threadLocalRandomProbe;
- /** Secondary seed isolated from public ThreadLocalRandom sequence */
- @sun.misc.Contended("tlr")
- int threadLocalRandomSecondarySeed;
参考:
https://juejin.im/post/5c471d75e51d45299a08b333
https://juejin.im/post/5cd644886fb9a032136fe6d7
日前有消息称,AMD将会推出锐龙3000XT系列处理器,包括锐龙9 3900XT、锐龙7 3800...
如果DIY让你觉得头疼,说明你的思路是正确的。 本文转载自网络,原文链接:https...
网盘接二连三的倒闭关门,老司机们苦不堪言,先不说影响了大家的日常使用习惯,...
问题复现 1970-01-01对于开发者来说都是不陌生的,有些系统对于时间的处理如果不...
随着618购物狂欢进入最后的倒计时阶段,各大商家纷纷开启年中大促序幕。作为一家...
PC产业经历过的辉煌岁月在不少人心中记忆犹新,飞速发展的硬件产业、层出不穷的...
2021年了,DIY装机对很多人来说依然是个难题,难的不是钱多钱少,而是如何从琳琅...
关于CPU和程序的执行 CPU是计算机的大脑。 1、程序的运行过程,实际上是程序涉及...
如何性能白捡,或许是DIY玩家最大的乐子之一了。例如远古时期铅笔改CPU针脚让毒...
本文转载自微信公众号「人人都是极客」,作者布道师Peter。转载本文请联系人人都...