C++/compilation:是否可以设置vptr的大小(全局vtable + 2字节索引)

Vin*_*ent 17 c++ compilation g++ intel vptr

我最近发布了一个关于由于C++中的虚拟性导致的内存开销的问题.答案让我了解vtable和vptr的工作原理.我的问题如下:我在超级计算机上工作,我有数十亿个对象,因此我不得不关心由于虚拟性造成的内存开销.经过一些测量,当我使用具有虚函数的类时,每个派生对象都有其8字节的vptr.这根本不可忽视.

我想知道intel icpc或g ++是否有一些配置/选项/参数,使用"全局"vtable和可调精度的索引而不是vptr.因为这样的事情将允许我使用2字节索引(unsigned short int)而不是8字节vptr用于数十亿个对象(并且很好地减少了内存开销).有没有办法用编译选项做到这一点(或类似的东西)?

非常感谢你.

Mat*_* M. 17

不幸的是......不是自动的

但请记住,v-table只不过是运行时多态性的语法糖.如果您愿意重新设计代码,有几种选择.

  1. 外部多态性
  2. 手工制作的V桌
  3. 手工制作的多态性

1)外部多态性

我们的想法是,有时候你只需要一种瞬态的多态性.也就是说,例如:

std::vector<Cat> cats;
std::vector<Dog> dogs;
std::vector<Ostrich> ostriches;

void dosomething(Animal const& a);
Run Code Online (Sandbox Code Playgroud)

在这种情况下嵌入虚拟指针似乎很浪费CatDog因为您知道动态类型(它们按值存储).

外部多态性是关于具有纯粹的具体类型和纯接口,以及在中间暂时(或永久地,但不是您想要的)简单的桥接器将具体类型适应于接口.

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

    virtual size_t age() const = 0;
    virtual size_t weight() const = 0;

    virtual void eat(Food const&) = 0;
    virtual void sleep() = 0;

private:
    Animal(Animal const&) = delete;
    Animal& operator=(Animal const&) = delete;
};

// Concrete class
class Cat {
public:
    size_t age() const;
    size_t weight() const;

    void eat(Food const&);
    void sleep(Duration);
};
Run Code Online (Sandbox Code Playgroud)

这座桥是一劳永逸地写的:

template <typename T>
class AnimalT: public Animal {
public:
    AnimalT(T& r): _ref(r) {}

    virtual size_t age() const override { return _ref.age(); }
    virtual size_t weight() const { return _ref.weight(); }

    virtual void eat(Food const& f) override { _ref.eat(f); }
    virtual void sleep(Duration const d) override { _ref.sleep(d); }

private:
    T& _ref;
};

template <typename T>
AnimalT<T> iface_animal(T& r) { return AnimalT<T>(r); }
Run Code Online (Sandbox Code Playgroud)

你可以使用它:

for (auto const& c: cats) { dosomething(iface_animal(c)); }
Run Code Online (Sandbox Code Playgroud)

它会产生每个项目两个指针的开销,但只要你需要多态性.

另一种方法是AnimalT<T>使用值(而不是引用)并提供一种clone方法,它允许您根据情况在具有v指针之间完全选择.

在这种情况下,我建议使用一个简单的类:

template <typename T> struct ref { ref(T& t): _ref(t); T& _ref; };

template <typename T>
T& deref(T& r) { return r; }

template <typename T>
T& deref(ref<T> const& r) { return r._ref; }
Run Code Online (Sandbox Code Playgroud)

然后修改一下桥:

template <typename T>
class AnimalT: public Animal {
public:
    AnimalT(T r): _r(r) {}

    std::unique_ptr< Animal<T> > clone() const { return { new Animal<T>(_r); } }

    virtual size_t age() const override { return deref(_r).age(); }
    virtual size_t weight() const { return deref(_r).weight(); }

    virtual void eat(Food const& f) override { deref(_r).eat(f); }
    virtual void sleep(Duration const d) override { deref(_r).sleep(d); }

private:
    T _r;
};

template <typename T>
AnimalT<T> iface_animal(T r) { return AnimalT<T>(r); }

