在 windbg "x /2" 结果中强制执行 vftable 条目,需要考虑什么?
Enforcing a vftable entry in windbg "x /2" results, what to consider?
(这是一个关于软件设计的大问题。如果它不适用于 Whosebug,我愿意将其复制到软件工程社区)
我正在使用 heap_stat,一个调查转储的脚本。这个脚本基于这样的想法,对于任何具有虚函数的对象,vftable
字段总是第一个(允许找到对象的 class 的内存地址)。
在我的应用程序中有一些对象有 vftable
个条目(通常每个 STL
个对象都有),但也有相当多的对象没有。
为了强制出现 vftable
字段,我做了以下测试:
创建一个废话class,有一个虚函数,让我的class继承这个废话class:
class NONSENSE {
virtual int nonsense() { return 0; }
};
class Own_Class : public NONSENSE, ...
正如预期的那样,这在符号中创建了一个 vftable
条目,我可以找到它(使用 Windbg
的 x /2 *!Own_Class*vftable*
命令):
00000000`012da1e0 Own_Application!Own_Class::`vftable'
我还看到了内存使用的差异:
sizeof(an normal Own_Class object) = 2928
sizeof(inherited Own_Class object) = 2936
=> 已为此对象添加 8 个字节。
有一个问题:显然有相当多的对象被定义为:
class ATL_NO_VTABLE Own_Class
此 ATL_NO_VTABLE
阻止创建 vftable
条目,这意味着以下内容(ATL_NO_VTABLE
等于 __declspec(novtable)
):
// __declspec(novtable) is used on a class declaration to prevent the vtable
// pointer from being initialized in the constructor and destructor for the
// class. This has many benefits because the linker can now eliminate the
// vtable and all the functions pointed to by the vtable. Also, the actual
// constructor and destructor code are now smaller.
在我看来,这意味着 vftable
不会被创建,因为对象方法被更直接地调用,对方法执行和堆栈处理的速度有影响。允许创建 vftable
具有以下影响:
不予考虑:
- 堆栈上还有一个调用,这只对内存使用量已经达到极限的系统有影响。 (我不知道链接器是如何指向特定方法的)
- CPU 使用量增长太小,无法看到。
- 速度下降会小到看不见。
要考虑:
- 如前所述,应用程序的内存使用量每个对象增加 8 个字节。当一个常规对象的大小为 1000 字节左右时,这意味着内存使用量增加了 ±1%,但是对于内存大小小于 80 字节的对象,这可能会导致内存使用量增加 +10%。
现在我有以下问题:
- 我对影响的分析是否正确?
- 有没有更好的方法强制创建
vftable
字段,影响更小?
- 我错过了什么吗?
提前致谢
Is my analysis on the impact correct?
没有。 __declspec(novtable)
省略了给定 class 的 vtable 本身 的生成,指向 vtable 的 指针仍然存在,因此 sizeof 将不变。
__declspec(novtable)
用于派生 classes 的基础 classes。因此派生的构造函数 class 会将 vtable 指针设置为派生 vtable,不需要基 vtable。
因此,此优化消除了一个指针赋值(在构造函数代码的生成部分),以及 vtable 本身的一点 space。 per-object 优化对你的目标不是很有用,因为它只做小的 per-class 优化。
如果您不自己创建基础实例,并且不在 constructor/destructor 中调用虚拟方法,它就可以工作。
通过使它们成为非虚函数来省略虚函数调用是完全不同的故事。它被称为去虚拟化。当编译器可以确定使用了 class 的实例时,它会将虚拟调用替换为非虚拟调用。
__declspec(novtable)
无论如何都无法帮助去虚拟化。 final
/ sealed
关键字可能有助于去虚拟化,因为他们说没有进一步派生 classes/methods。
关于 vtable 指针是第一个成员的假设,这可能是错误的。如果你的基础 classes 没有 vtable,但有一些数据成员,vtable 指针将不是第一个。也可能有不止一个虚表指针。
要以编程方式分析转储中的结构,我建议使用适当的 API。有两个 API:DIA SDK and dbghelp functions。它们很相似,但第一个是基于对象 (COM) 的,第二个是平面的 API,所以第一个可能更容易使用。
由于 heap_stat 脚本的方法存在固有的局限性,我建议堆分析使用 UMDH,它根本不依赖 vtable,并显示各种对象
与此同时,我发现了一种非常简单的方法来强制每个 class 的 vftable'
条目:只需将每个析构函数声明为虚拟的。
为了找到所有还不是虚拟的析构函数,我在开发目录中的 Ubuntu 应用程序中启动了以下命令:
find ./ -name "*.h" -exec fgrep "~" {} /dev/null \; | grep -v "virtual"
在将所有析构函数声明为虚函数后,我打算做一些性能测试(我认为将方法声明为虚函数可能会对速度产生影响,因为方法声明已更改,尤其是对于负载较重的服务器应用程序),我会post 保持最新。
(这是一个关于软件设计的大问题。如果它不适用于 Whosebug,我愿意将其复制到软件工程社区)
我正在使用 heap_stat,一个调查转储的脚本。这个脚本基于这样的想法,对于任何具有虚函数的对象,vftable
字段总是第一个(允许找到对象的 class 的内存地址)。
在我的应用程序中有一些对象有 vftable
个条目(通常每个 STL
个对象都有),但也有相当多的对象没有。
为了强制出现 vftable
字段,我做了以下测试:
创建一个废话class,有一个虚函数,让我的class继承这个废话class:
class NONSENSE {
virtual int nonsense() { return 0; }
};
class Own_Class : public NONSENSE, ...
正如预期的那样,这在符号中创建了一个 vftable
条目,我可以找到它(使用 Windbg
的 x /2 *!Own_Class*vftable*
命令):
00000000`012da1e0 Own_Application!Own_Class::`vftable'
我还看到了内存使用的差异:
sizeof(an normal Own_Class object) = 2928
sizeof(inherited Own_Class object) = 2936
=> 已为此对象添加 8 个字节。
有一个问题:显然有相当多的对象被定义为:
class ATL_NO_VTABLE Own_Class
此 ATL_NO_VTABLE
阻止创建 vftable
条目,这意味着以下内容(ATL_NO_VTABLE
等于 __declspec(novtable)
):
// __declspec(novtable) is used on a class declaration to prevent the vtable
// pointer from being initialized in the constructor and destructor for the
// class. This has many benefits because the linker can now eliminate the
// vtable and all the functions pointed to by the vtable. Also, the actual
// constructor and destructor code are now smaller.
在我看来,这意味着 vftable
不会被创建,因为对象方法被更直接地调用,对方法执行和堆栈处理的速度有影响。允许创建 vftable
具有以下影响:
不予考虑:
- 堆栈上还有一个调用,这只对内存使用量已经达到极限的系统有影响。 (我不知道链接器是如何指向特定方法的)
- CPU 使用量增长太小,无法看到。
- 速度下降会小到看不见。
要考虑:
- 如前所述,应用程序的内存使用量每个对象增加 8 个字节。当一个常规对象的大小为 1000 字节左右时,这意味着内存使用量增加了 ±1%,但是对于内存大小小于 80 字节的对象,这可能会导致内存使用量增加 +10%。
现在我有以下问题:
- 我对影响的分析是否正确?
- 有没有更好的方法强制创建
vftable
字段,影响更小? - 我错过了什么吗?
提前致谢
Is my analysis on the impact correct?
没有。 __declspec(novtable)
省略了给定 class 的 vtable 本身 的生成,指向 vtable 的 指针仍然存在,因此 sizeof 将不变。
__declspec(novtable)
用于派生 classes 的基础 classes。因此派生的构造函数 class 会将 vtable 指针设置为派生 vtable,不需要基 vtable。
因此,此优化消除了一个指针赋值(在构造函数代码的生成部分),以及 vtable 本身的一点 space。 per-object 优化对你的目标不是很有用,因为它只做小的 per-class 优化。
如果您不自己创建基础实例,并且不在 constructor/destructor 中调用虚拟方法,它就可以工作。
通过使它们成为非虚函数来省略虚函数调用是完全不同的故事。它被称为去虚拟化。当编译器可以确定使用了 class 的实例时,它会将虚拟调用替换为非虚拟调用。
__declspec(novtable)
无论如何都无法帮助去虚拟化。 final
/ sealed
关键字可能有助于去虚拟化,因为他们说没有进一步派生 classes/methods。
关于 vtable 指针是第一个成员的假设,这可能是错误的。如果你的基础 classes 没有 vtable,但有一些数据成员,vtable 指针将不是第一个。也可能有不止一个虚表指针。
要以编程方式分析转储中的结构,我建议使用适当的 API。有两个 API:DIA SDK and dbghelp functions。它们很相似,但第一个是基于对象 (COM) 的,第二个是平面的 API,所以第一个可能更容易使用。
由于 heap_stat 脚本的方法存在固有的局限性,我建议堆分析使用 UMDH,它根本不依赖 vtable,并显示各种对象
与此同时,我发现了一种非常简单的方法来强制每个 class 的 vftable'
条目:只需将每个析构函数声明为虚拟的。
为了找到所有还不是虚拟的析构函数,我在开发目录中的 Ubuntu 应用程序中启动了以下命令:
find ./ -name "*.h" -exec fgrep "~" {} /dev/null \; | grep -v "virtual"
在将所有析构函数声明为虚函数后,我打算做一些性能测试(我认为将方法声明为虚函数可能会对速度产生影响,因为方法声明已更改,尤其是对于负载较重的服务器应用程序),我会post 保持最新。