有时候,使用template可能会导致代码膨胀:其二进制码带着重复(或几乎重复)的代码、数据,或两者。其结果有可能源码看起来合身而整齐,但目标码却不是那么回事。
如何避免这样的二进制浮夸?
主要工具为共性与变性分析。
当你编写某个函数时,你明白其中某些部分的实现码和另一个函数的实现码实质相同,你会抽出两个函数的共同部分,把它们放进第三个函数中,然后令原先两个函数调用这个新函数。
编写template时,也是做相同的分析,以相同的方式避免重复。在template代码中,重复是隐晦的,毕竟只存在一份template源码,所以你必须训练自己去感受当template被具现化多次时可能发生的重复。
例如,假设你想为固定尺寸的正方矩阵编写一个template。该矩阵的性质之一是支持逆矩阵运算。
//template支持n x n矩阵,元素是类型为T的object
template<typename T,std::size_t n>
class SquareMatrix {
public:
//...
void invert();//求逆矩阵
};
这个template接受一个类型参数T,除此之外还接受一个类型为size_t的参数,那是个非类型参数。
现在,考虑这些代码:
SquareMatrix<double, 5> sm1;
//...
sm1.invert();//调用SquareMatrix<double,5>::invert
SquareMatrix<double, 10> sm2;
//...
sm2.invert();//调用SquareMatrix<double,10>::invert
这会具现化两份invert。这些函数并非完全相同,因为其中一个操作的是5*5矩阵而另一个操作的是10*10矩阵,但除了常量5和10,两个函数的其他部分完全相同。这是template引出代码膨胀的一个典型例子。
下面是对SquareMatrix的第一次修改:
template<typename T>//与尺寸无关的base class
class SquareMatrixBase {//用于正方矩阵
protected:
//...
void invert(size_t matrixSize);//以给定的尺寸求逆矩阵
//...
};
template<typename T,std::size_t n>
class SquareMatrix : private SquareMatrixBase<T> {
private:
using SquareMatrixBase<T>::invert;//避免遮掩base版的invert
public:
//...
//制造一个inline调用,调用base class版的invert
void invert() { this->invert(n); }
};
上述代码还有一些棘手的问题没有解决。
SquareMatrixBase::invert如何知道该操作什么数据?虽然它从参数中知道矩阵尺寸,但它如何知道那个特定矩阵的数据在哪儿?想必只有derived class知道。Derived class如何联络其base class做逆运算动作?
一个可能的做法是为SquareMatrixBase::invert添加另一个参数,也许是个指针,指向一块用来放置矩阵数据的内存起始点。这行得通,但十之八九invert不是唯一一个可写为“形式与尺寸无关可移至SquareMatrixBase内”的SquareMatrix函数。若有若干这样的函数,我们唯一要做的就是找出保存矩阵元素值的那块内存。我们可以对所有这样的函数添加一个额外参数,却得一次又一次地告诉SquareMatrixBase相同的信息,这样似乎不好。
另一个办法是令SquareMatrixBase贮存一个指针,指向矩阵数值所在的内存,而只要它存储了那些东西,也就可能存储矩阵尺寸。
template<typename T>
class SquareMatrixBase {
protected:
//存储矩阵大小和一个指针,指向矩阵数值
SquareMatrixBase(std::size_t n, T* pMem)
:size(n),pData(pMem){}
//重新赋值给pData
void setDataPtr(T* ptr) { pData = ptr; }
//...
private:
std::size_t size;//矩阵的大小
T* pData;//指针,指向矩阵内容
};
这允许derived class决定内存分配方式。某些实现版本也许会决定将矩阵数据存储在SquareMatrix对象内部:
template<typename T,std::size_t n>
class SquareMatrix : private SquareMatrixBase<T> {
public:
//送出矩阵大小和数据指针给base class
SquareMatrix():SquareMatrixBase<T>(n,data){}
//...
private:
T data[n * n];
};
这种类型的对象不需要动态分配内存,但对象自身可能非常大。
另一种做法是把每一个矩阵的数据放进heap(也就是通过new来分配内存):
template<typename T,std::size_t n>
class SquareMatrix : private SquareMatrixBase<T> {
public:
//将base class的数据指针设为null,为矩阵分配内存,
//将指向该内存的指针存储起来,然后将它的一个副本交给base class
SquareMatrix():SquareMatrixBase<T>(n,0),pData(new T[n*n])
{
this->setDataPtr(pData.get());
}
//...
private:
boost::scoped_array<T> pData;
};
不论数据存储于何处,从膨胀的角度看,现在许多SquareMatrix成员函数可以单纯地以inline方式调用base class版本,后者由“持有同型元素”(不论矩阵大小)的所有矩阵共享。与此同时,不同大小的SquareMatrix对象有着不同的类型,所以即使(例如SquareMatrix<double,5>和SquareMatrix<double,10>)对象使用相同的SquareMatrix<double>成员函数,我们也没机会传递一个SquareMatrix<double,5>对象到一个期望获得SquareMatrix<double,10>函数去。
这很棒,但必须付出代价。
硬是绑着矩阵尺寸的那个invert版本,有可能生成比共享版本(其中尺寸乃以函数参数传递或存储在对象内)更佳的代码。例如在尺寸专属版中,尺寸是个编译期常量,因此可以藉由常量的广传达到最优化,包括把它们折进被生成指令中成为直接操作数。这在“与尺寸无关”的版本中是无法办到的。
从另一个角度看,不同大小的矩阵只拥有单一版本的invert,可减少执行文件大小,也就因此降低程序的working set大小(所谓working set是指对一个在“虚内存环境”下执行的进程而言,其所使用的那一组内存页),并强化指令高速缓冲区内的引用集中化。这些都可能使程序执行得更快速,超越“尺寸专属版”invert的最优化效果。
哪一个影响占主要地位?
欲知答案,唯一的办法是两者都尝试并观察你的平台的行为以及面对代表性数据组时的行为。
另一个效能评比所关心的主题是对象大小。
你可将前述“与矩阵大小无关的函数版本”搬至base class内,这会增加每一个对象的大小。
例如,在上述的代码中,每一个SquareMatix对象都有一个指针指向SquareMatrixBase class内的数据。虽然每个derived class已经有一种取得数据的办法,这会对每一个SquareMatrix对象增加至少一个指针那么大。
这篇文章只讨论由non-type template parameter(非类型模板参数)带来的膨胀,但type parameter(类型参数)也会导致膨胀。
例如在许多平台上int和long有相同的二进制表述,所以像vector<int>或vector<long>的成员函数有可能完全相同,这正是膨胀的最佳定义。某些连接器会合并完全相同的函数实现码,但有些不会,后者意味某些template被具现化为int和long两个版本,并因此造成代码膨胀。
类似情况,在大多数平台上,所有指针类型都有相同的二进制表述,因此凡template持有指针者(例如list<int*>,list<const int*>等等)往往应对每一个成员函数使用唯一一份底层实现。这意味,若你实现某些成员函数而它们操作强型指针,你应该令它们调用另一个操作无类型指针的函数,由后者完成实际工作。某些C++标准程序库实现版本的确为vector,deque和list等template做了这件事。若你关心你的template可能出现代码膨胀,也许你会想让你的template也做相同的事情。
总结
1.template生成多个class和多个函数,所以任何template代码都不该与某个造成膨胀的template参数产生相依关系。
2.因非类型模板参数而造成的代码膨胀往往可消除,做法是以函数参数或class成员变量替换template参数。
3.因类型参数而造成的代码膨胀,往往可降低,做法是让带有完全相同二进制表述的具现类型共享实现码。