C++/编译:是否可以设置VPTR(全局vtable + 2字节索引)的大小
C++/compilation : is it possible to set the size of the vptr (global vtable + 2 bytes index)
我最近发布了一个关于C++虚拟性导致的内存开销的问题。答案使我能够理解vtable和vptr的工作原理。我的问题是:我在超级计算机上工作,我有数十亿个对象,因此我必须关心由于虚拟性而导致的内存开销。经过一些措施,当我使用带有虚函数的类时,每个派生对象都有其 8 字节的 vptr。这完全不能忽略不计。
我想知道英特尔 icpc 或 g++ 是否有一些配置/选项/参数,以使用具有可调精度的"全局"vtable 和索引而不是 vptr。因为这样的事情将允许我对数十亿个对象使用 2 字节索引(无符号短 int(而不是 8 字节 vptr(并很好地减少内存开销(。有没有办法用编译选项做到这一点(或类似的东西(?
谢谢。
不幸的是...不是自动的。
但请记住,v-table 只不过是运行时多态性的语法糖。如果您愿意重新设计代码,有几种选择。
- 外部多态性
- 手工制作的V型表
- 手工制作的多态性
1(外部多态性
这个想法是,有时你只需要瞬态的多态性。也就是说,例如:
std::vector<Cat> cats;
std::vector<Dog> dogs;
std::vector<Ostrich> ostriches;
void dosomething(Animal const& a);
在这种情况下嵌入虚拟指针对于Cat
或Dog
来说似乎是浪费,因为您知道动态类型(它们按值存储(。
外部多态性是关于具有纯混凝土类型和纯接口,以及中间的简单桥接,以暂时(或永久,但这不是您想要的(使具体类型适应接口。
// 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 指针的工作原理,那么您可以自己完美地工作。
struct FooVTable {
typedef void (Foo::*DoFunc)(int, int);
DoFunc _do;
};
然后为锚定在 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
需要包含其派生类的所有标头。这有效地密封了层次结构。
我自愿提出了从最开放的解决方案到最封闭的解决方案。它们具有不同程度的复杂性/灵活性,由您选择最适合您的一种。
一件重要的事情,在后两种情况下,销毁和复制需要特别小心。
- 从不同线程使用int64的不同字节安全吗
- 将Integer转换为4字节的unsined字符矢量(按大端字节顺序)
- 在UNIX系统中使用DIR查找文件的字节大小
- 如何使用Crypto++并为RSA返回可打印的字节/字符数组
- std::当在256字节边界上写入整数时,流的奇怪行为
- 当比特(而不是字节)的顺序至关重要时的持久性
- 从文件中读取多个字节,并将它们存储在C++中进行比较
- 如何在文件中查找字节序列
- luaL_dofile在已知良好的字节码上失败,可以使用未编译的版本
- 字节到位运算符重载C++
- 在java中读取c++字节的位字段
- 使用 std::vector::reverse_iterator 将 int 序列化为字节向量?
- 字节真的是最小可寻址单元吗
- struct.error:解压缩 C++ 结构时,解包需要 288 字节的缓冲区
- 读取文件中所有可能的十六进制 16 字节序列并打印每个序列
- 如何使用 OpenCV 解码在两个 UWP 应用之间发送的图像字节?
- 如何将字节数组元素替换为修改的十六进制 ASCII 符号?
- asn1c 不会从 asn.1 模块中提取八位字节字符串的默认值
- 如何将原始字节附加到 std::vector?
- C++/编译:是否可以设置VPTR(全局vtable + 2字节索引)的大小