剖析Disruptor:为什么会这么快?(二)神奇的缓存行填充
原文地址:http://ifeve.com/disruptor-padding/
作者:Trisha 译者:方腾飞 校对:丁一
我们经常提到一个短语Mechanical Sympathy,这个短语也是Martin博客的标题(译注:Martin Thompson),Mechanical Sympathy讲的是底层硬件是如何运作的,以及与其协作而非相悖的编程方式。
我在上一篇文章中提到RingBuffer后,我们收到一些关于RingBuffer中填充高速缓存行的评论和疑问。由于这个适合用漂亮的图片来说明,所以我想这是下一个我该解决的问题了。
(译注:Martin Thompson很喜欢用Mechanical Sympathy这个短语,这个短语源于赛车驾驶,它反映了驾驶员对于汽车有一种天生的感觉,所以他们对于如何最佳的驾御它非常有感觉。)
计算机入门
我喜欢在LMAX工作的原因之一是,在这里工作让我明白从大学和A Level Computing所学的东西实际上还是有意义的。做为一个开发者你可以逃避不去了解CPU,数据结构或者大O符号 —— 而我用了10年的职业生涯来忘记这些东西。但是现在看来,如果你知道这些知识并应用它,你能写出一些非常巧妙和非常快速的代码。
因此,对在学校学过的人是种复习,对未学过的人是个简单介绍。但是请注意,这篇文章包含了大量的过度简化。
CPU是你机器的心脏,最终由它来执行所有运算和程序。主内存(RAM)是你的数据(包括代码行)存放的地方。本文将忽略硬件驱动和网络之类的东西,因为Disruptor的目标是尽可能多的在内存中运行。
CPU和主内存之间有好几层缓存,因为即使直接访问主内存也是非常慢的。如果你正在多次对一块数据做相同的运算,那么在执行运算的时候把它加载到离CPU很近的地方就有意义了(比如一个循环计数-你不想每次循环都跑到主内存去取这个数据来增长它吧)。
越靠近CPU的缓存越快也越小。所以L1缓存很小但很快(译注:L1表示一级缓存),并且紧靠着在使用它的CPU内核。L2大一些,也慢一些,并且仍然只能被一个单独的 CPU 核使用。L3在现代多核机器中更普遍,仍然更大,更慢,并且被单个插槽上的所有 CPU 核共享。最后,你拥有一块主存,由全部插槽上的所有 CPU 核共享。
当CPU执行运算的时候,它先去L1查找所需的数据,再去L2,然后是L3,最后如果这些缓存中都没有,所需的数据就要去主内存拿。走得越远,运算耗费的时间就越长。所以如果你在做一些很频繁的事,你要确保数据在L1缓存中。
Martin和Mike的 QCon presentation演讲中给出了一些缓存未命中的消耗数据:
从CPU到 | 大约需要的 CPU 周期 | 大约需要的时间 |
主存 | 约60-80纳秒 | |
QPI 总线传输 (between sockets, not drawn) |
约20ns | |
L3 cache | 约40-45 cycles, | 约15ns |
L2 cache | 约10 cycles, | 约3ns |
L1 cache | 约3-4 cycles, | 约1ns |
寄存器 | 1 cycle |
如果你的目标是让端到端的延迟只有 10毫秒,而其中花80纳秒去主存拿一些未命中数据的过程将占很重的一块。
缓存行
现在需要注意一件有趣的事情,数据在缓存中不是以独立的项来存储的,如不是一个单独的变量,也不是一个单独的指针。缓存是由缓存行组成的,通常是64字节(译注:这篇文章发表时常用处理器的缓存行是64字节的,比较旧的处理器缓存行是32字节),并且它有效地引用主内存中的一块地址。一个Java的long类型是8字节,因此在一个缓存行中可以存8个long类型的变量。
非常奇妙的是如果你访问一个long数组,当数组中的一个值被加载到缓存中,它会额外加载另外7个。因此你能非常快地遍历这个数组。事实上,你可以非常快速的遍历在连续的内存块中分配的任意数据结构。我在第一篇关于ring buffer的文章中顺便提到过这个,它解释了我们的ring buffer使用数组的原因。
因此如果你数据结构中的项在内存中不是彼此相邻的(链表,我正在关注你呢),你将得不到免费缓存加载所带来的优势。并且在这些数据结构中的每一个项都可能会出现缓存未命中。
不过,所有这种免费加载有一个弊端。设想你的long类型的数据不是数组的一部分。设想它只是一个单独的变量。让我们称它为head
,这么称呼它其实没有什么原因。然后再设想在你的类中有另一个变量紧挨着它。让我们直接称它为tail
。现在,当你加载head
到缓存的时候,你也免费加载了tail
。
听想来不错。直到你意识到tail
正在被你的生产者写入,而head
正在被你的消费者写入。这两个变量实际上并不是密切相关的,而事实上却要被两个不同内核中运行的线程所使用。
设想你的消费者更新了head
的值。缓存中的值和内存中的值都被更新了,而其他所有存储head
的缓存行都会都会失效,因为其它缓存中head
不是最新值了。请记住我们必须以整个缓存行作为单位来处理(译注:这是CPU的实现所规定的,详细可参见深入分析Volatile的实现原理),不能只把head
标记为无效。
现在如果一些正在其他内核中运行的进程只是想读tail
的值,整个缓存行需要从主内存重新读取。那么一个和你的消费者无关的线程读一个和head
无关的值,它被缓存未命中给拖慢了。
当然如果两个独立的线程同时写两个不同的值会更糟。因为每次线程对缓存行进行写操作时,每个内核都要把另一个内核上的缓存块无效掉并重新读取里面的数据。你基本上是遇到两个线程之间的写冲突了,尽管它们写入的是不同的变量。
这叫作“伪共享”(译注:可以理解为错误的共享),因为每次你访问head
你也会得到tail
,而且每次你访问tail
,你也会得到head
。这一切都在后台发生,并且没有任何编译警告会告诉你,你正在写一个并发访问效率很低的代码。
解决方案-神奇的缓存行填充
你会看到Disruptor消除这个问题,至少对于缓存行大小是64字节或更少的处理器架构来说是这样的(译注:有可能处理器的缓存行是128字节,那么使用64字节填充还是会存在伪共享问题),通过增加补全来确保ring buffer的序列号不会和其他东西同时存在于一个缓存行中。
public long p1, p2, p3, p4, p5, p6, p7; // cache line paddingprivate volatile long cursor = INITIAL_CURSOR_VALUE;public long p8, p9, p10, p11, p12, p13, p14; // cache line padding
因此没有伪共享,就没有和其它任何变量的意外冲突,没有不必要的缓存未命中。
在你的Entry
类中也值得这样做,如果你有不同的消费者往不同的字段写入,你需要确保各个字段间不会出现伪共享。
修改:Martin写了一个从技术上来说更准确更详细的关于伪共享的文章,并且发布了性能测试结果。
原创文章,转载请注明: 转载自并发编程网 – ifeve.com本文链接地址: 剖析Disruptor:为什么会这么快?(二)神奇的缓存行填充
剖析Disruptor:为什么会这么快?(二)神奇的缓存行填充相关推荐
- java disruptor压测_Java并发框架Disruptor实现原理与源码分析(二) 缓存行填充与CAS操作...
##缓存行填充 关于缓存行填充在我个人的印象里面第一次看到是在Java的java.util.concurrent包中,因为当时很好奇其用法背后的逻辑,所以查了很多资料才明白到底是怎么回事*(也许事实上 ...
- 剖析Disruptor:为什么会这么快?(一)锁的缺点
原文:http://ifeve.com/disruptor-locks-are-bad/ 作者:Trisha's 译者:张文灼,潘曦 整理和校对:方腾飞,丁一 Martin Fowler写了一篇非 ...
- 从缓存行出发理解volatile变量、伪共享False sharing、disruptor
volatile关键字 当变量被某个线程A修改值之后,其它线程比如B若读取此变量的话,立刻可以看到原来线程A修改后的值 注:普通变量与volatile变量的区别是volatile的特殊规则保证了新值能 ...
- CPU/内存/缓存行/Disruptor
CPU/内存/缓存行/Disruptor:缓存分为多级缓存(L1/L2/L3...多级缓存)+主存mainMemory,内存中存储的是运行程序和所需数据, 不同级别的缓存大小分配不一样,而且反应速度不 ...
- @Contended / Disruptor 缓存行占满注解
目录 缓存行与伪共享 Disruptor 缓存行填充 @Contended 速度测试 存储设备往往是速度越快价格越昂贵,速度越快价格越低廉.在计算机中,CPU 的速度远高于主存的速度,而主存的速度又远 ...
- 剖析Elasticsearch集群系列之二:分布式的三个C、translog和Lucene段
2019独角兽企业重金招聘Python工程师标准>>> 剖析Elasticsearch集群系列之二:分布式的三个C.translog和Lucene段 博客分类: java 搜索引擎, ...
- 面试准备每日系列:计算机底层之并发编程(二)缓存行、一致性协议、伪共享、disruptor、CAS等待
文章目录 1. 缓存行 Cache line 2. 缓存一致性协议 & 伪共享 3. 为什么不加volatile? 4. 编程先可用再调优 5. disruptor & CAS等待 1 ...
- Spring Boot 揭秘与实战(二) 数据缓存篇 - Guava Cache
本文,讲解 Spring Boot 如何集成 Guava Cache,实现缓存. 博客地址:blog.720ui.com/ 在阅读「Spring Boot 揭秘与实战(二) 数据缓存篇 - 快速入门」 ...
- CPU的一、二、三级缓存
在Java并发编程中,我们经常会遇到共享变量的读写问题,关于这类问题我们经常会说到原子性.可见性.有序性这三大特性,再进一步会了解到总线和CPU的一.二.三级缓存.关于这三个级别的缓存网上文章介绍比较 ...
- Spring Boot 揭秘与实战(二) 数据缓存篇 - EhCache
文章目录 1. EhCache 集成 2. 源代码 本文,讲解 Spring Boot 如何集成 EhCache,实现缓存. 在阅读「Spring Boot 揭秘与实战(二) 数据缓存篇 - 快速入门 ...
最新文章
- 基于OpenCV实战:绘制图像轮廓(附代码)
- 饥荒海难机器人怎么用_饥荒海难机器人作用详解 机器人有什么用
- JAVA中循环删除list中元素的方法总结
- 为机柜鸣冤:数据中心被忽视的重要设备
- 无招胜有招之Java进阶JVM(一)
- Vue自定义属性的设置及获取
- 3个要点,教你设计好无限滚屏
- Flutter进阶—布局一个控件
- Java Web的分页工具类
- linux双机热备份
- 【网络】趣谈网络协议总结
- udp 消息转发 服务器,UDP消息发送
- 2021年前端会有什么新变化?
- iOS开发基础知识--碎片37
- 2020-2021 ACM-ICPC, Asia Seoul Regional Contest L. Two Buildings (决策单调性 分治)
- 鱼塘钓鱼 贪心算法
- 电池充电放电试验标准介绍
- 不可思议,作家王小波居然是国内第一代程序员!
- 邓宁-克鲁格效应_冒名顶替综合症和社区中的邓宁-克鲁格效应
- 哪项不是计算机联网检测系统的常见故障,计算机联网检测系统应用研究.docx
热门文章
- Docker生产环境配置——设置direct-lvm模式
- 【转】解决“你没有权限访问,请与网络管理员联系”
- 前台实现ajax 需注意的地方
- 11th Iran Nationwide Internet Contest 解题报告
- 送给创业的朋友:落入坑洞的猎人
- 物流管理系统【前台+后台】(Spring+SpringMVC+MyBatis+vue+shiro)(二)
- 软件需求分析学习笔记
- Python多人聊天室
- fedora20配置静态ip
- PACPerformance