C++变量回顾
声明:
本文基于Kurt Guntheroth所著Optimized C++: Proven Techniques for Heightened Performance的中文译本《C++性能优化指南》,人民邮电出版社,2018,杨文轩译。
自从接触高性能计算问题,便深感计算机系统之繁杂。从底层的cpu架构、向量指令集、cache实现,到操作系统的进程/线程切换、编译器对代码的处理等等都无时无刻影响着程序执行的效率。且不论硬件和系统之类乱七八糟的东西,就拿开发者写出来的程序来说,一切高级语言的程序实现在经过编译器的转化后,程序在计算机底层的动作往往与开发者的想法有所出入。按理说现代计算机系统的设计指导思想之一就是抽象+封装。这种思想使得开发者无需考虑底层的工作机制,专心于自己眼前的事物,从而大大提升了开发者的工作效率。但不得不说这种层层抽象、层层封装的俄罗斯套娃是以牺牲计算性能为代价的。由于上层开发者不明白(一般也不需要明白)底层的内部实现方法,所以底层开发者必须保证他写的代码的可靠性,不得不让编译器生成的机器指令多做一些“无用功”。好在得益于几十年来硬件技术的改善,开发者所能使用的算力远非同日可语。在一般情形下,与这些俄罗斯套娃带来的便利相比,造成的计算性能的浪费无伤大雅。然而一旦对计算资源有所约束,又或者需要面对巨量的性能开销,就不得不考虑对程序的优化了。
既然如前文所述,计算性能的浪费来自于上层开发者无视底层实现的自说自话,那么想要优化程序,自然就得从了解底层实现开始。这本《C++性能优化指南》就介绍了很多C++的标准和一些可能的编译器实现思路,相当值得参考。这里结合本人的经验,记录本书第六章关于C++变量的相关说法,方便以后查阅。
C++变量回顾
C++每个变量(普通数据类型、数组、结构体、实例)在内存中的存储布局都是固定的,它们的大小在编译之时就已经确定了。
C++允许程序获得变量的大小和指向该变量的指针,但不允许指定变量的每一位的布局。
C++标准允许改变struct
成员变量内部的顺序和布局,也提供了多个变量可以共享同一内存块的union
,但程序所看到的联合是依赖于实现的。
变量的存储期(生存周期)
每个变量都有它自己的存储期,也叫生存周期。只有在这段时间内,变量所占用的存储空间和里面存储的值才是有意义的。C++不能直接指定变量的生存周期,只能从变量声明中推断。
C++ 11中的生存周期有这几种:静态、线程局部、自动,以及动态生存周期。
静态生存周期
编译器会为每一个静态变量分配一个固定位置和固定大小的内存空间,该空间在其生存周期内会被一直保留。全局静态变量在进入
main()
之前构建,退出main()
后销毁。函数内静态变量则在“程序执行第一次进入函数前”被构建。
也就是说函数内静态变量可能和全局变量同时被构建,也可能直到第一次调用前才构建,这取决于编译器的具体实现。*
为静态变量创建存储空间没有运行时开销。
命名空间作用域内定义的变量,还有被声明为static
或者extern
的变量具有静态生存周期。
线程局部生存周期
从C++11开始,程序可以声明具有线程局部生存周期的变量。
跟C++11开始引入的std::thread类有关吗?这种变量我还没接触过,有待验证。线程局部变量在进入线程时被构建,退出线程时被销毁。其生命周期与线程的生命周期相同,每一个线程都包含一份这类变量的独立副本。
一般来说访问线程局部变量的开销高于静态变量,具体取决于操作系统和编译器。在某些系统中,线程局部变量的存储空间是由线程分配的,所以访问线程局部变量比访问全局变量多一次指令。**大概是需要计算主进程内存空间到相应线程的内存空间的offset,我是这么理解的**
在其他系统中,需要通过线程ID索引一张全局表来访问线程局部变量。尽管这个操作开销是O(1),但会发生一次函数调用和一些计算,导致线程局部变量的开销变得更大。自C++11开始,用
thread_local
类型声明的变量具有线程局部生存周期。自动生存周期
自动生存周期的变量会被分配到预留的函数调用栈中。在编译时就会计算出其距离栈指针的offset,然后以该offset为基准占用一段固定大小的内存空间,但自动变量的绝对地址指导程序进入变量的作用域之后才能确定。自动变量在运行到它的声明位置时被构建,当程序离开大括号括起来的代码块时,自动变量被摧毁。
为自动变量分配内存空间没有运行时开销。但自动变量可以占用的总存储空间有限,深度函数调用/递归太多层会溢出(segment fault警告)。因此适用于只在代码块附近被使用的对象。
可以通过名字访问自动变量,但只在构建后到摧毁前可见;变量摧毁后,指向该变量的指针和引用依然存在,但解引它会导致未定义的程序行为(segment fault警告)。自动生存周期没有专门的关键字。函数的形参变量具有自动生存周期;除非使用了特殊关键字,声明在可执行代码块内部的变量也具有自动存储期。
动态生存周期
动态生存周期的变量保存在程序向系统请求的内存中,其地址在运行时确定。
当需要内存空间时,程序会显式地调用内存管理器来请求内存空间并构建动态变量;当不需要该变量时,程序应显式地调用内存管理器来摧毁动态变量,并将所占空间返还给内存管理器。(也就是new和delete)值得一提的是数组的声明。C++允许运行时通过一个(非常量)表达式来指定动态数组变量的最高维度。这是唯一一种编译时变量占用内存大小不固定的情况。