原文链接:http://mechanitis.blogspot.com/2011/07/dissecting-disruptor-why-its-so-fast_22.html 需翻墙

计算机入门

     我喜欢在LMAX工作的原因之一是,在这里工作让我明白从大学和A Level Computing所学的东西实际上还是有意义的。做为一个开发者你可以逃避不去了解CPU,数据结构或者大O符号 —— 而我用了10年的职业生涯来忘记这些东西。但是现在看来,如果你知道这些知识并应用它,你能写出一些非常巧妙和非常快速的代码。
 
     因此,对在学校学过的人是种复习,对未学过的人是个简单介绍。但是请注意,这篇文章包含了大量的过度简化。
 
     CPU是你机器的心脏,最终由它来执行所有运算和程序。主内存(RAM)是你的数据(包括代码行)存放的地方。本文将忽略硬件驱动和网络之类的东西,因为Disruptor的目标是尽可能多的在内存中运行。
 
     CPU和主内存之间有好几层缓存,因为即使直接访问主内存也是非常慢的。如果你正在多次对一块数据做相同的运算,那么在执行运算的时候把它加载到离CPU很近的地方就有意义了(比如一个循环计数-你不想每次循环都跑到主内存去取这个数据来增长它吧)。

     越靠近CPU的缓存越快也越小。所以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位处理器),并且它有效地引用主内存中的一块地址。一个Java的long类型是8字节,因此在一个缓存行中可以存8个long类型的变量。(此处忽略了多级缓存)
 
 
 
     奇妙的是:如果你访问一个long数组,当数组中的一个值被加载到缓存中,它会额外加载另外7个,因此你能非常快地遍历这个数组。事实上,遍历在内存中连续分配的任意数据结构都是非常快的,因为存在这样的机制。
 
     因此如果你数据结构中的项在内存中不是彼此相邻的(比如说:链表),你将得不到免费缓存加载所带来的优势。并且在这些数据结构中的每一个项都可能会出现缓存未命中。
 
     不过,这种免费加载存在弊端。设想:你的long类型的数据不是数组的一部分,它只是一个单独的变量,暂时称它为head;同时你的类中有另一个变量紧挨着它,暂时称它为tail。现在,当你加载head到缓存的时候,你也免费加载了tail
 

     现在看起来还不错。直到生产者要将生产的内容放到tail,此时head中保存的内容也正被消费者(译注:和生产者不在同一个内核中)消费。这两个变量并无直接联系,但需要被这两个线程使用,且这两个线程运行在不同的内核(译注:这里是指物理上的内核,即多核CPU)中。
 
     设想某一时刻消费者更新了head的值。缓存中的值和内存中的值都被更新了,而其他所有存储head的缓存行都会都会失效,因为其它缓存中head不是最新值了。而我们必须以整个缓存行作为单位来处理,不能只把head标记为无效。
 

     假如此时生产者进程要访问tail中的内容,就导致整个缓存行需要从主内存重新读取,因为缓存未命中。一个和消费者无关的进程(生产者),想要访问一个和head无关的数据(tail),却被意外拖慢了速度。
 
     如果两个独立的线程同时写这两个值会更糟。因为每次线程对缓存行进行写操作时,每个内核都要把另一个内核上的缓存块无效掉并重新读取里面的数据(译注:这里假设每个线程独占一个内核)。尽管它们写入的是不同的变量,但几乎等于两个线程之间的写冲突。
 
     这叫作“伪共享”(False sharing),因为每次你访问head你也会得到tail,而且每次你访问tail,你也会得到head。这一切都在后台发生,并且没有任何编译警告会告诉你,你正在写一个并发访问效率很低的代码。

解决方案-神奇的缓存行填充

     Disruptor采用了缓存行填充的方法,来消除这个问题。这种做法适用于64字节(或更小)的处理器架构,通过增加补全来确保ring buffer的序列号不会和其他数据同时存在于一个缓存行中。
 
public long p1, p2, p3, p4, p5, p6, p7; // cache line padding
private volatile long cursor = INITIAL_CURSOR_VALUE;
public long p8, p9, p10, p11, p12, p13, p14; // cache line padding

(译注:前后各七位填充字段,保证cursor[1]在缓冲行中任意位置,其周围都有足够的填充字段)

 
     因此没有伪共享,就没有和其它任何变量的意外冲突,没有不必要的缓存未命中。 在你的Entry类中也值得这样做,如果你有不同的消费者往不同的字段写入,你需要确保各个字段间不会出现伪共享。
 
 
译者注:
[1]不同于传统队列的head、tail、size variables定义的队列,Disruptor对外只有一个变量,那就是队尾元素的下标,Disruptor称其为cursor。 
 

