当前位置: 首页 > news >正文

伪共享问题如何解决?

伪共享问题是多核处理器环境下常见的性能瓶颈之一,特别是在多线程编程中。想要解决它,就必须先了解缓存行的概念。

缓存行

缓存行是指在 CPU 缓存中最小的数据单位,通常包含一定数量的字节(例如,常见的缓存行大小为 64 字节)。当处理器从主内存读取数据时,它不仅读取请求的字节,还会读取周围的数据形成一个缓存行,以减少未来的内存访问次数。这样做的好处是可以减少内存访问的频率,提高缓存命中率,从而提升性能。

缓存行对齐

缓存行对齐是指将数据的存储位置调整为缓存行大小的整数倍,以便CPU可以一次加载整个数据。

为什么需要缓存行对齐?

当CPU访问主存中的数据时,它通常会加载整个缓存行到缓存中。如果数据没有正确地对齐到缓存行边界,那么CPU可能需要多次访问主存以获取完整的数据。这被称为缓存行分裂(Cache Line Splitting),它会导致额外的性能开销。通过缓存行对齐,我们可以减少缓存行分裂的发生,从而提高程序的性能。

如何在JVM中应用缓存行对齐?

在JVM中,我们可以通过调整对象的布局来实现缓存行对齐。以下是一些建议:

  • 避免在对象内部跨越缓存行边界的访问:如果对象的字段跨越了缓存行边界,那么访问这些字段可能会导致缓存行分裂。为了避免这种情况,我们应该确保对象的所有字段都位于同一个缓存行内。
  • 使用填充(Padding):我们可以使用特定类型的字段(如long或double)作为填充,以确保对象的大小是缓存行大小的整数倍。这样,无论对象如何排列,其边界都将与缓存行边界对齐。
    示例见结尾。
  • 考虑对象的访问模式:当多个对象被频繁地一起访问时,我们应该考虑将这些对象排列在一起,以便它们可以一次性加载到缓存中。
  • 使用太阳花(Sunflower)策略:这是一种对象布局策略,通过将对象分组到不同的“太阳花”中,可以确保在同一时间访问的对象位于不同的缓存行中,从而减少缓存行争用。
  • 使用数组:将多个变量存储在一个数组中,每个变量占据数组中的一个元素,这样它们自然分布在不同的缓存行中。
    class ThreadSafeData {volatile long[] variables = new long[2]; // 假设缓存行大小为 64 字节,每个 long 占 8 字节// 线程 1 访问 variables[0]// 线程 2 访问 variables[1]
    }
    

使用填充技术的示例:

