实现vptr的替代方案?

时间:2012-04-12 14:02:04

标签: c++ compiler-construction compiler-optimization micro-optimization vptr

这个问题不是关于C ++语言本身(即不是关于标准),而是关于如何调用编译器来实现虚函数的替代方案。

实现虚函数的一般方案是使用指向指针表的指针。

class Base {
     private:
        int m;
     public:
        virtual metha();
};

等效地说C会像

struct Base {
    void (**vtable)();
    int m;
}

第一个成员通常是指向虚拟函数列表等的指针(应用程序无法控制的内存中的一块区域)。在大多数情况下,这会在考虑成员之前花费指针的大小等等。因此在大约4个字节的32位寻址方案中等等。如果在应用程序中创建了40k多态对象的列表,则大约为40k x在任何成员变量等之前4个字节= 160k字节。我也知道这恰好是C ++编译中最快和最常见的实现。

我知道多重继承很复杂(尤其是虚拟类,即菱形结构等)。

另一种方法是将第一个变量作为vptrs表的索引id(等效于C,如下所示)

struct Base {
    char    classid;     // the classid here is an index into an array of vtables
    int     m;
}

如果应用程序中的类总数小于255(包括所有可能的模板实例化等),则char足以保存索引,从而减少应用程序中所有多态类的大小(我是排除对齐问题等。)

我的问题是,在GNU C ++,LLVM或任何其他编译器中是否有任何切换来执行此操作?或减少多态对象的大小?

编辑:我了解指出的对齐问题。还有一点,如果这是64位系统(假设为64位vptr),每个多态对象成员的成本约为8字节,那么vptr的成本就是内存的50%。这主要涉及大量创建的小型多态,所以我想知道如果不是整个应用程序,这个方案是否至少可以用于特定的虚拟对象。

4 个答案:

答案 0 :(得分:3)

您的建议很有意思,但如果可执行文件由多个模块组成,并在其中传递对象,则无效。鉴于它们是单独编译的(比如DLL),如果一个模块创建一个对象并将其传递给另一个,另一个模块调用一个虚方法 - 它如何知道classid引用哪个表?您将无法添加另一个moduleid,因为这两个模块在编译时可能不会彼此了解。所以除非你使用指针,否则我认为这是一个死胡同......

答案 1 :(得分:3)

有几点意见:

  1. 是的,可以使用较小的值来表示类,但是某些处理器需要对齐数据,以便通过将数据值与例如对齐的要求可能会丢失空间节省。 4字节边界。此外,class-id必须位于多态继承树的所有成员的明确定义的位置,因此它可能超过其他日期,因此无法避免对齐问题。

  2. 存储指针的成本已经移到代码中,每次使用多态函数都需要代码将class-id转换为vtable指针或某个等效的数据结构。所以它不是免费的。显然,成本权衡取决于代码量与对象的数量。

  3. 如果从堆中分配对象,则通常会在orer中浪费空间以确保对象被调到最差边界,因此即使存在少量代码和大量多态对象,内存管理开销明显大于指针和char之间的差异。

  4. 为了允许程序独立编译,整个程序中的类数量,以及类ID的大小必须在编译时知道,否则代码无法编译访问它。这将是一个重大的开销。在最坏的情况下修复它更简单,并简化编译和链接。

  5. 请不要让我阻止你尝试,但是使用任何可能使用可变大小id来获取函数地址的技术来解决还有很多问题。

    我强烈建议您Ian Piumarta's Cola

    查看Wikipedia Cola

    它实际上采用了不同的方法,并以更灵活的方式使用指针,以构建继承,或基于原型,或开发人员需要的任何其他机制。

答案 2 :(得分:3)

不,没有这样的转换。

LLVM / Clang代码库避免了由成千上万的类分配的类中的虚拟表:这在封闭层次结构中运行良好,因为单个enum可以枚举所有可能的类然后每个类都链接到enum的值。 已关闭显然是因为enum

然后,虚拟性由switch上的enum实现,并在调用方法之前进行适当的转换。再一次,关闭。必须为每个新类修改switch


第一种选择:外部vpointer。

如果您发现自己处于过度支付vpointer税的情况,那么大部分对象都是已知类型。然后你可以外化它。

class Interface {
public:
  virtual ~Interface() {}

  virtual Interface* clone() const = 0; // might be worth it

