对象的创建和销毁往往会造成性能的损失。在继承层次中,对象的创建将引起其先辈的创建。对象的销毁也是如此。其次,对象相关的开销与对象本身的派生链的长度和复杂性相关。所创建的对象(以及其后销毁的对象)的数量与派生的复杂度成正比。
并不是说继承根本上就是代码性能的绊脚石。我们必须区分全部计算开销、必须开销和计算损失(computional penalty). 全部计算开销是一次计算中所执行的全部指令的总和。必须开销是全部指令的子集,它的结果是必要的。这部分计算是必需的,其余部分即为计算损失。计算损失是可以通过别的设计和实现来消除的那部分计算。
我们不能断言采用了复杂的继承的设计一定是坏的,也不能断定它们总是带来性能损失。我们只能说总的开销会随着派生树规模的增长而增加。如果所有的计算都是有价值的,那么它们都是必须的开销。实际上,继承层次不见得是完善的,在这种情况下,它们很可能会导致计算损失。
对象的复合与继承一样,都引入了与对象创建和销毁有关的类似性能问题。在对象被创建(或销毁)时,必须同时创建(或销毁)它所包含的成员对象。
创建和销毁被包含对象是另一个值得注意的问题:在创建(或销毁)被包含对象时无法阻止子对象的创建(或销毁),因为这是编译器自动强加的步骤。
性能优化经常需要牺牲一些其它软件目标,诸如灵活性、可维护性、成本和重用之类的重要目标经常必须为性能让步。
在C++中,不自觉地在程序开始处预先定义所有对象的做法是一种浪费。因为这样可能会创建一些直到最后都没有用到的对象。在C++中,把变量的创建延迟到第一次使用前。
构造函数和析构函数可以像手工编写的C代码一样有效。然而在实践中,它们经常包含冗余计算。
对象的创建(或销毁)触发对父对象和成员对象的递归创建(或销毁)。
要确保所编写的代码实际使用了所有创建的对象和这些对象所执行的计算。
对象的生命周期不是无偿的。至少对象的创建和销毁会消耗CPU周期。不要随意创建一个对象,除非你打算使用它。通常情况下,要等到需要使用对象的地方再创建它。
编译器必须初始化被包含的成员对象之后再执行构造函数体。你必须在初始化阶段完成成员对象的创建。这可以降低随后在构造函数部分调用赋值操作符的开销。在某些情况下,这样也可以避免临时对象的产生。
以下是测试代码(constructors_and_destructors.cpp):
#include "constructors_and_destructors.hpp"
#include <iostream>
#include <string>
#include <mutex>
#include <chrono>
namespace constructors_destructors_ {
// reference: 《提高C++性能的编程技术》:第二章:构造函数和析构函数
class SimpleMutex { // 单独的锁类
public:
SimpleMutex(std::mutex& mtx) : mymtx(mtx) { acquire(); }
~SimpleMutex() { release(); }
private:
void acquire() { mymtx.lock(); }
void release() { mymtx.unlock(); }
std::mutex& mymtx;
};
class BaseMutex { // 基类
public:
BaseMutex(std::mutex& mtx) {}
virtual ~BaseMutex() {}
};
class DerivedMutex : public BaseMutex {
public:
DerivedMutex(std::mutex& mtx) : BaseMutex(mtx), mymtx(mtx) { acquire(); }
~DerivedMutex() { release(); }
private:
void acquire() { mymtx.lock(); }
void release() { mymtx.unlock(); }
std::mutex& mymtx;
};
class Person1 {
public:
Person1(const char* s) { name = s; } // 隐式初始化和显示赋值
private:
std::string name;
};
class Person2 {
public:
Person2(const char* s) : name(s) {} // 显示初始化
private:
std::string name;
};
int test_constructors_destructors_1()
{
// 测试三种互斥锁的实现
// Note:与书中实验结果有差异,在这里继承对象并不会占用较多的执行时间,在这里这三种所占用时间基本差不多
using namespace std::chrono;
high_resolution_clock::time_point time_start, time_end;
const int cycle_number {100000000};
int shared_counter {0};
{ // 1.直接调用mutex
std::mutex mtx;
shared_counter = 0;
time_start = high_resolution_clock::now();
for (int i = 0; i < cycle_number; ++i) {
mtx.lock();
++shared_counter;
mtx.unlock();
}
time_end = high_resolution_clock::now();
std::cout<< "time spen1: "<<(duration_cast<duration<double>>(time_end - time_start)).count()<< " seconds\n";
}
{ // 2.不从基类继承的独立互斥对象
std::mutex mtx;
shared_counter = 0;
time_start = high_resolution_clock::now();
for (int i = 0; i < cycle_number; ++i) {
SimpleMutex m(mtx);
++shared_counter;
}
time_end = high_resolution_clock::now();
std::cout<< "time spen2: "<<(duration_cast<duration<double>>(time_end - time_start)).count()<<" seconds\n";
}
{ // 3.从基类派生的互斥对象
std::mutex mtx;
shared_counter = 0;
time_start = high_resolution_clock::now();
for (int i = 0; i < cycle_number; ++i) {
DerivedMutex m(mtx);
++shared_counter;
}
time_end = high_resolution_clock::now();
std::cout<< "time spen3: "<<(duration_cast<duration<double>>(time_end - time_start)).count()<<" seconds\n";
}
// 隐式初始化和显示赋值与显示初始化性能对比:使用显示初始化操作要优于使用隐式初始化和显示赋值操作
{ // 1.隐式初始化和显示赋值操作
time_start = high_resolution_clock::now();
for (int i = 0; i < cycle_number; ++i) {
Person1 p("Pele");
}
time_end = high_resolution_clock::now();
std::cout<< "隐式初始化, time spen: "<<(duration_cast<duration<double>>(time_end - time_start)).count()<<" seconds\n";
}
{ // 2.显示初始化操作
time_start = high_resolution_clock::now();
for (int i = 0; i < cycle_number; ++i) {
Person2 p("Pele");
}
time_end = high_resolution_clock::now();
std::cout<<"显示初始化, time spen: "<<(duration_cast<duration<double>>(time_end - time_start)).count()<<" seconds\n";
}
return 0;
}
} // namespace constructors_destructors_
运行结果如下: