写在前面

好久没有写博客了,一直在不断地探索响应式DDD,又get到了很多新知识,解惑了很多老问题,最近读了Martin Fowler大师一篇非常精彩的博客The LMAX Architecture,里面有一个术语Mechanical Sympathy,姑且翻译成软硬件协同编程(Hardware and software working together in harmony),很有感悟,说的是要把编程与底层硬件协同起来,这样对于开发低延迟、高并发的系统特别地重要,为什么呢,今天我们就来讲讲CPU的高速缓存。

电脑的缓存系统


电脑的缓存系统分了很多层级,从外到内依次是主内存、三级高速缓存、二级高速缓存、一次高速缓存,所以,在我们的脑海里,觉点磁盘的读写速度是很慢的,而内存的读写速度确是快速的,的确如此,从上图磁盘和内存距离CPU的远近距离就看出来。这里先说明一个概念,主内存被所有CPU共享;三级缓存被同一个插槽内的CPU所共享;单个CPU独享自己的一级、二级缓存,即高速缓存。CPU是真正做事情的地方,它会先从高速缓存中去获取所需的数据,如果找不到,再去三级缓存中查找,如果还是找不到最终就去会主内存查找,如果每一次都这样来来回回地取数据,那么无疑是非常耗时。如果能够把数据缓存到高速缓存中就好了,这样不仅CPU第一次就可以直接从高速缓存中命中数据,而且每个CPU都独占自己的高速缓存,多线程下也不存在临界资源的问题,这才是真正的低延迟,但是这个地方对我们而言根本不透明,肿么办?

探索高速缓存的构造

我们先来看一张使用鲁大师检测的处理器信息截图,如下:

从上图可以看到,CPU高速缓存(一、二级)的存储单元为Line,大小为64 bytes,也就是说无论我们的数据大小是多少,高速缓存都是以64 bytes为单位缓存数据,比如一个8位的long类型数组,即使只有第一位有数据,每次高速缓存加载数据的时候,都会顺带把后面7位数据也一起加载(因为数组内元素的内存地址是连续的),这就是底层硬件CPU的工作机制,所以我们要利用这个天然的优势,让数据独占整个缓存行,这样CPU命中的缓存行中就一定有我们的数据。

示例

使用不同的线程数,对一个long类型的数值计数500亿次。

备注:统计分析图表和总结在最后。

1. 一般的实现方式

大多数程序员都会这样子构造数据,老铁没毛病。

代码

///// <summary>///// CPU伪共享高速缓存行条目(伪共享)///// </summary>public class FalseSharingCacheLineEntry{    public long Value = 0L;
}

单线程


平均响应时间 = 1508.56 毫秒。

双线程


平均响应时间 = 4460.40 毫秒。

三线程


平均响应时间 = 7719.02 毫秒。

四线程


平均响应时间 = 10404.30 毫秒。

2. 独占缓存行,直接命中高速缓存。

2.1 直接填充

代码

/// <summary>/// CPU高速缓存行条目(直接填充)/// </summary>public class CacheLineEntry{       protected long P1, P2, P3, P4, P5, P6, P7;     public long Value = 0L;        protected long P9, P10, P11, P12, P13, P14, P15;
}

为了保证高速缓存行中一定有我们的数据,所以前后都填充7个long。

单线程


平均响应时间 = 1516.33 毫秒。

双线程


平均响应时间 = 1529.97 毫秒。

三线程


平均响应时间 = 1563.65 毫秒。

四线程


平均响应时间 = 1616.12 毫秒。

2.2 内存布局填充

作为一个C#程序员,必须写出优雅的代码,可以使用StructLayout、FieldOffset来控制class、struct的内存布局。

备注:就是上面直接填充的优雅实现方式而已。

代码

/// <summary>/// CPU高速缓存行条目(控制内存布局)/// </summary>[StructLayout(LayoutKind.Explicit, Size = 120)]public class CacheLineEntryOne{[FieldOffset(56)]    private long _value;    public long Value{        get => _value;        set => _value = value;}
}

单线程


平均响应时间 = 2008.12 毫秒。

双线程


平均响应时间 = 2046.33 毫秒。

三线程


平均响应时间 = 2081.75 毫秒。