剖析Disruptor:为什么会这么快?(二)神奇的缓存行填充的更多相关文章

  1. 从缓存行出发理解volatile变量、伪共享False sharing、disruptor

    volatilekeyword 当变量被某个线程A改动值之后.其他线程比方B若读取此变量的话,立马能够看到原来线程A改动后的值 注:普通变量与volatile变量的差别是volatile的特殊规则保证 ...

  2. Spring Boot 揭秘与实战(二) 数据缓存篇 - Guava Cache

    文章目录 1. Guava Cache 集成 2. 个性化配置 3. 源代码 本文,讲解 Spring Boot 如何集成 Guava Cache,实现缓存. 在阅读「Spring Boot 揭秘与实 ...

  3. 二、Memcached缓存穿透、缓存雪崩

    二.Memcached缓存穿透.缓存雪崩 1. 缓存雪崩 可能是数据魏加载到缓存中,或者缓存同一时间大面积失效,导致大量请求去数据库查询的过程,数据库过载,崩溃. 解决方法: 1 采用加锁计数,使用合 ...

  4. 使用本地缓存快还是使用redis缓存好?

    使用本地缓存快还是使用redis缓存好? Redis早已家喻户晓,其性能自不必多说. 但是总有些时候,我们想把性能再提升一点,想着redis是个远程服务,性能也许不够,于是想用本地缓存试试!想法是不错 ...

  5. 一.rest-framework之版本控制 二、Django缓存 三、跨域问题 四、drf分页器 五、响应器 六、url控制器

    一.rest-framework之版本控制 1.作用 用于版本的控制 2.内置的版本控制 from rest_framework.versioning import QueryParameterVer ...

  6. {MySQL的库、表的详细操作}一 库操作 二 表操作 三 行操作

    MySQL的库.表的详细操作 MySQL数据库 本节目录 一 库操作 二 表操作 三 行操作 一 库操作 1.创建数据库 1.1 语法 CREATE DATABASE 数据库名 charset utf ...

  7. Spring Boot 揭秘与实战(二) 数据缓存篇 - Redis Cache

    文章目录 1. Redis Cache 集成 2. 源代码 本文,讲解 Spring Boot 如何集成 Redis Cache,实现缓存. 在阅读「Spring Boot 揭秘与实战(二) 数据缓存 ...

  8. Spring Boot 揭秘与实战(二) 数据缓存篇 - EhCache

    文章目录 1. EhCache 集成 2. 源代码 本文,讲解 Spring Boot 如何集成 EhCache,实现缓存. 在阅读「Spring Boot 揭秘与实战(二) 数据缓存篇 - 快速入门 ...

  9. Spring Boot 揭秘与实战(二) 数据缓存篇 - 快速入门

    文章目录 1. 声明式缓存 2. Spring Boot默认集成CacheManager 3. 默认的 ConcurrenMapCacheManager 4. 实战演练5. 扩展阅读 4.1. Mav ...

随机推荐

  1. javascript url 相关函数(escape/encodeURL/encodeURIComponent区别)

    JS获取url参数及url编码.解码 完整的URL由这几个部分构成:scheme://host:port/path?query#fragment ,各部分的取法如下: window.location. ...

  2. 【Java编程进阶-1】enum枚举的使用

    枚举主要用于枚举常量,下面举个简单的应用. 比如一个公司有如下几个部门: 研发部: 销售部: 财务部: (其他部门暂时不列举) 部门的某些信息相对固定,此时可以考虑使用枚举来说明: 枚举类 Depts ...

  3. VS合集/6.0/2005/2008/2010/2012/2013 绿色版精简版

    VS合集/6.0/2005/2008/2010/2012/2013 绿色版精简版 找到这里的都是老司机,别的不多说了 链接: http://pan.baidu.com/s/1i5IyYZb       ...

  4. ThreadPoolExecutor使用介绍

    private static ExecutorService exec = new ThreadPoolExecutor(8, 8, 0L,TimeUnit.MILLISECONDS, new Lin ...

  5. uva 725 Division(暴力模拟)

    Division 紫书入门级别的暴力,可我还是写了好长时间 = = [题目链接]uva 725 [题目类型]化简暴力 &题解: 首先要看懂题意,他的意思也就是0~9都只出现一遍,在这2个5位数 ...

  6. php之面向对象

    <?php declare(encoding='UTF-8'); class Site{ /*成员变量*/ var $url; var $title = "gunduzi" ...

  7. js复习(一)

    一.常用数据框1.alert(""):警告对话框,作用是弹出一个警告对话框 2.confirm(""):确定对话框,弹出一个带确定和取消按钮的对话框--确定返回 ...

  8. linux常用命令 3

    示例定义的 mytest或者test 用户 mygroup 用户组 cat /etc/group 查看组 groupname:x:groupId:其他成员 组名:x(加密):组ID:组成员cat /e ...

  9. C++学习3

    C++仍然在使用C语言的 char.int.long 等基本数据类型,它们在现代操作系统(Windows XP.Win7.Win10 等)中的长度如下表所示: longlong是C99新增的一种数据类 ...

  10. spring注解注入

    @Autowired public void setUserDAO(UserDAO userDAO) { this.userDAO = userDAO; } @Test public void tes ...