package org.hbin;import java.util.ArrayList;
import java.util.List;/*** @author Haley* @version 1.0* 2024/8/21*/
public class CacheLineWithPadding {private static long count = 10000_0000L;public static class Data {// 使用7个long类型填充,占用7*8=56字节public volatile long p1, p2, p3, p4, p5, p6, p7;public volatile long value; // 占用8字节
//        public volatile long p8, p9, p10, p11, p12, p13, p14;}public static Data[] array = {new Data(), new Data()};public static long test() {Thread t1 = new Thread(() -> {for (long i = 0; i < count; i++) {array[0].value = i;}});Thread t2 = new Thread(() -> {for (long i = 0; i < count; i++) {array[1].value = i;}});long start = System.currentTimeMillis();try {t1.start();t2.start();t1.join();t2.join();} catch (Exception e) {e.printStackTrace();}return System.currentTimeMillis() - start;}public static void main(String[] args) {List<Long> list = new ArrayList<>();for (int i = 0; i < 10; i++) {list.add(test());}System.out.println(list);System.out.println(list.stream().mapToLong(Long::longValue).average().orElse(0.0));}
}

运行上面的代码,观察输出时间;注释上述代码16行public volatile long p1, p2, p3, p4, p5, p6, p7;,两次运行代码,你可以看到运行时间明显增加了。正是因为有这一行中声明的7个long类型的数据填充,使得缓存更加高效。

放开18行注释掉的代码public volatile long p8, p9, p10, p11, p12, p13, p14;,运行程序并将结果和当前进行对比,性能应该也会略有提升的哟!你知道原因么?

jdk8的解决方式

jdk1.8中,官方已经提供了对伪共享的解决办法,那就是sun.misc.Contended注解,有了这个注解解决伪共享就变得简单多了。

@sun.misc.Contended
public static class Data {public volatile long value;
}

默认情况下此注解是无效的,需要在JVM启动时开启这个注解。
设置方式:-XX:-RestrictContended

伪共享问题

伪共享(False Sharing)是指多线程程序中,多个线程访问不同变量,但这些变量位于同一个缓存行中。当一个线程更新这个缓存行中的一个变量时,会导致整个缓存行被刷新到主内存中,从而可能导致其他线程读取该缓存行时需要从主内存中重新加载,造成不必要的性能损失。
Disruptor项目的经典示例图演示了伪共享的问题,如下:
在这里插入图片描述
上图中,一个运行在处理器 core1上的线程想要更新变量 X 的值,同时另外一个运行在处理器 core2 上的线程想要更新变量 Y 的值。但是,这两个频繁改动的变量都处于同一条缓存行。
根据MESI协议(不清楚的小伙伴请自行参考我写的另一篇文章,详细介绍了MESI协议),两个线程就会轮番发送 RFO 消息,占得此缓存行的拥有权。当 core1 取得了拥有权开始更新 X,则 core2 对应的缓存行需要设为 I 状态。当 core2 取得了拥有权开始更新 Y,则 core1 对应的缓存行需要设为 I 状态。轮番夺取拥有权不但带来大量的 RFO 消息,而且如果某个线程需要读此行数据时,L1 和 L2 缓存上都是失效数据,只有 L3 缓存上是同步好的数据。要知道,读 L3 的数据非常影响性能。更坏的情况是跨槽读取,L3 都要 miss,只能从内存上加载。
表面上 X 和 Y 都是被独立线程操作的,而且两操作之间也没有任何关系。只不过它们共享了一个缓存行,但所有竞争冲突都是来源于共享。

伪共享的解决方案

在实际的生产开发过程中,我们一定要通过缓存行填充去解决掉潜在的伪共享问题吗?
其实并不一定。
首先伪共享是很隐蔽的,我们暂时无法从系统层面上通过工具来探测伪共享事件。其次,不同类型的计算机具有不同的微架构(如 32 位系统和 64 位系统的 java 对象所占自己数就不一样),如果涉及到跨平台的应用,那就更难以把握了,一个确切的填充方案只适用于一个特定的操作系统。
其次,缓存的资源是有限的,如果填充会浪费珍贵的 cache 资源,并不适合大范围应用。最后,目前主流的 Intel 微架构 CPU 的 L1 缓存,已能够达到 80% 以上的命中率。

http://www.lryc.cn/news/429503.html

相关文章:

  • 基于web框架的协同过滤的美食推荐系统【数据爬虫、管理系统、数据可更新、样式可调整】
  • Eureka中的多实例配置:如何处理微服务实例动态扩展与缩减
  • Ubuntu 22.04使用 IPTables 配置防火墙
  • Java语言程序设计——篇十三(1)
  • GB/T 5023.3-2008额定电压450/750V及以下聚氯乙烯绝缘电缆
  • 深入单例模式
  • MongoDB 单机和集群环境部署教程
  • 【学习笔记】Day 20
  • StringBuffer与StringBuilder 2024-8-21 22-13
  • 会声会影剪辑视频收费吗,会声会影最新破解版
  • 在Windows11强制开启copilot
  • 基于Java的开源CMS有哪些推荐,各自特点是什么
  • IPC进程通信以及网络通信
  • 【脏数据 bug 解决】ValueError: mean must have 1 elements if it is an iterable, got 3
  • 【Vue3】集成 Ant Design Vue
  • 如何处理前端项目中的SEO优化:从SPA到SSR与SSG
  • 【UE5】Groom毛发系统的基本使用——给小白人添加头发
  • DataWorks函数
  • 设计模式学习优质网站分享:refactoring.guru
  • JVM-Java的四种引用
  • 探索《黑神话:悟空》品质保障的背后:ISO体系认证
  • ArcGIS Pro 实现人口分布栅格TIFF数据的网格提取与可视化
  • select的缺点;poll ;poll的缺点;epoll
  • keli5_报错 Cannot Load Device Description问题
  • 算法的学习笔记—把二叉树打印成多行(牛客JZ78)
  • FreeRTOS 时间管理
  • F. Valuable Cards D. Smithing Skill
  • 【电子通识】IPC-A-600中对验收标准的定义
  • MyBatis(初阶)
  • KDP数据平台:以实战案例验证技术领先力