template <typename T>
AnimalT<ref<T>> iface_animal_ref(T& r) { return Animal<ref<T>>(r); }
Run Code Online (Sandbox Code Playgroud)

这种方式可以选择何时需要多态存储,何时不需要.


2)手工制作的v-tables

(只能在封闭的层级上轻松工作)

在C中通常通过提供自己的v-table机制来模拟面向对象.由于您似乎知道v-table是什么以及v-pointer如何工作,因此您可以自己完美地完成它.

struct FooVTable {
    typedef void (Foo::*DoFunc)(int, int);

    DoFunc _do;
};
Run Code Online (Sandbox Code Playgroud)

然后为锚定在以下内容的层次结构提供全局数组Foo:

extern FooVTable const* const FooVTableFoo;
extern FooVTable const* const FooVTableBar;

FooVTable const* const FooVTables[] = { FooVTableFoo, FooVTableBar };

enum class FooVTableIndex: unsigned short {
    Foo,
    Bar
};
Run Code Online (Sandbox Code Playgroud)

那么你所需要的Foo就是保持最派生的类型:

class Foo {
public:

    void dofunc(int i, int j) {
        (this->*(table()->_do))(i, j);
    }

protected:
    FooVTable const* table() const { return FooVTables[_vindex]; }

private:
    FooVTableIndex _vindex;
};
Run Code Online (Sandbox Code Playgroud)

由于FooVTables数组和FooVTableIndex枚举需要知道层次结构的所有类型,因此封闭的层次结构存在.

可以绕过枚举索引,并且通过使数组非常量,可以预先初始化为更大的大小,然后在init处使每个派生类型自动地在那里注册.因此在初始化阶段期间检测到索引的冲突,甚至可以具有自动解决方案(扫描阵列以获得空闲时隙).

这可能不太方便,但确实提供了打开层次结构的方法.显然,启动任何线程之前编码更容易,因为我们在这里讨论全局变量.


3)手工制作的多态性

(仅适用于封闭层次结构)

后者基于我探索LLVM/Clang代码库的经验.编译器遇到的问题与您面临的问题完全相同:对于数十或数十万个小项目,每个项目的vpointer确实会增加内存消耗,这很烦人.

因此,他们采取了一种简单的方法:

  • 每个类层次结构都有一个enum列出所有成员的伴侣
  • 层次结构中的每个类在enumerator构造时将其伴随传递给它的基础
  • 通过enum适当地切换和铸造来实现虚拟性

在代码中:

enum class FooType { Foo, Bar, Bor };

class Foo {
public:
    int dodispatcher() {
        switch(_type) {
        case FooType::Foo:
            return static_cast<Foo&>(*this).dosomething();

        case FooType::Bar:
            return static_cast<Bar&>(*this).dosomething();

        case FooType::Bor:
            return static_cast<Bor&>(*this).dosomething();
        }
        assert(0 && "Should never get there");
    }
private:
    FooType _type;
};
Run Code Online (Sandbox Code Playgroud)

交换机非常烦人,但它们或多或少可以自动播放一些宏和类型列表.LLVM通常使用如下文件:

 // FooList.inc
 ACT_ON(Foo)
 ACT_ON(Bar)
 ACT_ON(Bor)
Run Code Online (Sandbox Code Playgroud)

然后你做:

 void Foo::dodispatcher() {
     switch(_type) {
 #   define ACT_ON(X) case FooType::X: return static_cast<X&>(*this).dosomething();

 #   include "FooList.inc"

 #   undef ACT_ON
     }

     assert(0 && "Should never get there");
 }
Run Code Online (Sandbox Code Playgroud)

Chris Lattner评论说,由于如何生成交换机(使用代码偏移表),这产生的代码类似于虚拟调度的代码,因此具有大约相同的CPU开销量,但是用于较低的内存开销.

显然,一个缺点是Foo.cpp需要包含其派生类的所有头.这有效地密封了层次结构.


我自愿提出从最开放的解决方案到最封闭的解决方案.它们具有不同程度的复杂性/灵活性,您可以自行选择最适合您的复杂性/灵活性.

一个重要的事情,在后两种情况下,破坏和复制需要特别小心.