根据上篇文章《C++虚函数会导致性能开销大?》,我们了解了虚函数的机制,以及虚函数对性能的影响。
只能在运行期间解析的虚函数调用是不允许使用内联的。这往往会造成性能问题,该问题我们必须解决。因为函数调用的动态绑定是继承的结果,所以消除动态绑定的一种方法是用基于模板的设计来替代继承。模板把解析的步骤从运行期间提前到编译期间,从这个意义上说,模板提高了性能。而对于我们所关心的编译时间,适当增加也是可以接受的。
下面我们从代码的角度进行说明。
代码示例
假设我们要开发一个线程安全的string类,它可以被并发线程安全地使用。
第一步,先设计Locker抽象基类将声明公共接口:
//接口类
class Locker
{
Locker(){};
virtual ~Locker(){};
virtual void lock()=0;
virtual void unlock()=0;
};
CriticalSectionLock和MutexLock将从基类Locker派生出来。
//派生类
class CriticalSectionLock:public Lock
{
....
};
class MutexLock:public Lock
{
....
};
第二步,从现有的标准string类派生出线程安全的string类,有如下三种方式:
● 继承 (存在性能问题)
可以派生出一个单独的ThreadSafeString类,它包含指向Locker对象的指针(建立成员变量:锁基类对象指针,在初始化列表进行初始化,传入不同类型,多态机制)。在运行期间通过多态机制选择特定的同步机制。
把所选择的同步机制(具体类型)作为构造函数的参数:
//通过多态机制实现不同类型锁的使用
class ThreadSafeString : public string
{
public:
ThreadSafeString (const char *s, Locker *lockPtr): string(s), pLock(lockPtr) {};
...
int length();
private:
Locker *pLock;//基类类型,锁类型当前不固定,使用多态机制在运行时才能知道具体的子类类型
};
int ThreadSafeString::length()
{
pLock->lock();
int len = string::length();
pLock->unlock();
return len;
}
//使用锁
CriticalSectionLock cs;
ThreadSafeString csString("Hello", &cs);//在运行时传入具体的子类类型
MutexLock mtx;
ThreadSafeString csString("Hello", &mtx);//在运行时传入具体的子类类型
这种实现比较简洁。但是它确实带来了性能的损失:
虚函数调用lock()和unlock()仅在执行期间解析,因此不能对它们内联。
● 硬编码
可以从string类中派生出三个独立类:CriticalSection String、MutexString和SemaphoreString。每个类实现各自名字所代表的同步机制(建立成员变量:锁子类对象)。
例如CriticalSectionString类:
//用标准的string类被用做基类。每个由它派生的类实现某一特定的同步机制。
class CriticalSectionString : public string
{
public:
...
int length();
private:
CriticalSectionLock cs;//锁类型固定,不是基类类型,而是具体的子类类型,不使用多态机制
};
int CriticalSectionString::length()
{
cs.lock();
int len = string::length();
cs.unlock();
return len;
}
这种设计在性能上具有优势:
这个线程安全的string类在编译期间指定了特定的同步类(指定了固定的子类类型,例如CriticalSectionString)。因此,编译器可以绕过动态绑定来选择正确的lock()和unlock()方法。更重要的是,这种设计允许编译器内联这两个方法。
该设计的不足之处:
在于需要为每种同步机制编写各自的string类,导致代码重用性较低。
● 模板
基于模板的string类,该类由Locker类型参数化后得到(模板类中建立成员变量:默认类型是基类锁对象)。
基于模板的设计结合了两方面的优点: 重用和效率。
//ThreadSafeString是作为模板实现的,它的参数由LOCKER模板参数决定
template <class LOCKER>
class ThreadSafeString : public string
{
public:
ThreadSafeString(const char *s) : string(s) {}
...
int length();
private:
LOCKER lock;//锁类型固定,由LOCKER模板参数决定(实际执行处传入的是子类类型)
};
template <class LOCKER>
inline
int ThreadSafeString<LOCKER>::length()
{
lock.lock();
int len = string::length();
lock.unlock();
return len;
}
//实例化模板
ThreadSafeString <CriticalSectionLock> csString ="hello";
或者使用互斥
ThreadSafeString <MutexLock> mtxString = "hello";
这种设计也避免了对lock和unlock的虚函数调用。ThreadSafeString声明在实例化模板时选择特定的同步类型。如同硬编码一样,它使编译器可以解析这两个虚函数调用并且内联它们。
模板把计算从执行期间提前到编译期间来做,并且在编译时使用内联,因此提高了性能。
从上面几种方式能得出下面的结论:
①在测试类里建立lock类型的成员对象,如果类型已经固定成子类类型,则lock类的虚函数可以被适当的编译器静态地解析;
②在测试类里,类型在编译时具体类型还未固定(暂时是基类类型,使用多态机制运行时才能固定类型),则lock类的虚函数不能被编译器静态地解析;
总结
● 虚函数的代价在于无法内联函数调用,因为这些调用是在运行时动态绑定的。唯一潜在的效率问题是从内联获得的速度(如果可以内联的话)。但对于那些代价并非取决于调用和返回开销的函数来说,内联效率不是问题。
● 模板比继承提供更好的性能。它把对类型的解析提前到编译期间,我们认为这是没有成本的。