您好!欢迎来到爱源码

爱源码

热门搜索: 抖音快手短视频下载   

双面字节:小伙子,什么是伪分享?如何避免? [免费源码]

  • 时间:2022-06-12 02:14 编辑: 来源: 阅读:332
  • 扫一扫,手机访问
摘要:双面字节:小伙子,什么是伪分享?如何避免? [免费源码]
周末有读者告诉我,有人问他“什么是伪分享?”如何避免虚假分享的问题?这其实是一个考察CPU缓存的问题,我之前的图形系统里也提到过。 今天,我再告诉你一遍。 CPU如何读写数据?我们先来了解一下CPU的架构。只有了解了CPU的架构,才能更好的理解CPU是如何读写数据的。现代CPU的架构图如下:可以看出,一个CPU通常有多个CPU核,比如上图中的CPU核1和2。并且每个CPU核都有自己的L1缓存和L2缓存,而L1缓存通常分为dCache(数据缓存)和iCache(指令缓存),而L3缓存是多个核共享的,这是CPU典型的缓存级别。 以上提到的都是CPU内部的缓存。你看外面,会有内存和硬盘。这些存储设施共同构成了金字塔存储层次。 如下图所示:从上图也可以看出,从上到下,存储设施的容量会更大,访问速度会更慢。 至于各个存储设施的访问延迟,可以看下表:可以看到CPU访问L1缓存的速度比内存快100倍,这也是L1~L3缓存存在于CPU中的原因。目的是把Cache作为CPU和内存之间的缓存层来抓,减少访问内存的频率。 CPU从内存读取数据到缓存时,不是一个字节一个字节的,而是一个块一个块的。这个数据块叫做CPU线,所以CPU线是CPU从内存读取数据到缓存的单位。 至于CPU线长,可以在Linux系统下通过以下方式查看。你可以看到我的服务器的L1缓存行大小是64字节,也就是说一次加载的L1缓存数据的大小是64字节。 然后,当加载数组时,CPU会将数组中的许多连续数据加载到缓存中。所以要按照物理内存地址分布的顺序来访问元素,这样在访问数组元素的时候,缓存命中率会很高,从而减少从内存中读取数据的频率,从而提高程序的性能。 然而,当我们使用单独的变量而不是数组时,会出现缓存伪共享的问题,这是一个性能杀手,我们应该避免它。 接下来,我们来看看什么是缓存伪共享。如何避免这个问题?现在假设有一个双核现有CPU。这两个CPU内核并行运行两个不同的线程。它们同时从内存中读取两个不同的数据,分别是long类型的变量A和B。这两个数据的地址在物理内存中是连续的。如果Cahce行的大小是64字节,变量a在Cahce行的开头,那么这两个数据在Cache行的同一个位置,由于CPU行是CPU从内存读取数据到Cache的单位,所以这两个数据会同时读入两个CPU核各自的Cache中。 我们来思考一个问题。如果这两个核的线程修改了不同的数据,比如CPU核1的线程只修改了变量A,或者CPU核2的线程只修改了变量B,会发生什么情况?分析一下伪分享的问题。现在,让我们结合MESI协议来解释一下保证多核缓存一致性的整个过程。如果你还不了解MESI协议,可以看看我的文章《10张图打开CPU缓存一致性之门》 ①首先,变量A和B不在缓存中。我们假设1号核绑定到线程A,2号核绑定到线程B,线程A只能读写变量A,线程B只能读写变量B。 ②一号核读取变量A,由于CPU从内存读取数据到缓存的单位是缓存线,变量A和变量B的数据属于同一个缓存线,所以A和B的数据都会加载到缓存中,这个缓存线会被标记为“独占”。 ③接下来,core 2开始从内存中读取变量B,同样将缓存行大小的数据读入缓存。这个缓存行中的数据还包含变量A和b,此时核1和核2的缓存行状态变为“共享”。 ④1号核需要修改变量A,发现这条缓存线的状态是“共享”,需要通过总线给2号核发送消息,通知2号核将缓存中对应的缓存线标记为“无效”,然后1号核对应的缓存线状态变为“已修改”,修改变量A。 ⑤.之后,核2需要修改变量B,核2的缓存中对应的缓存线处于无效状态。此外,因为核心1的高速缓存具有相同的数据,并且状态为“已修改”,所以需要首先将对应于核心1的高速缓存的高速缓存线写回内存。然后,第2核将缓存行大小的数据从内存读入缓存,最后将变量B修改到第2核的缓存中,并将状态标记为“已修改”。 所以可以发现,如果1号和2号CPU核连续交替地分别修改变量A和B,那么这两个步骤④和⑤就会重复,Cache并没有缓存的效果。虽然变量A和B实际上是没有任何关系的,但是这个缓存行中的任何数据在被修改后都会相互影响,导致这两步④和⑤。 所以这种由于多个线程同时读写同一缓存行的不同变量而导致CPU缓存失效的现象称为伪共享(* * * * * *假共享* * * *)。 避免虚假分享的方法。所以对于多线程共享的热数据,也就是会被频繁修改的数据,要避免这些数据恰好在同一个缓存行,否则就会出现假共享的问题。 接下来我们来看看在实际项目中如何避免虚假分享的问题。 Linux内核中有__cacheline_aligned_in_smp的宏定义,用来处理伪共享的问题。 从上面的宏定义我们可以看到:如果在多核(MP)系统中,宏定义是__Cache Line_aligned,也就是Cache Line的大小;如果在单核系统中,宏定义为空;因此,对于同一缓存行中的共享数据,如果多核之间竞争严重,为了防止伪共享的发生,可以采用上述宏定义,使变量在缓存行中对齐。 比如有如下结构:结构中的两个成员变量A和B在物理内存地址上是连续的,所以它们可能位于同一个缓存行中,如下图所示:所以,为了防止前述的缓存伪共享问题,我们可以使用上述细节的宏定义将B的地址设置为缓存行对齐地址,如下图所示:这样, 变量A和B不会在同一个缓存行,如下图所示:所以,避免缓存伪共享,其实就是用空间换时间的思路,浪费一部分缓存空间,从而提高性能。 让我们看一个应用级的规避方案。有一个Java并发框架颠覆者,用“字节填充+继承”来避免伪共享的问题。 ruptor中有一个RingBuffer类经常被多个线程使用。代码如下:你可能觉得RingBufferPad类中七个long类型的名字很奇怪,但实际上,虽然看起来没什么用,但对性能的提升起到了至关重要的作用。 众所周知,CPU缓存从内存中读取数据的单位是CPU线。一般64位CPU的CPU线大小是64字节,一个long类型的数据是8字节,所以CPU会一次性加载8个long类型的数据。 根据JVM对象继承关系中的父类成员和子类成员,连续排列内存地址,所以RingBufferPad中的7个long数据作为缓存行预填充,而RingBuffer中的7个long数据作为缓存行后填充。这14个长变量没有实际用途,更不用说读写了。 此外,RingBufferFelds中定义的这些变量是最终变量,这意味着它们在第一次加载后不会被修改。因为“前端和后端”填充了7个不会被读写的长变量,所以无论缓存行如何加载,整个缓存行都没有数据会被升级。因此,只要数据被频繁读取和访问,自然就不存在数据被换出缓存的可能性,因此也就不存在假共享问题。 链接:https://mp.weixin.qq.com/s/zeGxBx77TFGtVeMRBVR-Lg作者:小林编码


  • 全部评论(0)
