浅谈C++性能榨汁机之伪共享

前言

在多核并发编程中,如果将互斥锁的争用比作“性能杀手”的话,那么伪共享则相当于“性能刺客”。“杀手”与“刺客”的区别在于杀手是可见的,遇到杀手时我们可以选择战斗、逃跑、绕路、求饶等多种手段去应付,但“刺客”却不同,“刺客”永远隐藏在暗处,伺机给你致命一击,防不胜防。具体到我们的并发编程中,遇到锁争用影响并发性能情况时,我们可以采取多种措施(如缩短临界区,原子操作等等)去提高程序性能,但是伪共享却是我们从所写代码中看不出任何蛛丝马迹的,发现不了问题也就无法解决问题,从而导致伪共享在“暗处”严重拖累程序的并发性能,但我们却束手无策。

缓存行

为了进行下面的讨论,我们需要首先熟悉缓存行的概念,学过操作系统课程存储结构这部分内容的同学应该对存储器层次结构的金字塔模型印象深刻,金字塔从上往下代表存储介质的成本降低、容量变大,从下往上则代表存取速度的提高。位于金字塔模型最上层的是CPU中的寄存器,其次是CPU缓存(L1,L2,L3),再往下是内存,最底层是磁盘,操作系统采用这种存储层次模型主要是为了解决CPU的高速与内存磁盘低速之间的矛盾,CPU将最近使用的数据预先读取到Cache中,下次再访问同样数据的时候,可以直接从速度比较快的CPU缓存中读取,避免从内存或磁盘读取拖慢整体速度。

CPU缓存的最小单位就是缓存行,缓存行大小依据架构不同有不同大小,最常见的有64Byte和32Byte,CPU缓存从内存取数据时以缓存行为单位进行,每一次都取需要读取数据所在的整个缓存行,即使相邻的数据没有被用到也会被缓存到CPU缓存中(这里又涉及到局部性原理,后面文章会进行介绍)。

缓存一致性

在单核CPU情况下,上述方法可以正常工作,可以确保缓存到CPU缓存中的数据永远是“干净”的,因为不会有其他CPU去更改内存中的数据,但是在多核CPU下,情况就变得更加复杂一些。多CPU中,每个CPU都有自己的私有缓存(可能共享L3缓存),当一个CPU1对Cache中缓存数据进行操作时,如果CPU2在此之前更改了该数据,则CPU1中的数据就不再是“干净”的,即应该是失效数据,缓存一致性就是为了保证多CPU之间的缓存一致。

Linux系统中采用MESI协议处理缓存一致性,所谓MESI即是指CPU缓存的四种状态:

  • M(修改,Modified):本地处理器已经修改缓存行,即是脏行,它的内容与内存中的内容不一样,并且此 cache 只有本地一个拷贝(专有);
  • E(专有,Exclusive):缓存行内容和内存中的一样,而且其它处理器都没有这行数据;
  • S(共享,Shared):缓存行内容和内存中的一样, 有可能其它处理器也存在此缓存行的拷贝;
  • I(无效,Invalid):缓存行失效, 不能使用。

每个CPU缓存行都在四个状态之间互相转换,以此决定CPU缓存是否失效,比如CPU1对一个缓存行执行了写入操作,则此操作会导致其他CPU的该缓存行进入Invalid无效状态,CPU需要使用该缓存行的时候需要从内存中重新读取。由此就解决了多CPU之间的缓存一致性问题。

伪共享

何谓伪共享?上面我们提过CPU的缓存是以缓存行为单位进行的,即除了本身所需读写的数据之外还会缓存与该数据在同一缓存行的数据,假设缓存行大小是32字节,内存中有“abcdefgh”八个int型数据,当CPU读取“d”这个数据时,CPU会将“abcdefgh”八个int数据组成一个缓存行加入到CPU缓存中。假设计算机有两个CPU:CPU1和CPU2,CPU1只对“a”这个数据进行频繁读写,CPU2只对“b”这个数据进行频繁读写,按理说这两个CPU读写数据没有任何关联,也就不会产生任何竞争,不会有性能问题,但是由于CPU缓存是以缓存行为单位进行存取的,也是以缓存行为单位失效的,即使CPU1只更改了缓存行中“a”数据,也会导致CPU2中该缓存行完全失效,同理,CPU2对“b”的改动也会导致CPU1中该缓存行失效,由此引发了该缓存行在两个CPU之间“乒乓”,缓存行频繁失效,最终导致程序性能下降,这就是伪共享。

如何避免伪共享

避免伪共享主要有以下两种方式:

1.缓存行填充(Padding):为了避免伪共享就需要将可能造成伪共享的多个变量处于不同的缓存行中,可以采用在变量后面填充字节的方式达到该目的。

2.使用某些语言或编译器中强制变量对齐,将变量都对齐到缓存行大小,避免伪共享发生。

总结

