热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

计算机组成无锁编程追求极致性能

前言​现代计算机通常由CPU,以及主板、内存、硬盘等主要硬件结构组成,而决定计算机性能的最核心部件是CPU+内存,CPU负责处理程序指令,内存负责存储指令执行结果。在这个工作机制当

前言

​ 现代计算机通常由CPU,以及主板、内存、硬盘等主要硬件结构组成,而决定计算机性能的最核心部件是CPU+内存,CPU负责处理程序指令,内存负责存储指令执行结果。在这个工作机制当中CPU的读写效率其实是远远高于内存的,为提升执行效率减少CPU与内存的交互,一般在CPU上设计了缓存结构,常见的为三级缓存结构:

  • L1 Cache,分为数据缓存和指令缓存,逻辑核独占

  • L2 Cache,物理核独占,逻辑核共享

  • L3 Cache,所有物理核共享

下图为CPU-Core(TM)I7-10510U型号缓存结构

计算机组成-无锁编程追求极致性能

存储器存储空间大小:内存>L3>L2>L1>寄存器。

存储器速度快慢排序:寄存器>L1>L2>L3>内存。

缓存行大小


[root@192 ~]# getconf -a|grep CACHE
LEVEL1_ICACHE_SIZE                 32768 #L1缓存大小
LEVEL1_ICACHE_ASSOC                8 #L1缓存行大小
LEVEL1_ICACHE_LINESIZE             64
LEVEL1_DCACHE_SIZE                 32768
LEVEL1_DCACHE_ASSOC                8
LEVEL1_DCACHE_LINESIZE             64
LEVEL2_CACHE_SIZE                  262144 #L2缓存大小
LEVEL2_CACHE_ASSOC                 4
LEVEL2_CACHE_LINESIZE              64 #L2缓存行大小
LEVEL3_CACHE_SIZE                  8388608 #L3缓存大小
LEVEL3_CACHE_ASSOC                 16
LEVEL3_CACHE_LINESIZE              64 #L3缓存行大小
LEVEL4_CACHE_SIZE                  0
LEVEL4_CACHE_ASSOC                 0
LEVEL4_CACHE_LINESIZE              0
[root@192 ~]# cat /proc/cpuinfo |grep -i cache
cache size	: 8192 KB
cache_alignment	: 64
cache size	: 8192 KB
cache_alignment	: 64

JAVA程序毫无疑问也必须是运行在硬件机器之上,如何利用底层硬件工作原理,提升性能也必然是我们需要考虑的,笔者今天以无锁并发高性能框架Disruptor为例分析如何高效的利用CPU缓存。

Who is Disruptor?

​ Disruptor是一个开源框架,研发的初衷是为了解决高并发下队列锁的问题,最早由LMAX(一种新型零售金融交易平台)提出并使用,能够在无锁的情况下实现队列的并发操作,并号称能够在一个线程里每秒处理6百万笔订单。

缓存行填充

下方示例为Disruptor框架的内部代码:

abstract class RingBufferPad
{
    protected long p1, p2, p3, p4, p5, p6, p7;
}

分析:

  1. 变量p1~p7本身没有实际意义,只能用于缓存行填充,为了尽可能地用上CPU Cache
  2. 访问CPU里的L1 Cache或者L2 Cache、L3 Cache,访问延时是内存的1/15乃至1/100(内存的访问速度,是远远慢于CPU Cache的)
    • 因此,为了追求极限性能,需要尽可能地从CPU Cache里面读取数据
  3. CPU Cache装载内存里面的数据,不是一个个字段加载的,而是加载一整个缓存行
    • 64位的Intel CPU,缓存行通常是64 Bytes,一个long类型的数据需要8 Bytes,因此会一下子加载8个long类型的数据
  • 计算机组成-无锁编程追求极致性能
    • 遍历数组元素速度很快,后面连续7次的数据访问都会命中CPU Cache,不需要重新从内存里面去读取数据

缓存行失效

p1-p7仅用来填充缓存行,我们跟本用不到它,但是我们为什么要填充满一个缓存行呢?

  1. CPU在加载数据的时候,会把这个数据从内存加载到CPU Cache里面

  2. 此时,CPU Cache里面除了这个数据,还会加载这个数据前后定义的其他变量

    • 释义:在高并发场景下,假定并发访问变量p0,在p0后定义的其它变量也一并会被缓存load
  3. Disruptor是一个多线程的服务器框架,在这个数据前后定义的其他变量,可能会被多个不同的线程去更新数据,读取数据

    • 这些写入和读取请求,可能会来自于不同的CPU Core
    • 为了保证数据的同步更新,不得不把CPU Cache里面的数据,重新写回到内存里面或者重新从内存里面加载
    • CPU Cache的写回加载,都是以整个Cache Line作为单位的
  4. 如果常量的缓存失效,当再次读取这个值的时候,需要重新从内存读取,读取速度会大大变慢