资讯详情页最新发布上方横幅
最新发布的资讯信息
【域名/主机/服务器|】qq邮箱提醒在哪里打开(2024-06-04 18:58)
【技术支持|常见问题】1556原创ng8文章搜索页面不齐(2024-05-01 14:43)
【技术支持|常见问题】1502企业站群-多域名跳转-多模板切换(2024-04-09 12:19)
【技术支持|常见问题】1126完美滑屏版视频只能显示10个(2024-03-29 13:37)
【技术支持|常见问题】响应式自适应代码(2024-03-24 14:23)
【技术支持|常见问题】1126完美滑屏版百度未授权使用地图api怎么办(2024-03-15 07:21)
【技术支持|常见问题】如何集成阿里通信短信接口(2024-02-19 21:48)
【技术支持|常见问题】算命网微信支付宝产品名称年份在哪修改?风水姻缘合婚配对_公司起名占卜八字算命算财运查吉凶源码(2024-01-07 12:27)
【域名/主机/服务器|】帝国CMS安装(2023-08-20 11:31)
【技术支持|常见问题】通过HTTPs测试Mozilla DNS {免费源码}(2022-11-04 10:37)

联系我们
Q Q:375457086
Q Q:526665408
电话:0755-84666665
微信:15999668636
联系客服
企业客服1 企业客服2 联系客服
86-755-84666665
手机版
手机版
扫一扫进手机版
返回顶部