一般伪共享都很隐蔽,很难被发现,当伪共享真正构成性能瓶颈的时候,我们有必要去努力找到并解决它,但是在大部分对性能追求没有那么高的应用中,伪共享的存在对程序的危害很小,有时并不值得耗费精力和额外的内存空间(缓存行填充)去查找系统存在的伪共享。还是那句我一直以来遵循的话“不要过度优化,不要提前优化。”。

以上就是浅谈C++性能榨汁机之伪共享的详细内容,更多关于C++性能榨汁机之伪共享的资料请关注我们其它相关文章!

(0)

相关推荐

  • c++实现的常见缓存算法和LRU

    前言 对于web开发而言,缓存必不可少,也是提高性能最常用的方式.无论是浏览器缓存(如果是chrome浏览器,可以通过chrome:://cache查看),还是服务端的缓存(通过memcached或者redis等内存数据库).缓存不仅可以加速用户的访问,同时也可以降低服务器的负载和压力.那么,了解常见的缓存淘汰算法的策略和原理就显得特别重要. 常见的缓存算法 LRU (Least recently used) 最近最少使用,如果数据最近被访问过,那么将来被访问的几率也更高. LFU (Least

  • C++数据结构与算法之双缓存队列实现方法详解

    本文实例讲述了C++数据结构与算法之双缓存队列实现方法.分享给大家供大家参考,具体如下: "双缓存队列"是我在一次开发任务中针对特殊场景设计出来的结构.使用场景为:发送端持续向接收端发送数据包--并且不理会接收端是否完成业务逻辑.由于接收端在任何情况下停止响应即可能产生数据丢失,因此无法简单的设计一条线程安全队列来对数据写入或读取(读取数据时将队列上锁视为对写入的停止响应). 鉴于此,我的设计思路如下: 接收端首先向A队列中写入数据,然后当数据处理请求到来的时候切换到B队列继续写入,之

  • 详解C++ 共享数据保护机制

    下面随笔说明C++共享数据保护机制. 共享数据的保护 对于既需要共享.又需要防止改变的数据应该声明为常类型(用const进行修饰). 对于不改变对象状态的成员函数应该声明为常函数. (1)常类型 ①常对象:必须进行初始化,不能被更新. const 类名 对象名 ②常成员 用const进行修饰的类成员:常数据成员和常函数成员 ③常引用:被引用的对象不能被更新. const  类型说明符  &引用名 ④常数组:数组元素不能被更新(详见第6章). 类型说明符  const  数组名[大小]... ⑤常

  • C++进程间共享数据实例

    本文实例讲述了C++进程间共享数据的实现方法,分享给大家供大家参考.具体实现方法如下: 复制代码 代码如下: int main(int argc, char *argv[])  {      //RecursiveDelete("C:\\20_128\\");      //SelfRun("runModel");      //进程间内存共享      LPCTSTR lpName= "hello";      LPCTSTR lpConten

  • C++进程共享数据封装成类实例

    本文实例讲述了C++进程共享数据封装成类的方法,分享给大家供大家参考.具体方法如下: ShareMemory.cpp源文件如下: 复制代码 代码如下: #include "ShareMemory.h"    CShareMemory::CShareMemory(const    char* pszMapName, int nFileSize, BOOL bServer):m_hFileMap(NULL),m_pBuffer(NULL)  {      if (bServer) //是服

  • 探究在C++程序并发时保护共享数据的问题

    我们先通过一个简单的代码来了解该问题. 同步问题 我们使用一个简单的结构体 Counter,该结构体包含一个值以及一个方法用来改变这个值: struct Counter { int value; void increment(){ ++value; } }; 然后启动多个线程来修改结构体的值: int main(){ Counter counter; std::vector<std::thread> threads; for(int i = 0; i < 5; ++i){ threads

  • 浅谈C++性能榨汁机之伪共享

    前言 在多核并发编程中,如果将互斥锁的争用比作"性能杀手"的话,那么伪共享则相当于"性能刺客"."杀手"与"刺客"的区别在于杀手是可见的,遇到杀手时我们可以选择战斗.逃跑.绕路.求饶等多种手段去应付,但"刺客"却不同,"刺客"永远隐藏在暗处,伺机给你致命一击,防不胜防.具体到我们的并发编程中,遇到锁争用影响并发性能情况时,我们可以采取多种措施(如缩短临界区,原子操作等等)去提高程序性能,

  • 浅谈webpack性能榨汁机(打包速度优化)

    最近对项目的本地开发环境进行了打包速度优化,原有项目,网上能搜到的优化方案基本都加了,在16年低配mac pro 上打包时间为25秒多,但我发现细节做一些调整可能大大降低打包时间,最终优化到7秒多 dll 原有项目是线上和本地公用一套dll配置,因为antd这类ui库需要按需加载所以不能放到dll中,这时可以单独写一个dll配置,将所有第三方库添加到dll中. 这时因为.babelrc中添加了babel-plugin-import插件会导致优化不生效,所以需要对开发环境单独配置babel opt

  • 浅谈Android性能优化之内存优化

    1.Android内存管理机制 1.1 Java内存分配模型 先上一张JVM将内存划分区域的图 程序计数器:存储当前线程执行目标方法执行到第几行. 栈内存:Java栈中存放的是一个个栈帧,每个栈帧对应一个被调用的方法.栈帧包括局部标量表, 操作数栈. 本地方法栈:本地方法栈主要是为执行本地方法服务的.而Java栈是为执行Java方法服务的. 方法区:该区域被线程共享.主要存储每个类的信息(类名,方法信息,字段信息等).静态变量,常量,以及编译器编译后的代码等. 堆:Java中的堆是被线程共享的,

  • 浅谈android性能优化之启动过程(冷启动和热启动)

    本文介绍了浅谈android性能优化之启动过程(冷启动和热启动) ,分享给大家,具体如下: 一.应用的启动方式 通常来说,启动方式分为两种:冷启动和热启动. 1.冷启动:当启动应用时,后台没有该应用的进程,这时系统会重新创建一个新的进程分配给该应用,这个启动方式就是冷启动. 2.热启动:当启动应用时,后台已有该应用的进程(例:按back键.home键,应用虽然会退出,但是该应用的进程是依然会保留在后台,可进入任务列表查看),所以在已有进程的情况下,这种启动会从已有的进程中来启动应用,这个方式叫热

  • 浅谈python多线程和多线程变量共享问题介绍

    1.demo 第一个代码是多线程的简单使用,编写了线程如何执行函数和类. import threading import time class ClassName(threading.Thread): """创建类,通过多线程执行""" def run(self): for i in range(5): print(i) time.sleep(1) def sing(): for i in range(1,11): print("唱歌第

  • 浅谈PHP性能优化之php.ini配置

    内存 默认设置 memory_limit = 128M 单个进程可使用的内存最大值,这个值的设定可以从以下几点考虑: 应用的类型.如果是内存集中型应用,可增加该值: 单个 PHP 进程平均消耗的内存,该值可通过多次运行同一个脚本来计算平均值: 能负担多少个 php-fpm 进程:该值等于分配的总内存除以单个 PHP 进程平均消耗的内存: 文件上传 默认设置 file_uploads = On max_file_uploads = 20 upload_max_filesize = 2M max_e

  • 浅谈react性能优化的方法

    React性能优化思路 软件的性能优化思路就像生活中去看病,大致是这样的: 使用工具来分析性能瓶颈(找病根) 尝试使用优化技巧解决这些问题(服药) 使用工具测试性能是否确实有提升(疗效确认) 初识react只是为了尽快完成项目,后期进行代码审查时候发现有很多地方需要优化,因此做了个小结. Code Splitting shouldComponentUpdate避免重复渲染 使用不可突变数据结构 组件尽可能的进行拆分.解耦 列表类组件优化 bind函数优化 不要滥用props ReactDOMSe

  • 浅谈Vue 性能优化之深挖数组

    背景 最近在用 Vue 重构一个历史项目,一个考试系统,题目量很大,所以核心组件的性能成为了关注点.先来两张图看下最核心的组件 Paper 的样式. 从图中来看,分为答题区与选择面板区. 稍微对交互逻辑进行下拆解: 答题模式与学习模式可以相互切换,控制正确答案显隐. 单选与判断题直接点击就记录答案正确性,多选是选择答案之后点击确定才能记录正确性. 选择面板则是记录做过的题目的情况,分为六种状态(未做过的,未做过且当前选择的,做错的,做错的且当前选择的,做对的,做对的且当前选择的),用不同的样式去

  • Java 中的伪共享详解及解决方案

    1. 什么是伪共享 CPU 缓存系统中是以缓存行(cache line)为单位存储的.目前主流的 CPU Cache 的 Cache Line 大小都是 64 Bytes.在多线程情况下,如果需要修改"共享同一个缓存行的变量",就会无意中影响彼此的性能,这就是伪共享(False Sharing). 2. 缓存行 由于共享变量在 CPU 缓存中的存储是以缓存行为单位,一个缓存行可以存储多个变量(存满当前缓存行的字节数):而CPU对缓存的修改又是以缓存行为最小单位的,那么就会出现上诉的伪共

  • 浅谈django url请求与数据库连接池的共享问题

    但凡介绍数据库连接池的文章,都会说"数据库连接是一种关键的有限的昂贵的资源,这一点在多用户的网页应用程序中体现得尤为突出.对数据库连接的管理能显著影响到整个应用程序的伸缩性和健壮性,影响到程序的性能指标.数据库连接池正是针对这个问题提出来的.数据库连接池负责分配.管理和释放数据库连接,它允许应用程序重复使用一个现有的数据库连接,而不是再重新建立一个:释放空闲时间超过最大空闲时间的数据库连接来避免因为没有释放数据库连接而引起的数据库连接遗漏.这项技术能明显提高对数据库操作的性能." 这句

随机推荐