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

写在前面

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

电脑的缓存系统


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

对于CPU而言,只有第一、二、三级才是缓存区,主内存不是,如果需要到主内存读取数据,这种情况称为缓存未命中(cache miss)。

探索高速缓存的构造

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

从上图可以看到,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
寄存器寄存器

每一个开发人员都应该知道计算机硬件IO的延迟数传送门

源码参考:
https://github.com/justmine66/Disruptor/blob/master/tests/Disruptor.ConsoleTest/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;
}

总结

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

写在最后

如果有什么疑问和见解,欢迎评论区交流。
如果你觉得本篇文章对您有帮助的话,感谢您的【推荐】。
如果你对.NET高性能编程感兴趣的话可以【关注我】,我会定期的在博客分享我的学习心得。
欢迎转载,请在明显位置给出出处及链接

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/311880.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

python操作excel_使用Python操作Excel时必学的3个库

Python对Excel的操作我主要用xlwt、xlrd、xlutils这三个库。1、xlwt主要用来创建并写入数据到Excel。已经存在的表不可以写入。以下使用Python写九九乘法表到Excel运行之后&#xff0c;代码文件所在的文件夹会多出一个”九九乘法表“的Excel&#xff0c;内容如下图&#xff1a;…

线性代数---线性方程组

线性代数—线性方程组 常见题型的解题技巧 如果存在什么问题&#xff0c;欢迎批评指正&#xff01;谢谢&#xff01;

gRPC in ASP.NET Core 3.x -- Protocol Buffer, Go语言的例子(下)

前两篇文章半年前写的&#xff1a;gRPC in ASP.NET Core 3.0 -- Protocol Buffer&#xff08;1&#xff09;&#xff0c;gRPC in ASP.NET Core 3.0 -- Protocol Buffer&#xff08;2&#xff09;这是上一篇&#xff1a;gRPC in ASP.NET Core 3.x -- Protocol Buffer, Go语言的例…

得到选择框句柄 怎么操作_知道借名买房有风险,只能选择借名买房该怎么操作?...

文/楼市大家谈(quanadcom)正常情况下&#xff0c;购房者买房之时最看重房子的产权&#xff0c;因此无论是签订购房合同还是办理房产证时&#xff0c;最为在意的都是以谁的名义买房&#xff0c;房子究竟记在谁的名下&#xff0c;但是在有些情况下&#xff0c;一些人迫于无奈&…

数据结构---平衡二叉树

数据结构—平衡二叉树 原理&#xff1a;参考趣学数据结构 代码&#xff1a; #include<stdio.h> #include<stdlib.h> typedef struct avlTree {int data;struct avlTree *lchild, *rchild;int height; }avlTree;int height(avlTree* AVLTree);//声明avl树的深度 …

python list转换成array_一文掌握Python【不定期更新】

目录一、Numpy1 基本操作2 随机数3 打乱训练数据4 得到元素的最值5 拼接数组6 得到函数的信息7 得到累乘即各项相乘的结果8 判断一个数是否在数组中9 数组的变换10 排序11 元素的筛选12.保存文件/打开文件13.限制范围二、PIL1.安装2.PIL与Numpy的互相转化3.获取Image信息4.打开…

开源netcore前后端分离,前端服务端渲染方案

SPA单页面应用容器 开源地址&#xff1a; https://github.com/yuzd/Spa功能介绍前端应用开发完后打包后自助上传部署发布配合服务端脚本(javascript)实现服务端业务逻辑编写渲染SSR功能可以快速回滚到上一个版本可以设置环境变量供SSR功能使用服务端脚本提供执行日志 redis db三…

[蓝桥杯2016初赛]方格填数-next_permutation

代码如下&#xff1a; #include <iostream> #include <algorithm> using namespace std;int main() {int a[10] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};int cnt 0;do {if ((abs(a[0] - a[1]) ! 1) && (abs(a[1] - a[2]) ! 1) && (abs(a[3] - a[4]) ! …

word List 13

word List 13 如果存在什么问题&#xff0c;欢迎批评指正&#xff01;谢谢&#xff01;

