解决虚函数性能问题,选择模板还是继承?

根据上篇文章《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类的虚函数不能被编译器静态地解析;


总结

虚函数的代价在于无法内联函数调用,因为这些调用是在运行时动态绑定的。唯一潜在的效率问题是从内联获得的速度(如果可以内联的话)。但对于那些代价并非取决于调用和返回开销的函数来说,内联效率不是问题。

模板比继承提供更好的性能。它把对类型的解析提前到编译期间,我们认为这是没有成本的

原文链接:,转发请注明来源!