7

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

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

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

相当于说 C 会是这样的

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

第一个成员通常是指向虚函数列表等的指针(应用程序无法控制的内存中的一块区域)。在大多数情况下,在考虑成员等之前,这恰好花费了指针的大小。因此,在 32 位寻址方案中,大约 4 个字节等。如果您在应用程序中创建了 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 足以容纳索引,从而减少应用程序中所有多态类的大小(我不包括对齐问题, ETC)。

我的问题是,GNU C++、LLVM 或任何其他编译器中是否有任何开关可以做到这一点?或减小多态对象的大小?

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

4

4 回答 4

3

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

于 2012-04-12T14:12:52.977 回答
3

不,没有这样的开关。

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

然后,在调用该方法之前,虚拟性由 a switchon theenum和适当的强制转换实现。再次关闭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以避免填充...

于 2012-04-12T14:54:38.753 回答
3

几点观察:

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

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

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

  4. 为了让程序能够独立编译,整个程序中的类的数量,因此在编译时必须知道class-id的大小,否则无法编译代码来访问它。这将是一笔巨大的开销。在最坏的情况下修复它更简单,并简化编译和链接。

请不要让我阻止您尝试,但是使用任何可能使用可变大小 id 来派生函数地址的技术来解决更多问题。

我强烈建议您在Wikipedia Cola上查看Ian Piumarta 的可乐

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

于 2012-04-12T14:20:50.740 回答
2

简短的回答是不,我不知道有任何开关可以使用任何常见的 C++ 编译器来执行此操作。

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

我还要指出,它通常不会有很多好处。至少在典型情况下,您希望结构/类中的每个元素都处于“自然”边界,这意味着它的起始地址是其大小的倍数。使用包含单个 int 的类的示例,编译器将为 vtable 索引分配一个字节,然后立即填充三个字节,以便下一个int将落在一个四的倍数的地址处。最终结果将是类的对象将占用与我们使用指针完全相同的存储量

我要补充一点,这也不是一个牵强的例外。多年来,最小化插入到结构/类中的填充的标准建议是,将预期最大的项目放在开头,然后朝着最小的方向发展。这意味着在大多数代码中,您最终会在结构的第一个显式定义的成员之前得到相同的三个字节的填充。

要从中获得任何好处,您必须意识到这一点,并拥有一个包含(例如)三个字节数据的结构,您可以将其移动到您想要的位置。然后你会将它们移动到结构中明确定义的第一个项目。不幸的是,这也意味着如果您关闭此开关以便拥有一个 vtable 指针,最终编译器会插入可能不必要的填充。

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

于 2012-04-12T14:51:25.807 回答