四线程


平均响应时间 = 2163.092 毫秒。

3. 统计分析


上面的图表已经一目了然了吧,一般实现方式的持续时间随线程数呈线性增长,多线程下表现的非常糟糕,而通过直接、内存布局方式填充了数据后,响应时间与线程数的多少没有无关,达到了真正的低延迟。其中直接填充数据的方式,效率最高,内存布局方式填充次之,在四线程的情况下,一般实现方式持续时间为10.4秒多,直接填充数据的方式为1.6秒,内存布局填充方式为2.2秒,延迟还是比较明显,为什么会有这么大的差距呢?

刨根问底

在C#下,一个long类型占8 byte,对于一般的实现方式,在多线程的情况下,隶属于每个独立线程的数据会共用同一个缓存行,所以只要有一个线程更新了缓存行的数据,那么整个缓存行就自动失效,这样就导致CPU永远无法直接从高速缓存中命中数据,每次都要经过一、二、三级缓存到主内存中重新获取数据,时间就是被浪费在了这样的来来回回中。而对数据进行填充后,隶属于每个独立线程的数据不仅被缓存到了CPU的高速缓存中,而且每个数据都独占整个缓存行,其他的线程更新数据,并不会导致自己的缓存行失效,所以每次CPU都可以直接命中,不管是单线程也好,还是多线程也好,只要线程数小于等于CPU的核数都和单线程一样的快速,正如我们经常在一些性能测试软件,都会看到的建议,线程数最好小于等于CPU核数,最多为CPU核数的两倍,这样压测的结果才是比较准确的,现在明白了吧。

最后来看一下大师们总结的未命中缓存的测试结果

从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
寄存器 寄存器

源码参考:
https://github.com/justmine66/MDA/blob/master/tests/MDA.Test.Disruptor/FalseSharingTest.cs

延伸阅读

Magic cache line padding
The LMAX Architecture

补充

感谢@ firstrose同学主动测试后的提醒,大家应该向他学习,带着疑惑看博客,不明白的自己动手测试。对于内存布局填充方式,去掉属性后,经过测试性能与直接填充方式几乎无差别了,不过本示例代码仅仅作为一个测试参考,主要目的是给大家布道如何利用CPU高速缓存工作机制,通过缓存行的填充来避免假共享,从而写出真正低延迟的代码。

/// <summary>/// CPU高速缓存行条目(控制内存布局)/// </summary>[StructLayout(LayoutKind.Explicit, Size = 120)]public class CacheLineEntryOne{[FieldOffset(56)]    public long Value;
}

总结

编写单、多线程下表现都相同的代码,历来都是非常困难的,需要不断地从深度、广度上积累知识,学无止境,无痴迷,不成功,希望大家能有所收获。

原文地址:https://www.cnblogs.com/justmine/p/9696160.html


.NET社区新闻,深度好文,欢迎访问公众号文章汇总 http://www.csharpkit.com

