为什么C++的初始分配比C要大得多?

141

在使用相同的代码时,仅仅改变编译器(从 C 编译器到 C++ 编译器),将会改变内存分配的数量。我不太确定为什么会这样,希望更好地理解它。到目前为止,我得到的最好的回复是 "可能是 I/O 流",这并不是很详细,让我想起了 C++ 的“你所不用,即你所不付”的方面。

我正在使用 Clang 和 GCC 编译器,版本分别为 7.0.1-8 和 8.3.0-6。我的系统运行在 Debian 10(Buster)上,是最新版本。测试基于 Valgrind Massif 进行。

#include <stdio.h>

int main() {
    printf("Hello, world!\n");
    return 0;
}

这里使用的代码没有改变,但是编译成C语言或C++语言会影响Valgrind基准测试的结果。然而,这些值在不同编译器下保持一致。程序的运行时内存分配情况(峰值)如下:

  • GCC (C): 1,032字节(1 KB)
  • G++ (C++): 73,744字节(~74 KB)
  • Clang (C): 1,032字节(1 KB)
  • Clang++ (C++): 73,744字节(~74 KB)

编译命令如下:

clang -O3 -o c-clang ./main.c
gcc -O3 -o c-gcc ./main.c
clang++ -O3 -o cpp-clang ./main.cpp
g++ -O3 -o cpp-gcc ./main.cpp
对于Valgrind,我在每个编译器和语言上运行valgrind --tool=massif --massif-out-file=m_compiler_lang ./compiler-lang,然后使用ms_print来显示峰值。这样做有问题吗?

11
首先,你是如何进行构建的?你使用了什么选项?又是如何测量的?你如何运行 Valgrind? - Some programmer dude
17
如果我没记错的话,现代的C++编译器有一种异常模型,在进入try块时不会有性能损失,但会增加内存占用,可能采用跳转表等方式实现。你可以尝试编译时禁用异常,看看对程序的影响如何。另外,你也可以逐步禁用其他C++特性,观察它们对内存占用的影响。 - François Andrieux
3
当使用clang++ -xc编译而不是clang时,出现了相同的分配情况,这强烈暗示其原因是由于链接的库。 - Justin
14
这确实是C ++,我没有看到它违反任何C ++规范的部分......除了可能包含stdio.h而不是cstdio,但至少在较旧的C ++版本中允许这样做。您认为这个程序中有什么“畸形”吗? - Vality
4
我发现那些gcc和clang编译器在"C"模式下生成的字节数与在"C++"模式下生成的字节数完全相同,这让我感到可疑。你是否犯了录错字? - RonJohn
显示剩余6条评论
2个回答

150
堆使用量来自C++标准库,它在启动时为内部库使用分配内存。如果您不链接它,则C和C++版本之间应该没有任何区别。使用GCC和Clang编译文件的命令如下:
g++ -Wl,--as-needed main.cpp
这将指示链接器不链接未使用的库。在您的示例代码中,没有使用C++库,因此不应链接到C++标准库。
您也可以使用C文件进行测试。使用以下命令进行编译:
gcc main.c -lstdc++
即使您已经建立了一个C程序,堆使用量也会重新出现。
显然,堆使用是取决于您正在使用的特定C++库实现。在您的情况下,这是GNU C++库,libstdc++。其他实现可能不会分配相同数量的内存,或者它们可能根本不分配任何内存(至少在启动时不分配)。例如,LLVM C++库(libc++)在我的Linux机器上不会在启动时进行堆分配:
clang++ -stdlib=libc++ main.cpp
与完全没有链接一样,堆使用相同。
(如果编译失败,则libc++可能未安装。包名称通常包含“libc ++”或“libcxx”。)

50
看到这个答案,我的第一个想法是:“如果这个标记有助于减少不必要的开销,为什么不默认打开呢?” 针对这个问题是否有一个好的答案呢? - Nat
4
我的猜测是在开发时编译速度会变慢。当你准备创建发布版本时,你会打开它。此外,在一个普通/大型代码库中,差异可能是微不足道的(如果你使用了大量的STD库等)。 - DarcyThomas
24
-Wl,--as-needed 标志会删除您在 -l 标志中指定但实际上没有使用的库。因此,如果不使用某个库,就不要链接它。您不需要使用此标志。但是,如果您的构建系统添加了太多库,并且清理所有不需要的库并只链接所需的库会很麻烦,则可以使用此标志。 但是,标准库是一个例外,因为它会自动链接。因此,这是一个特殊情况。 - Nikos C.
36
@Nat --as-needed可能会产生一些不必要的副作用,其工作原理是检查您是否使用库的任何符号,并将未通过测试的符号排除。但是:库还可以隐式地执行各种操作,例如,如果库中有静态C++实例,则其构造函数将自动调用。有些情况下,您可能需要一个未显式调用的库,尽管这种情况比较罕见。 - Norbert Lange
2
@NorbertLange 是的,有一些边角情况会导致 --as-needed 出现问题。不过这种情况似乎非常罕见。据我所知,大多数 Linux 发行版都启用了这个标志来处理那些随意链接所有内容并最终导致软件包依赖关系膨胀的软件包。当然,最好的解决方案是保持您的构建系统良好,以便您始终知道实际需要哪些库。 - Nikos C.
3
构建系统通常不能自动知道您的应用程序使用哪些符号,以及哪些库实现了这些符号(这在编译器、架构、发行版和C/C++库之间存在差异)。至少对于基本运行时库而言,正确处理这个问题相当麻烦。但是对于你需要使用库的罕见情况,你应该只为这一个库使用--no-as-needed,并在其他地方保留--as-needed。我看到的一个用例是用于跟踪/调试(lttng)的库以及执行某种身份验证/连接操作的库。 - Norbert Lange

16

GCC和Clang都不是编译器,它们实际上是工具链驱动程序。这意味着它们会调用编译器、汇编器和链接器。

如果你使用C或C++编译器编译代码,将会产生相同的汇编结果。汇编器会产生相同的对象文件。区别在于,工具链驱动程序将为两种不同的语言提供不同的输入给链接器:不同的启动文件(例如,C++需要执行命名空间级别的静态或线程本地存储期对象的构造函数和析构函数的代码,并需要堆栈帧的基础设施来支持异常处理期间的展开),C++标准库(其也有命名空间级别的静态存储期对象),以及可能的附加运行时库(例如,带有堆栈展开基础设施的libgcc)。

简而言之,导致占用空间增加的不是编译器,而是通过选择C++语言而选择使用的相关库。

虽然C++采用了“只为所用付费”的哲学,但使用该语言仍需要付出代价。你可以禁用部分语言特性(RTTI、异常处理),但这时你已经不再使用C++了。正如其他答案中提到的,如果你完全不使用标准库,可以让编译器去掉它(--Wl,--as-needed)。但是如果你既不想使用C++的任何功能和库,那么选择C++作为编程语言又有何意义呢?


启用异常处理即使您实际上不使用它也会产生成本是一个问题。这在C++功能中并不常见,这是C++标准工作组试图想出解决方法的事情。请参阅Herb Sutter在ACCU 2019年的主题演讲De-fragmenting C++:使异常更经济实用。尽管如此,在当前的C++中,这是一个不幸的事实。即使/当使用关键字添加新异常的新机制时,传统的C++异常也可能始终存在这种成本。 - Peter Cordes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接