源作者:小智雅汇
如果一个变量在整个程序运行期间都存在所谓临时变量,并不是指用户定义的某种临时用的变量,而是指编译器内部按需自动生成的各种隐式的临时变量。C++中的临时变量问题是影响程序运行性能的一个关键因素,因此,为了提高程序的运行性能,应该理解C++的一些内部机制,尽量降低或减少临时变量问题带来的影响。
产生临时变量的情形:
① 类型转换
② 函数传参与返回及局部变量
③ 表达式
1 类型转换时的临时变量问题
在进行混合运算、函数调用参数传递和函数调用返回时,经常存在类型转换问题。对于普通内置数据类型来说,类型转换不存在明显的性能问题。然而,对于抽象数据类型(类)而言,类型转换会带来较大的性能问题。究其原因,主要是涉及相应对象实例的临时构造与析构问题。特别是,对于自动隐式类型转换,更应该引起重视。
具体而言,依据类型转换原则,每当将一个小类型转换为一个大类型时采用自动隐式类型转换,此时,如果大类型是抽象数据类型,那么,依据抽象数据类型的构造原理,此时需要调用大抽象数据类型的类型转换构造函数,并以小类型(一般是内置类型。对于抽象数据类型,则是通过类型转换运算符重载方法实现)作为参数进行隐式类型转换。
对于抽象数据类型,无论是类型转换构造函数,还是类型转换运算符重载,发生类型转换时,编译器会自动调用这些函数,建立一个无名的临时对象实例(即转变后的抽象数据类型的实例),当本次运算、本次函数调用或本次函数返回工作结束后,该临时对象实例被自动析构,即调用相应抽象数据类型的析构函数。因此,临时实例的构造与析构因存在函数调用而导致程序性能受到影响。例如,对于表达式c1+2.5,其中c1是某种抽象数据类型,此时需要将2.5作为参数,隐式调用该抽象数据类型的转换构造函数构造出一个临时的该抽象数据类型的实例,并再次调用该抽象数据类型的重载运算函数operator+()实现最终的运算。该表达式处理完成,所构造的临时实例还需要被自动析构。
2 函数传参与返回及局部变量
2.1局部变量生命期带来的临时变量问题
依据面向功能(函数)方法的原理,局部变量的生命期是从函数被调用开始,到函数调用返回时结束。因此,如果局部变量是抽象数据类型,那么,每次函数调用都会有实例构造和析构问题,这就影响程序的运行性能。因此,对于批量数据组织(例如数组),无论是内置类型还是抽象数据类型,应该考虑将其限定为static局部变量,以便消除每次大批量局部变量的构造与撤销(对于内置类型)或对象实例的反复构造与析构(对于抽象数据类型)。
另外,对于局部的抽象数据类型实例的定义,应尽量放在(靠近)其使用处,以便消除因不必要的析构函数调用(例如,定义后还没有使用前,因满足某种条件引起函数返回而引发局部实例被析构)而影响性能的弊端。
2.2函数调用参数传递时的临时变量问题
变量是指在程序运行过程中函数调用时,存在两个方面的临时变量构造与析构问题。一是参数传递时的隐式类型转换所引起的临时变量问题;二是参数传递方式所带来的临时变量问题。前者首先需要通过调用类型转换函数临时构造一个目标类型的对象实例,然后通过调用复制构造函数将该临时对象实例传递给对应的参数。后者主要是由传值方式所带来的问题。与内置类型不同,抽象数据类型如果采用值传递,将会涉及参数对象实例在堆栈空间的构造问题,此时复制构造函数被调用,增加函数调用开销。因此,对于抽象数据类型,一般都是使用引用传递。
2.3函数调用返回时的临时变量问题
函数调用返回时,也存在两个方面的临时变量构造与析构问题。一是函数结果返回时的隐式类型转换所引起的临时变量问题;二是函数调用返回时返回方式所带来的临时变量问题。前者与参数传递时的临时变量问题类似,不再赘述。后者主要是由传值返回方式所带来的问题。
与内置类型不同,抽象数据类型如果采用值返回,将会涉及函数结果对象实例在堆栈空间的临时构造问题,此时会增加构造函数和析构函数被调用的开销。因此,对于抽象数据类型,函数调用返回一般也都采用引用返回(此时必须确保不能导致无效引用,或者通过const&延长临时对象实例的生命期)。此时,如果要消除因引用返回带来的左值效应所导致的安全隐患,可以通过const进行限定(参见上面有关const机制的相应解析)。
特别是,上述各种情况对于含有嵌入对象的类或继承树中的派生类,其临时对象实例的构造与析构开销会更大!严重影响程序的运行性能。可见,C++引入引用机制的本质作用就是为了提高性能及安全性(即引用总是存在明确的绑定对象)。
尽管C++的引用机制可以提高性能,然而,还是存在若干问题。首先,虽然通过const引用机制可以拓展左值应用,实现到值(常量或一个表达式)或临时实例的绑定,但其不能改变被绑定对象的值。也就是,这种拓展力度不够,不具备左值应用的相似特点。其次,函数机制中,对于抽象数据类型而言,引用型参数传递对于右值形态实参,会涉及临时实例的构造与析构开销;引用型返回时对于右值形态返回值,也会涉及临时实例的构造与析构开销,并且导致无效引用安全隐患。为此,新的C++标准中增加了右值引用
机制,进一步完善了引用机制。
右值引用的标志采用两个&&符号,即 typename&&。例如:int&& x = 8;。其中,x就是右值引用变量。右值引用主要用于对右值形态(即“值”)或临时对象实例的绑定
(与之对应的左值引用主要用于对左值形态,即变量/“地址”的绑定)。事实上,右值形态最终也是一种临时对象实例。因此,右值引用的本质就是要实现对临时对象实例的控制、管理和读写访问(通过绑定的右值引用变量进行),延长其生命周期(即使其与右值引用变量具有相同的生命周期)。
具体而言,就是实现资源控制权转移,并且,通过这种转移消除临时实例构造与析构的开销,进一步提高程序运行性能。另外,新C++标准也提供了move()库函数,用于实现对左值的右值转换(用于将一个生命周期即将结束的左值按右值引用思想继续使用)。
3 表达式计算时的临时变量问题
对于含有抽象类型的表达式计算,最终都归结为类型转换和函数调用(包括重载的运算符)两个方面。
ref
沈军《计算思维与程序设计》
-End-