C++开发CFD代码需要注意的内存管理
在C++编程中,内存管理是核心概念之一。它直接影响到程序的执行效率。如果内存管理做得不好,即使C++作为一种编译型语言在理论上具有性能优势,实际运行时也可能比解释型语言更慢。幸运的是,掌握内存管理并不难,只需理解一些基础概念。C++语言本身不自动管理内存,而是将这一责任交给了开发者。不过,通过使用标准模板库(STL),我们可以在很大程度上简化内存管理的任务,从而专注于编写高性能的代码。
理解C++中的内存及其管理,可能是掌握这门语言的一半关键。栈是相对较小但速度快的内存区域,通常大小在8MB左右。堆则是一个可以非常大且速度稍慢的内存区域,理论上没有大小限制。
理解栈和堆的工作原理对于有效管理内存至关重要。栈是程序运行时用于存储局部变量的内存区域,其大小相对固定,通常在操作系统的控制下,容量有限但访问速度很快。堆则是一个动态的内存区域,其大小理论上没有限制,可以根据程序的需要动态分配和回收。栈的快速访问特性使其适合存储小的、生命周期短的变量,而堆则适合存储大的数据结构,如大型数组,因为这些数据可能在程序运行期间动态增长。
如果打个比喻,想想停车。可以将栈比作多层停车场,你有两个选择,要么你停在一个多层停车场,或者你使用代客泊车服务。如果你使用多层停车场,你知道你确切地停在哪里。你知道是哪一层,如果我们假设每个停车位都标记了,你也知道那一层上的位置。这意味着找到你的车很快。如果你使用代客泊车服务,你交出你的车,它会停在某个地方,但你不知道在哪里,你只知道一个数字或票据,你可以用它来取回你的车。所以你不知道车在哪里,但你有一种机制,通过它你可以取回你的车(比如出示你的票据)。我们可以同意,平均来说,使用多层停车场会比代客泊车更快地找到你的车。在这个比喻中,多层停车场是我们的栈,代客泊车服务是我们的堆。
所以栈是一块小的内存区域,我们可以快速地从中检索数据,但我们在可用空间和可以存储的数据量上是有限的。另一方面,堆是广阔的,我们可以存储大量的数据。然而,如果我们想检索它,我们首先需要通过引用找到它,这需要更长的时间。
我们说栈很小,但这只是与堆相比,它仍然可以存储相当多的数据。通常,栈保存我们频繁访问的信息,在CFD的背景下,这些是我们的求解器设置,比如迭代次数、时间步长大小、CFL数等等。所以如果一个变量保存一个值(比如一个数字、一个字符串或一个布尔值),那么把它放在栈上,但如果变量保存同一类型的多个值,即一个数字、字符串、布尔值等的数组,那么它应该放在堆上。
在决定使用栈还是堆时,需要考虑数据的大小和生命周期。例如,在CFD模拟中,处理大量的坐标和解决方案数组时,由于它们的大小可能很大且在程序运行期间可能增长,因此应该在堆上分配。这样可以避免因数据量过大而导致栈溢出。相对地,对于那些仅存储单个值的变量,如整数、浮点数、布尔值或短字符串,由于它们占用的空间小,生命周期通常与函数调用相关,因此更适合存储在栈上。
对于大型数据数组,如坐标、速度、压力、温度等解决方案数组,应使用堆。这些数据可能增长,因此应放在堆上以避免栈溢出。其他所有内容则放在栈上。
CPU高速运行需要快速的数据访问,这通过内存缓存来实现。缓存是位于CPU和主内存之间的一层快速存储,它按照一定的层次结构(L1、L2、L3等)组织,以减少CPU访问主内存的次数。当CPU执行指令需要数据时,它会首先在缓存中查找,如果找到则称为缓存命中,访问速度非常快;如果未找到,则需要从更慢的存储介质中获取数据,这称为缓存未命中,会显著降低程序的执行速度。
CPU通过内存层次结构访问数据,包括寄存器、L1、L2、L3缓存,以及主内存和硬盘。缓存的大小与访问速度成反比。CFD应用需要高效管理内存,因为它们对内存的需求很大。
从理论上讲,你可以使用硬盘作为RAM。有了TB级的硬盘,这将是非常有利可图的。实际上,如果你使用的是像Ubuntu这样的UNIX发行版,在安装阶段它会询问你是否想要设置交换空间,这本质上是分配一部分硬盘用作RAM。如果你使用的是旧的固态硬盘,我们可以看到大约需要3毫秒才能访问数据。与62.9纳秒相比,你很快就会明白为什么你不想使用硬盘作为RAM。如果你使用硬盘作为RAM,你将增加你的计算时间大约48倍(3ms / 62.9 ns)。
那么,我们为什么要花这么多时间在CPU及其缓存上呢?因为CFD应用程序是内存密集型的应用程序,我们需要有效地管理它。
6、屋顶线内存模型(Roofline Memory Model)
大多数人可能不知道缓存,这没关系,但我们作为CFD开发者需要至少在基础知识上理解内存,现在你已经知道了。这里还有一个小秘密;大多数应用程序实际上是内存受限的,即如果你要测量你的应用程序的性能(通常以GFLOPS或(giga)每秒浮点操作数来完成,即你每秒能够做多少浮点操作),你将不会以CPU的最大时钟速度运行,而是受到你加载的内存量的限制。
屋顶线模型是一个用于评估和理解计算性能的工具,它揭示了程序性能与内存带宽之间的关系。根据这个模型,程序的性能受限于内存的带宽,即数据加载的速度。如果程序的计算强度(每字节执行的浮点操作数)足够高,就可以接近CPU的最大性能。反之,如果内存访问成为瓶颈,即使CPU的计算能力很强,程序的整体性能也会受到限制。
在这个图表中,我们可以看到,只有在我们有足够的(浮点)操作数每秒(FLOPS)对于我们加载的每个字节时,我们才能获得峰值性能,换句话说,我们想要最大化FLOPS的数量,并最小化加载的内存量,实现这一点的一种方法是尽可能避免缓存未命中(但你不能完全避免缓存未命中,最终你必须将新数据加载到你的缓存中,你只是想在可能的地方最小化)
这就是关于内存管理的全部内容,简单来说。虽然我们在这里只是浅尝辄止,您可以使任何主题变得更加复杂,但您现在应该对这个问题有了一个很好的了解。