计算机组成-无锁编程追求极致性能

缓存行填充

abstract class RingBufferPad
{
    protected long p1, p2, p3, p4, p5, p6, p7;
}

abstract class RingBufferFields extends RingBufferPad
{
    ...
    private final long indexMask;
    private final Object[] entries;
    protected final int bufferSize;
    protected final Sequencer sequencer;
    ...
}

public final class RingBuffer extends RingBufferFields implements Cursored, EventSequencer, EventSink
{
    ...
    protected long p1, p2, p3, p4, p5, p6, p7;
    ...
}
  1. Disruptor在RingBufferFields里面定义的变量前后分别定义了7个long类型的变量
    • 前面7个继承RingBufferPad,后面7个直接定义RingBuffer类中
    • 这14个变量没有任何实际用途,既不会去,也不会去
  2. RingBufferFields里面定义的变量都是final的,第一次写入之后就不会再进行修改
    • 一旦被加载到CPU Cache之后,只要被频繁地读取访问,就不会被换出CPU Cache
    • 无论在内存的什么位置,这些变量所在的Cache Line都不会有任何写更新的请求

计算机组成-无锁编程追求极致性能

空间局部性+分支预测

  1. Disruptor整个框架是一个高速的生产者-消费者模型下的队列
    • 生产者不停地往队列里面生产新的需要处理的任务
    • 消费者不停地从队列里面处理掉这些任务
  2. 要实现一个队列,最合适的数据结构应该是链表,如Java中的LinkedBlockingQueue
  3. Disruptor并没有使用LinkedBlockingQueue,而是使用了RingBuffer的数据结构
    • RingBuffer的底层实现是一个固定长度的数组
    • 比起链表形式的实现,数组的数据在内存里面会存在空间局部性
      • 数组的连续多个元素会一并加载到CPU Cache里面,所以访问遍历的速度会更快
      • 链表里面的各个节点的数据,多半不会出现在相邻的内存空间
    • 数据的遍历访问还有一个很大的优势,就是CPU层面的分支预测会很准确
      • 可以更有效地利用CPU里面的多级流水线

计算机组成-无锁编程追求极致性能

计算机组成-无锁编程追求极致性能

CAS无锁

锁对性能的影响

  1. Disruptor作为一个高性能的生产者-消费者队列系统,一个核心的设计:通过RingBuffer实现一个无锁队列
  2. Java里面的LinkedBlockingQueue,比起Disruptor的RingBuffer要慢很多,主要原因
    • 链表的数据在内存里面的布局对于高速缓存不友好
    • LinkedBlockingQueue对于锁的依赖
      • 一般来说消费者比生产者快(不然队列会堆积),因为大部分时候,队列是的,生产者和消费者一样会产生竞争
  3. LinkedBlockingQueue的锁机制是通过ReentrantLock,需要JVM进行裁决
    • 锁的争夺,会把没有拿到锁的线程挂起等待,也需要进行一次上下文切换
    • 上下文切换的过程,需要把当前执行线程的寄存器等信息,保存到内存中的线程栈里面
      • 意味:已经加载到高速缓存里面的指令或者数据,又回到主内存里面,进一步拖慢性能

计算机组成-无锁编程追求极致性能

RingBuffer 无锁方案

  1. 加锁很慢,所以Disruptor的解决方案是无锁(没有操作系统层面的锁)
  2. Disruptor利用了一个CPU硬件支持的指令,称之为CAS(Compare And Swap)
  3. Disruptor的RingBuffer创建一个Sequence对象,用来指向当前的RingBuffer的头和尾
    • 头和尾的标识,不是通过一个指针来实现的,而是通过一个序号
  4. RingBuffer在进行生产者和消费者之间的资源协调,采用的是对比序号的方式
    • 当生产者想要往队列里面加入新数据的时候,会把当前生产者的Sequence的序号,加上需要加入的新数据的数量
    • 然后和实际的消费者所在的位置进行对比,看下队列里是不是有足够的空间加入这些数据
      • 而不是直接覆盖掉消费者还没处理完的数据
  5. CAS指令,既不是基础库里的一个函数,也不是操作系统里面实现的一个系统调用,而是一个CPU硬件支持的机器指令
    • 在Intel CPU上,为cmpxchg指令:compxchg [ax] (隐式参数,EAX累加器), [bx] (源操作数地址), [cx] (目标操作数地址)
    • 第一个操作数不在指令里面出现,是一个隐式的操作数,即EAX累加寄存器里面的值
    • 第二个操作数就是源操作数,指令会对比这个操作数和上面EAX累加寄存器里面的值
    • 伪代码:IF [ax]== [bx] THEN [ZF] = 1, [bx] = [cx] ELSE [ZF] = 0, [ax] = [bx]
    • 单个指令是原子的,意味着使用CAS操作的时候,不需要单独进行加锁,直接调用即可

计算机组成-无锁编程追求极致性能

Sequence关键代码如下:

public long addAndGet(final long increment)
{
    long currentValue;
    long newValue;

    // 如果CAS操作没有成功,会不断等待重试
    do
    {
        currentValue = get();
        newValue = currentValue + increment;
    }
    while (!compareAndSet(currentValue, newValue));

    return newValue;
}

public boolean compareAndSet(final long expectedValue, final long newValue)
{
    // 调用CAS指令
    return UNSAFE.compareAndSwapLong(this, VALUE_OFFSET, expectedValue, newValue);
}

Benchmark

互斥锁竞争、CAS乐观锁与无锁测试:

public class LockBenchmark {

    private static final long MAX = 500_000_000L;

    private static void runIncrement() {
        long counter = 0;
        long start = System.currentTimeMillis();
        while (counter 

** 结论:无锁性能要远高于cas与lock,cas要大于lock**

更多好文章,请关注公众号:奇客时间,原创JAVA架构技术栈社区


推荐阅读
  • 本文深入探讨了Linux内核中进程地址空间的设计与实现,包括虚拟地址空间的概念、内存描述符`mm_struct`的作用、内核线程与用户进程的区别、进程地址空间的分配方法、虚拟内存区域(VMA)的结构以及地址空间与页表之间的映射机制。 ... [详细]
  • 本文详细介绍了在PHP中如何获取和处理HTTP头部信息,包括通过cURL获取请求头信息、使用header函数发送响应头以及获取客户端HTTP头部的方法。同时,还探讨了PHP中$_SERVER变量的使用,以获取客户端和服务器的相关信息。 ... [详细]
  • 本文概述了在GNU/Linux系统中,动态库在链接和运行阶段的搜索路径及其指定方法,包括通过编译时参数、环境变量及系统配置文件等方式来控制动态库的查找路径。 ... [详细]
  • Java高级工程师学习路径及面试准备指南
    本文基于一位朋友的PDF面试经验整理,涵盖了Java高级工程师所需掌握的核心知识点,包括数据结构与算法、计算机网络、数据库、操作系统等多个方面,并提供了详细的参考资料和学习建议。 ... [详细]
  • 本文探讨了如何在PHP与MySQL环境中实现高效的分页查询,包括基本的分页实现、性能优化技巧以及高级的分页策略。 ... [详细]
  • 函子(Functor)是函数式编程中的一个重要概念,它不仅是一个特殊的容器,还提供了一种优雅的方式来处理值和函数。本文将详细介绍函子的基本概念及其在函数式编程中的应用,包括如何通过函子控制副作用、处理异常以及进行异步操作。 ... [详细]
  • 在使用 Nginx 作为服务器时,发现 Chrome 能正确从缓存中读取 CSS 和 JS 文件,而 Firefox 却无法有效利用缓存,导致加载速度显著变慢。 ... [详细]
  • 本文探讨了如何通过Service Locator模式来简化和优化在B/S架构中的服务命名访问,特别是对于需要频繁访问的服务,如JNDI和XMLNS。该模式通过缓存机制减少了重复查找的成本,并提供了对多种服务的统一访问接口。 ... [详细]
  • 深入理解:AJAX学习指南
    本文详细探讨了AJAX的基本概念、工作原理及其在现代Web开发中的应用,旨在为初学者提供全面的学习资料。 ... [详细]
  • 本文详细介绍了如何利用 Bootstrap Table 实现数据展示与操作,包括数据加载、表格配置及前后端交互等关键步骤。 ... [详细]
  • 实践指南:使用Express、Create React App与MongoDB搭建React开发环境
    本文详细介绍了如何利用Express、Create React App和MongoDB构建一个高效的React应用开发环境,旨在为开发者提供一套完整的解决方案,包括环境搭建、数据模拟及前后端交互。 ... [详细]
  • 本文详细介绍了HTTP协议中的缓存机制,包括ETag的使用方法和304状态码的意义,探讨了强缓存与协商缓存的区别及其工作原理,旨在帮助开发者更好地理解和优化网站性能。 ... [详细]
  • 探索OpenWrt中的LuCI框架
    本文深入探讨了OpenWrt系统中轻量级HTTP服务器uhttpd的工作原理及其配置,重点介绍了LuCI界面的实现机制。 ... [详细]
  • 本文探讨了一个Web工程项目的需求,即允许用户随时添加定时任务,并通过Quartz框架实现这些任务的自动化调度。文章将介绍如何设计任务表以存储任务信息和执行周期,以及如何通过一个定期扫描机制自动识别并加载新任务到调度系统中。 ... [详细]
  • 使用jQuery与百度地图API实现地址转经纬度功能
    本文详细介绍了如何利用jQuery和百度地图API将地址转换为经纬度,包括申请API密钥、页面构建及核心代码实现。 ... [详细]
author-avatar
花儿在绽放2502857073
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有