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

写在前面

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

电脑的缓存系统

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

探索高速缓存的构造

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

示例

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

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

1. 一般的实现方式

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

代码

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

单线程

640?wx_fmt=png
平均响应时间 = 1508.56 毫秒。

双线程

640?wx_fmt=png
平均响应时间 = 4460.40 毫秒。

三线程

640?wx_fmt=png
平均响应时间 = 7719.02 毫秒。

四线程

640?wx_fmt=png
平均响应时间 = 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。

单线程

640?wx_fmt=png
平均响应时间 = 1516.33 毫秒。

双线程

640?wx_fmt=png
平均响应时间 = 1529.97 毫秒。

三线程

640?wx_fmt=png
平均响应时间 = 1563.65 毫秒。

四线程

640?wx_fmt=png
平均响应时间 = 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;} }

单线程

640?wx_fmt=png
平均响应时间 = 2008.12 毫秒。

双线程

640?wx_fmt=png
平均响应时间 = 2046.33 毫秒。

三线程

640?wx_fmt=png
平均响应时间 = 2081.75 毫秒。

四线程

640?wx_fmt=png
平均响应时间 = 2163.092 毫秒。

3. 统计分析

640?wx_fmt=png
上面的图表已经一目了然了吧,一般实现方式的持续时间随线程数呈线性增长,多线程下表现的非常糟糕,而通过直接、内存布局方式填充了数据后,响应时间与线程数的多少没有无关,达到了真正的低延迟。其中直接填充数据的方式,效率最高,内存布局方式填充次之,在四线程的情况下,一般实现方式持续时间为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

640?wx_fmt=jpeg

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

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

相关文章

RMQ问题:与众不同(st表的高端应用)

解析 预处理 用pre[i]表示以i结尾的最长完美序列起始点&#xff0c;用last[i]表示数字i最后出现的位置 那么可以得到递推式&#xff1a; pre[i]max(pre[i-1],last[x[i]]1);也就是说这个pre要么是受前一位一样的限制&#xff0c;要么是受自己的限制 用f[i]表示以i结尾的最长完…

codeforces1554 E. You(思维+数学+转化)

E. You 首先我们假设每个节点有个点权&#xff0c;点权是儿子的数量。 一个点可以和他的父亲所要权值1&#xff08;先删去自己&#xff0c;再删去父亲&#xff09;&#xff0c;并且此操作不可逆。于是dfs从叶子节点一次递推即可得出某个kkk是否能作为gcd⁡\gcdgcd。 由于上述…

牛客题霸 [括号生成] C++题解/答案

牛客题霸 [括号生成] C题解/答案 题意&#xff1a; 给出n对括号&#xff0c;请编写一个函数来生成所有的由n对括号组成的合法组合。 例如&#xff0c;给出n3&#xff0c;解集为&#xff1a; “((()))”, “(()())”, “(())()”, “()()()”, “()(())”, 题解&#xff1a; …

P5404-[CTS2019]重复【KMP,dp】

正题 题目链接:https://www.luogu.com.cn/problem/P5404 题目大意 给出一个字符串SSS&#xff0c;然后求有多少个长度为mmm的串TTT满足。无限多个串TTT拼接起来后能找出一个长度和SSS相等的子串字典序比SSS小。 1≤∣S∣,m≤20001\leq |S|,m\leq 20001≤∣S∣,m≤2000 解题思…

Docker最全教程——从理论到实战(四)

容器是应用走向云端之后必然的发展趋势&#xff0c;因此笔者非常乐于和大家分享我们这段时间对容器的理解、心得和实践。本篇教程持续编写了2个星期左右并且一直在完善、补充具体的细节和实践&#xff0c;预计全部完成需要1到2个月的时间。由于编写的过程中极其费时&#xff0c…

模板:高精

想必大家一定都深受高精所害 python党&#xff1a;啥叫高精&#xff1f; 这里送上一种高精板子 写法 转载自巨神wyx的博客 #include <bits/stdc.h> using namespace std;const int maxn 1000;struct bign {int d[maxn], len;void clean() {while (len > 1 &&…

牛客题霸 [最长公共子序列] C++题解/答案

牛客题霸 [最长公共子序列] C题解/答案 题目描述 给定两个字符串str1和str2&#xff0c;输出连个字符串的最长公共子序列。如过最长公共子序列为空&#xff0c;则输出-1。 题解&#xff1a; dp经典问题 代码&#xff1a; class Solution { public:/*** longest common su…

2021牛客暑期多校训练营4 G-Product(组合意义+容斥原理)

G-Product Spy_Savior题解 Ultraman-Ace题解 组合意义容斥原理 转化后题目关键要求有DnkDnkDnk个小球放在nnn个盒子中&#xff0c;每个盒子至少有k个小球的方案数。 题目的等价式子为 ∑∑i1naiDnk[ai≥k]D!∏i1nai!\sum_{\sum_{i1}^na_iDnk[a_i\ge k]}\frac{D!}{\prod_{i1}^…

P6122-[NEERC2016]Mole Tunnels【模拟费用流】

