C++内存管理全景指南

原文地址:https://mp.weixin.qq.com/s/YPIe4Nz_QASSiXqOgnFqnQ


导语 深入理解C++内存管理,一文了解所有C++内存问题,万字长文,建议收藏

随着人工智能,云计算等技术的迅猛发展,让Python,go等新兴语言流行了起来,很多人以为C++可能已经过时了,确实,C++编程语言走到今天已经有将近40年的历史了,但它依然是当今的主流语言,我们可以看一下世界权威编程语言排行榜,C++依然是属于第一梯队,C++在金融交易系统,游戏,数据库,编译器,大型桌面程序,高性能服务器,浏览器,各类编程比赛(ACM-ICPC,Topcoder,Codeforces,Google Code Jam)等领域任然是主力军。

C++的高抽象层次,又兼具高性能,是其他语言所无法替代的,C++标准保持稳定发展,更加现代化,更加强大,更加易用,熟练的 C++ 工程师自然也获得了“高水平、高薪资”的名声,但在各种活跃编程语言中,C++门槛依然很高,尤其C++的内存问题(内存泄露,内存溢出,内存宕机,堆栈破坏等问题),需要理解C++标准对象模型,C++标准库,标准C库,操作系统等内存设计,才能更加深入理解C++内存管理,这是跨越C++三座大山之一,我们必须拿下它。

Content

环境:

一 C++内存模型

C++11在标准库中引入了memory model,这应该是C++11最重要的特性之一了。C++11引入memory model的意义在于我们可以在high level language层面实现对在多处理器中多线程共享内存交互的控制。我们可以在语言层面忽略compiler,CPU arch的不同对多线程编程的影响了。我们的多线程可以跨平台。

内存模型

为 C++ 定义计算机内存存储的语义。可用于 C++ 程序的内存是一或多个相接的字节序列。内存中的每个字节拥有唯一的地址。

字节

字节是最小的可寻址内存单元。它被定义为相接的位序列,大到足以保有任何 UTF-8 编码单元( 256 个相异值)和 (C++14 起)基本执行字符集(要求为单字节的 96 个字符)的任何成员。类似 C , C++ 支持 8 位或更大的字节。char 、 unsigned char 和 signed char 类型把一个字节用于存储和值表示。字节中的位数可作为 CHAR_BIT 或 std::numeric_limits::digits 访问。

内存位置

内存位置是

  • 一个标量类型(算术类型、指针类型、枚举类型或 std::nullptr_t )对象
  • 或非零长位域的最大相接序列

注意:各种语言特性,例如引用和虚函数,可能涉及到程序不可访问,但为实现所管理的额外内存位置。

线程与数据竞争

  • 执行线程是程序中的控制流,它始于 std::thread::thread 、 std::async 或以其他方式所做的顶层函数调用。
  • 任何线程都能潜在地访问程序中的任何对象(拥有自动或线程局域存储期的对象仍可为另一线程通过指针或引用访问)。
  • 始终允许不同的执行线程同时访问(读和写)不同的内存位置,而无冲突或同步要求。

一个表达式的求值写入内存位置,而另一求值读或写同一内存位置时,称这些表达式冲突。拥有二个冲突求值的程序有数据竞争,除非

  • 两个求值都在同一线程上,或同一信 处理函数中执行,或
  • 两个冲突求值都是原子操作(见 std::atomic ),或
  • 一个冲突求值先发生于( happens-before )另一个(见内存顺序–std::memory_order )

若出现数据竞争,则程序的行为未定义。

内存顺序(std::memory_order)

如果不使用任何同步机制(例如 mutex 或 atomic),在多线程中读写同一个变量,那么程序的结果是难以预料的。简单来说,编译器以及 CPU 的一些行为,会影响到C++程序的执行结果

  • 即使是简单的语句,C++ 也不保证是原子操作。
  • CPU 可能会调整指令的执行顺序。
  • 在 CPU cache 的影响下,一个 CPU 执行了某个指令,不会立即被其它 CPU 看见。
  • Intel x86, x86-64等属于强排序CPU,x86-64的强内存模型总能保证按顺序执行,遵从数据依赖顺序,但PowerPC和ARM是弱排序CPU,有时需要依赖内存栅栏指令。

多线程读写同一变量需要使用同步机制,最常见的同步机制就是std::mutex和std::atomic。然而从性能角度看,通常使用std::atomic会获得更好的性能.

C++11 提供6 种可以应用于原子变量的内存次序:
  • momory_order_relaxed,
  • memory_order_consume,
  • memory_order_acquire,
  • memory_order_release,
  • memory_order_acq_rel,
  • memory_order_seq_cst

虽然共有 6 个选项,但它们表示的是四种内存模型:

  • Relaxed ordering
  • Release-Acquire ordering
  • Release-Consume ordering
  • Sequentially-consistent ordering
顺序一致次序(sequential consistent ordering)

对应memory_order_seq_cst. SC作为默认的内存序,是因为它意味着将程序看做是一个简单的序列。如果对于一个原子变量的操作都是顺序一致的,那么多线程程序的行为就像是这些操作都以一种特定顺序被单线程程序执行。从同的角度来看,一个顺序一致的 store 操作 synchroniezd-with 一个顺序一致的需要读取相同的变量的 load 操作。除此以外,顺序模型还保证了在 load 之后执行的顺序一致原子操作都得表现得在 store 之后完成。非顺序一致内存次序(non-sequentially consistency memory ordering)强调对同一事件(代码),不同线程可以以不同顺序去执行,不仅是因为编译器可以进行指令重排,也因为不同的 CPU cache 及内部缓存的状态可以影响这些指令的执行。但所有线程仍需要对某个变量的连续修改达成顺序一致。

松弛次序(relaxed ordering)

在这种模型下,std::atomic的load()和store()都要带上memory_order_relaxed参数。Relaxed ordering 仅仅保证load()和store()是原子操作,除此之外,不提供任何跨线程的同步。

获取-释放次序(acquire-release ordering)

在这种模型下,store()使用memory_order_release,而load()使用memory_order_acquire。这种模型有两种效果,第一种是可以限制 CPU 指令的重排:

  • 在store()之前的所有读写操作,不允许被移动到这个store()的后面。
  • 在load()之后的所有读写操作,不允许被移动到这个load()的前面。
数据依赖(Release-Consume ordering)

memory_order_consume 是 acquire-release 顺序模型中的一种,但它比较特殊,它为 inter-thread happens-before 引入了数据依赖关系:dependency-ordered-before ,一个使用memory_order_consume的操作具有消费语义(consume semantics)。我们称这个操作为消费操作(consume operations),对于memory_order_consume最的价值的观察结果就是总是可以安全的将它替换成memory_order_acquire,消费和获取都为了同一个目的:帮助非原子信息在线程间安全的传递。就像获取操作一样,消费操作必须与另一个线程的释放操作一起使用。它们之间主要的区别在于消费操作可以正确起作用的案例更少。相对于它的使用不便,反过来也就意味着消费操作在某些平台使用更有效。

2 非空类

4 单继承

C++内存管理全景指南

注意点:此种继承存在两份基类成员,使用时候需要指定路径,不方便,易出错。

8 The Diamond: 钻石类虚继承

解决上面的问题,让基类只有存在一份,共享基类;

声明:本站部分文章及图片源自用户投稿,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!

上一篇 2022年11月4日
下一篇 2022年11月4日

相关推荐