ASP.NetCore+VUE 实现学生成绩管理系统(一)

周三陪伴是最长情的告白还有两天情人节&#xff1a;「无论是在家里&#xff0c;还是在工作&#xff0c;或者是在自我防护中&#xff0c;多给家人爱人发句平安&#xff0c;是最有心意、最重要的一件事。」♥感谢老李????近来一段时间一直没有学习新的东西&#xff0c;闲暇的…

下拉菜单实现树状结构_树形图:复杂层次结构的数据可视化

树形图&#xff1a;复杂层次结构的数据可视化作者&#xff1a;Page Laubheimer[1]树形图是一种复杂的&#xff0c;基于区域的数据可视化&#xff0c;用于复杂层次结构的数据&#xff0c;可能难以精确解释。在许多情况下&#xff0c;最好使用更简单的可视化效果&#xff08;例如…

[蓝桥杯2016决赛]路径之谜

题目描述 小明冒充X星球的骑士&#xff0c;进入了一个奇怪的城堡。城堡里边什么都没有&#xff0c;只有方形石头铺成的地面。 假设城堡地面是 n x n 个方格。 按习俗&#xff0c;骑士要从西北角走到东南角。可以横向或纵向移动&#xff0c;但不能斜着走&#xff0c;也不能跳跃…

数据结构---二叉平衡排序树的删除

数据结构—二叉平衡排序树的删除 原理&#xff1a;参考趣学数据结构 代码&#xff1a; #include<stdio.h> #include<stdlib.h> typedef struct avlTree {int data;struct avlTree *lchild, *rchild;int height; }avlTree;int height(avlTree* AVLTree);//声明av…

Magicodes.IE 2.0发布

Magicodes.IE是我们维护的开源的导入导出通用库&#xff0c;去年年底已加入NCC开源组织。Github地址&#xff1a;https://github.com/xin-lai/Magicodes.IEMagicodes.IE不是一蹴而就&#xff0c;而是根据实际需求不断迭代出来的&#xff0c;而且历经多次重构。这一次&#xff0…

《ASP.NET Core 微服务实战》-- 读书笔记(第11章)

第 11 章 开发实时应用和服务在本章&#xff0c;我们将讨论“实时”的准确含义&#xff0c;以及在大部分消费者看来应该属于这一范畴的应用类型接着&#xff0c;我们将探讨 WebSocket&#xff0c;并分析为什么传统的 WebSocket 与云环境完全不相适应&#xff0c;最后我们将构建…

word List 14

word List 14 如果存在什么问题&#xff0c;欢迎批评指正&#xff01;谢谢&#xff01;

python编程模式是什么_python 开发的三种运行模式详细介绍

Python 三种运行模式 Python作为一门脚本语言&#xff0c;使用的范围很广。有的同学用来算法开发&#xff0c;有的用来验证逻辑&#xff0c;还有的作为胶水语言&#xff0c;用它来粘合整个系统的流程。不管怎么说&#xff0c;怎么使用python既取决于你自己的业务场景&#xff0…

重磅!K8S 1.18版本将内置支持SideCar容器。

作者&#xff1a;justmine头条号&#xff1a;大数据与云原生微信公众号&#xff1a;大数据与云原生创作不易&#xff0c;在满足创作共用版权协议的基础上可以转载&#xff0c;但请以超链接形式注明出处。为了方便阅读&#xff0c;微信公众号已按分类排版&#xff0c;后续的文章…

word List 15

word List 15 如果存在什么问题&#xff0c;欢迎批评指正&#xff01;谢谢&#xff01;

.NET Core 如何验证信用卡卡号

_点击上方蓝字关注“汪宇杰博客”导语最近在家闲的蛋疼需要写点文章。正好我本人在金融科技公司工作&#xff0c;对信用卡业务略有了解。我们看看如何在 .NET Core 里验证一个信用卡的卡号是否合法。信用卡卡号组成首先&#xff0c;信用卡的卡号一般为16位&#xff0c;也有少许…