高并发、低延迟之C#玩转CPU高速缓存(附示例)相关推荐

  1. 软硬件协同编程 - C#玩转CPU高速缓存(附示例)

    写在前面 好久没有写博客了,一直在不断地探索响应式DDD,又get到了很多新知识,解惑了很多老问题,最近读了Martin Fowler大师一篇非常精彩的博客The LMAX Architecture, ...

  2. TPL Dataflow组件应对高并发,低延迟要求

    长话短说 2C互联网业务增长,单机多核的共享内存模式带来的排障问题.编程困难:随着多核时代和分布式系统的到来,共享模型已经不太适合并发编程,因此actor-based模型又重新受到了人们的重视. -- ...

  3. 高吞吐低延迟Java应用的垃圾回收优化

    高吞吐低延迟Java应用的垃圾回收优化 高性能应用构成了现代网络的支柱.LinkedIn有许多内部高吞吐量服务来满足每秒数千次的用户请求.要优化用户体验,低延迟地响应这些请求非常重要. 比如说,用户经 ...

  4. kafka是如何做到百万级高并发低迟延的?

    Kafka是高吞吐低延迟的高并发.高性能的消息中间件,在大数据领域有极为广泛的运用.配置良好的Kafka集群甚至可以做到每秒几十万.上百万的超高并发写入.Kafka到底是如何做到这么高的吞吐量和性能的 ...

  5. 你们说说kafka是如何做到百万级高并发低迟延的?

    Kafka是高吞吐低延迟的高并发.高性能的消息中间件,在大数据领域有极为广泛的运用.配置良好的Kafka集群甚至可以做到每秒几十万.上百万的超高并发写入.Kafka到底是如何做到这么高的吞吐量和性能的 ...

  6. Neutrino追问AMA第15期|Celer 创始人董沫博士:链下扩容更容易做高互动低延迟的应用

    在3月7日 Neutrino 追问 AMA 第15期交流中,我们邀请到远在美国的Celer Network 创始人 董沫博士与 Neutrino 中国社区用户进行了一场关于<链下扩容能为区块链带 ...

  7. 喜欢玩游戏的发烧友不要错过了,五款高续航低延迟蓝牙耳机为你量身打造

    现在手机成为电子游戏的主流平台,<王者荣耀>.<刺激战场>等一波电竞手游的出现,把电竞游戏推向全民化.无论水平如何,只要有一台不是配置太烂的智能手机,就能随时刷上一局. 而玩游 ...

  8. 有什么适合玩手游的蓝牙耳机?高音质低延迟吃鸡王者轻松驾驭

    目前大家对蓝牙耳机的要求越来越高了,而且手游的大肆风靡,让本来关注度不高的游戏蓝牙耳机逐渐开始受到了热捧,厂家更新产品的周期缩短.品类增多更能说明这个问题.而目前火热的电竞行业更是对这些外设产品起到了 ...

  9. 喜欢玩游戏怎么能少了这几款蓝牙耳机?高音质低延迟手游必备神器

    由于游戏耳机更注重游戏中声音的精准定位以及提供真实的临场感,注定了其在音乐的还原度和表现力上表现平平.那么游戏耳机真的就无法兼顾游戏和音乐?这可不一定.很多品牌蓝牙耳机都能做到音质与性能兼顾了,所以关 ...

最新文章

  1. 中服公司企业信息化的ERP系统选择
  2. python range函数与numpy arange函数,xrange与range的区别
  3. 用Schema来决定CSV文件的字段类型
  4. 《剑指offer》— JavaScript(6)旋转数组的最小数字
  5. oracle的parse是什么意思,Oracle性能测量体系(Parse Time)
  6. 开源 非开源_在开源中吃我们自己的狗粮
  7. 本田da屏怎么进wince系统_本田新XR-V首试:配置提升,依然好开
  8. amos看拟合度在哪里看_amos模型拟合度
  9. Win7 vs2010+Silverlight4开发安装顺序
  10. BeX5企业快速开发平台-前端快速开发框架
  11. linux bzip2压缩文件,linux bzip2命令压缩或解压缩bzip2文件
  12. java类加载机制之类加载过程、类加载器及双亲委派模型详解
  13. 苹果屏幕镜像_苹果自带的屏幕镜像还能这样用,一秒小屏变大屏,网友:钱没白花...
  14. 选择一个网络托管业务域名代备案提供全面的解决方案
  15. deepin php与nginx,深度Deepin20安装Nginx1.19+Php7.3+H5ai实践指南
  16. 使用 React-Sketchapp
  17. 周期函数的自身叠加后功率函数思考
  18. 系统分析与控制_多智能体协同控制研究中各定位系统分析
  19. 凌晨3点不回家,你不要老婆孩子了?
  20. 迄今4代大数引擎概述

热门文章

  1. Microsoft POS for .NET v1.12 发布了
  2. [转]ArcGIS.Server.9.3和ArcGIS API for Flex实现Toolbar功能(四)
  3. Oracle结构设计技巧(访问数据库象访问内存一样 快)
  4. 自定义EventSource(一)EventCounter
  5. 为啥 .NET 自带的 JsonSerializer 无法序列化 Field ?
  6. 用啥Selenium?! .NET程序员就用自家的Playwright for .NET
  7. 2021年春招Elasticsearch面试题
  8. 记一次CPU持续100%及分析方法
  9. 【.Net core】EFCore——Code First生成数据库与表
  10. 高效掌握新技能的「树型思维」