  virtual void updateCount(int) = 0;

protected:
  Interface(Interface const&) {}
  Interface& operator=(Interface const&) { return *this; }
};

template <typename T>
class InterfaceBridge: public Interface {
public:
  InterfaceBridge(T& t): t(t) {}

  virtual InterfaceBridge* clone() const { return new InterfaceBridge(*this); }

  virtual void updateCount(int i) { t.updateCount(i); }

private:
  T& t; // value or reference ? Choose...
};

template <typename T>
InterfaceBridge<T> interface(T& t) { return InterfaceBridge<T>(t); }

然后,想象一个简单的课程:

class Counter {
public:
  int getCount() const { return c; }
  void updateCount(int i) { c = i; }
private:
  int c;
};

您可以将对象存储在数组中:

static Counter array[5];

assert(sizeof(array) == sizeof(int)*5); // no v-pointer

仍然使用多态函数:

void five(Interface& i) { i.updateCount(5); }

InterfaceBridge<Counter> ib(array[3]); // create *one* v-pointer
five(ib);

assert(array[3].getCount() == 5);

值与参考值实际上是设计张力。通常,如果需要clone,则需要按值存储,并且需要在按基类存储时进行克隆(例如boost::ptr_vector)。实际上可以提供两个接口(和桥接):

Interface <--- ClonableInterface
  |                 |
InterfaceB     ClonableInterfaceB

这只是额外打字。


另一个解决方案,更多涉及。

开关可通过跳转表实现。这样的表可以在运行时完美地创建,例如std::vector

class Base {
public:
  ~Base() { VTables()[vpointer].dispose(*this); }

  void updateCount(int i) {
    VTables()[vpointer].updateCount(*this, i);
  }

protected:
  struct VTable {
    typedef void (*Dispose)(Base&);
    typedef void (*UpdateCount)(Base&, int);

    Dispose dispose;
    UpdateCount updateCount;
  };

  static void NoDispose(Base&) {}

  static unsigned RegisterTable(VTable t) {
    std::vector<VTable>& v = VTables();
    v.push_back(t);
    return v.size() - 1;
  }

  explicit Base(unsigned id): vpointer(id) {
    assert(id < VTables.size());
  }

private:
  // Implement in .cpp or pay the cost of weak symbols.
  static std::vector<VTable> VTables() { static std::vector<VTable> VT; return VT; }

  unsigned vpointer;
};

然后,Derived类:

class Derived: public Base {
public:
  Derived(): Base(GetID()) {}

private:
  static void UpdateCount(Base& b, int i) {
    static_cast<Derived&>(b).count = i;
  }

  static unsigned GetID() {
    static unsigned ID = RegisterTable(VTable({&NoDispose, &UpdateCount}));
    return ID;
  }

  unsigned count;
};

好吧,现在你会意识到编译器为你做了多么棒的事情,即使是以一些开销为代价。

哦,由于对齐,只要Derived类引入指针,就有可能在Base和下一个属性之间使用4个字节的填充。您可以通过仔细选择Derived中的前几个属性来使用它们,以避免填充...

答案 3 :(得分:2)

简短的回答是,不,我不知道有任何转换使用任何常见的C ++编译器。

更长的答案是,为了做到这一点,你只需要将大部分智能构建到链接器中,这样它就可以协调在链接在一起的所有目标文件中分配ID。

我还要指出,它通常不会带来很多好处。至少在典型情况下,您希望struct / class中的每个元素都处于“自然”边界,这意味着它的起始地址是其大小的倍数。使用包含单个int的类的示例,编译器将为vtable索引分配一个字节,紧接着是三个填充的字节,因此下一个int将落在一个4的倍数的地址。最终的结果是类的对象将占用精确相同的存储量,就像我们使用指针一样。

我补充一点,这也不是一个牵强附会的例外。多年来,标准建议是尽量减少插入结构/类别中的填充,以使预期的项目在开始时最大,并向最小的方向发展。这意味着在大多数代码中,在结构的第一个显式定义的成员之前,你最终会得到相同的三个填充字节。

要从中获得任何好处,你必须要注意它,并且有一个结构(例如)你可以移动到你想要的三个字节的数据。然后你将它们移动到结构中显式定义的第一个项目。不幸的是,这也意味着如果你关闭了这个开关,所以你有一个vtable指针,你最终会得到编译器插入填充,否则可能是不必要的。

总结一下:它没有实现,如果它通常不会实现太多。