ppt模板免费下载哪个网站好,河南商务网站建设,浙江网站,上海网站建设服务公司伪共享问题是多核处理器环境下常见的性能瓶颈之一#xff0c;特别是在多线程编程中。想要解决它#xff0c;就必须先了解缓存行的概念。
缓存行
缓存行是指在 CPU 缓存中最小的数据单位#xff0c;通常包含一定数量的字节#xff08;例如#xff0c;常见的缓存行大小为 …伪共享问题是多核处理器环境下常见的性能瓶颈之一特别是在多线程编程中。想要解决它就必须先了解缓存行的概念。
缓存行
缓存行是指在 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*856字节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) {ListLong 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% 以上的命中率。