正题 题目链接:https://www.luogu.com.cn/problem/P6122 题目大意 给出nnn个点的一棵满二叉树&#xff0c;每个点有容量cic_ici​&#xff0c;mmm次从pip_ipi​处加一只仓鼠然后求每只仓鼠都到一个点的最短路径长度和。 1≤n≤1051\leq n\leq 10^51≤n≤105 解题思路 模拟费…

牛客题霸 [矩阵查找] C++题解/答案

牛客题霸 [矩阵查找] C题解/答案 题目描述 请写出一个高效的在m*n矩阵中判断目标值是否存在的算法&#xff0c;矩阵具有如下特征&#xff1a; 每一行的数字都从左到右排序 每一行的第一个数字都比上一行最后一个数字大 例如&#xff1a; 对于下面的矩阵&#xff1a; [ [1, 3,…

AtCoder Beginner Contest 211 E - Red Polyomino(暴力+状态记录)

E - Red Polyomino 暴力&#xff0c;对于状态的记录考虑下面用set<vector<string>> mp #include<bits/stdc.h> using namespace std; using lllong long; template <class Tint> T rd() {T res0;T fg1;char chgetchar();while(!isdigit(ch)) {if(ch-…

CF960G-Bandit Blues【第一类斯特林数,分治,NTT】

正题 题目链接:https://www.luogu.com.cn/problem/CF960G 题目大意 求有多少个长度为nnn的排列&#xff0c;使得有AAA个前缀最大值和BBB个后缀最大值。 0≤n,A,B≤1050\leq n,A,B\leq 10^50≤n,A,B≤105 解题思路 显然的是把最大的数两边然后左边的是前缀最大值&#xff0c;…

动态规划:openjudge 2.6-3532 最大上升子序列和 解题心得

传送门 题目描述 一个数的序列bi&#xff0c;当b1 < b2 < … < bS的时候&#xff0c;我们称这个序列是上升的。对于给定的一个序列(a1, a2, …,aN)&#xff0c;我们可以得到一些上升的子序列(ai1, ai2, …, aiK)&#xff0c;这里1 < i1 < i2 < … < iK…

边缘化搭建 DotNet Core 2.1 自动化发布和部署(下)

写在前面本篇文章是上一篇边缘化搭建 DotNet Core 2.1 自动化发布和部署(上)的后续操作&#xff0c;本文主要讲解如何开启Docker Remote API&#xff0c;开启Remote API后的权限安全问题。配置Jenkins构建项目&#xff0c;并在云服务器上构建成功。废话不多说&#xff0c;我们一…

牛客题霸 [二进制中1的个数] C++题解/答案

牛客题霸 [二进制中1的个数] C题解/答案 题目描述 输入一个整数&#xff0c;输出该数32位二进制表示中1的个数。其中负数用补码表示。 题解&#xff1a; 判断1的个数 x&(-x)2^k 有点类似于树状数组中lowbit的操作 代码&#xff1a; class Solution { public:int Num…

2021牛客暑期多校训练营5 G-Greater Integer, Better LCM(暴力+子集dp)

G-Greater Integer, Better LCM 看到校大佬的代码就瞬间会做了。 当时和队友想的是&#xff0c;首先转化题意即找两个数x≥a,y≥b[lcm(a,b)c]x\ge a,y\ge b[\text{lcm}(a,b)c]x≥a,y≥b[lcm(a,b)c]&#xff0c;首先不考虑x≥a,y≥bx\ge a,y\ge bx≥a,y≥b的限制&#xff0c;显…

P2179-[NOI2012]骑行川藏【导数,二分】

正题 题目链接:https://www.luogu.com.cn/problem/P2179 题目大意 给出EEE和nnn个si,ki,uis_i,k_i,u_isi​,ki​,ui​求一个序列viv_ivi​满足 ∑i1nkisi(vi−ui)2≤E\sum_{i1}^nk_is_i(v_i-u_i)^2\leq Ei1∑n​ki​si​(vi​−ui​)2≤E 的情况下最小化 ∑i1nsivi\sum_{i1}^…

不止代码:乘法游戏 题解(区间dp)

题目描述 乘法游戏是在一行牌上进行的。每一张牌包括了一个正整数。在每一个移动中&#xff0c;玩家拿出一张牌&#xff0c;得分是用它的数字乘以它左边和右边的数&#xff0c;所以不允许拿第1张和最后1张牌。最后一次移动后&#xff0c;这里只剩下两张牌。 你的目标是使得分的…

边缘化搭建DotNet Core 2.1 自动化构建和部署环境(上)

写在前面写这篇文章的缘由是由于笔者的对新兴技术方向有所追求&#xff0c;但个人资产有限&#xff0c;只能容许购买一台阿里云低配1核2G服务器。服务器上搭建了 Centos7 & Docker & Jenkins & ASP.NET Core 2.0 自动化发布和部署 环境后牺牲了大部分性能。造成了一…

牛客题霸 [丑数] C++题解/答案

牛客题霸 [丑数] C题解/答案 题目描述 把只包含质因子2、3和5的数称作丑数&#xff08;Ugly Number&#xff09;。例如6、8都是丑数&#xff0c;但14不是&#xff0c;因为它包含质因子7。 习惯上我们把1当做是第一个丑数。求按从小到大的顺序的第N个丑数。 题解&#xff1a;…