gpt4 book ai didi

c++ - C++/编译: is it possible to set the size of the vptr (global vtable + 2 bytes index)

转载 作者:IT老高 更新时间:2023-10-28 22:13:59 26 4
gpt4 key购买 nike

我最近发布了一个有关由于C++中的虚拟性而导致的内存开销的问题。答案使我了解了vtable和vptr的工作原理。
我的问题如下:我在 super 计算机上工作,我有数十亿个对象,因此,由于虚拟性,我必须关心内存开销。经过一些措施,当我将类与虚函数一起使用时,每个派生对象都有其8字节的vptr。这一点一点都不能忽略。

我不知道英特尔icpc或g++是否具有某些配置/选项/参数,以使用精度可调的“全局” vtable和索引而不是vptr。因为这样可以让我为2亿个对象使用2字节的索引(无符号short int)而不是8字节的vptr(这样可以大大减少内存开销)。有没有办法用编译选项来做到这一点(或类似的东西)?

非常感谢你。

最佳答案

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

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

  • 外部多态性
  • 手工制作的v-表
  • 手工多态


  • 1)外部多态性

    这个想法是有时您只需要以瞬时方式进行多态。也就是说,例如:
    std::vector<Cat> cats;
    std::vector<Dog> dogs;
    std::vector<Ostrich> ostriches;

    void dosomething(Animal const& a);

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

    外部多态性是关于具有纯具体类型和纯接口(interface)的,以及在中间有一个简单的桥来临时(或永久地,但在这里不是您想要的)使具体类型适应接口(interface)。
    // 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);
    };

    该桥是一劳永逸的:
    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); }

    您可以这样使用它:
    for (auto const& c: cats) { dosomething(iface_animal(c)); }

    每个项目会产生两个指针的开销,但前提是您需要多态性。

    一种替代方法是让 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; }

    然后修改一下网桥:
    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); }

    这样,您可以选择何时需要多态存储,何时不希望存储。

    2)手工制作的v-表

    (仅适用于封闭的阶层)

    在C语言中,通常通过提供自己的v表机制来模拟面向对象。由于您似乎知道什么是v表以及v指针的工作方式,因此您可以自己完美地工作。
    struct FooVTable {
    typedef void (Foo::*DoFunc)(int, int);

    DoFunc _do;
    };

    然后为 anchor 定在 Foo中的层次结构提供一个全局数组:
    extern FooVTable const* const FooVTableFoo;
    extern FooVTable const* const FooVTableBar;

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

    enum class FooVTableIndex: unsigned short {
    Foo,
    Bar
    };

    然后,您需要在 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;
    };

    之所以存在封闭的层次结构,是因为 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;
    };

    这些开关很烦人,但是它们可以或多或少地自动播放一些宏和类型列表。 LLVM通常使用以下文件:
     // FooList.inc
    ACT_ON(Foo)
    ACT_ON(Bar)
    ACT_ON(Bor)

    然后您执行以下操作:
     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");
    }

    克里斯·拉特纳(Chris Lattner)评论说,由于如何生成开关(使用代码偏移量表),因此产生的代码类似于虚拟调度的代码,因此具有大致相同的CPU开销,但内存开销较低。

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

    我自愿介绍了从最开放的解决方案到最封闭的解决方案。它们具有不同程度的复杂性/灵活性,由您选择最适合您的一个。

    重要的是,在后两种情况下,销毁和复制需要特别注意。

    关于c++ - C++/编译: is it possible to set the size of the vptr (global vtable + 2 bytes index),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/10562268/

    26 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com