【问题标题】:Why is C++ initial allocation so much larger than C's?为什么 C++ 初始分配比 C 大得多?
【发布时间】:2019-11-03 15:16:45
【问题描述】:

使用相同的代码时,只需更改编译器(从 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 以显示峰值。

我在这里做错了吗?

【问题讨论】:

  • 首先,您如何构建?你使用什么选项?你如何衡量?你是如何运行 Valgrind 的?
  • 如果我没记错的话,现代 C++ 编译器必须有一个异常模型,其中进入 try 块不会影响性能,但会以更大的内存占用为代价,可能使用跳转表或其他东西.也许尝试无例外地编译,看看有什么影响。编辑:事实上,反复尝试禁用各种 c++ 功能,看看对内存占用有什么影响。
  • 使用 clang++ -xc 而不是 clang 编译时,存在相同的分配,这强烈表明它是由于链接库所致
  • @bigwillydos 这确实是 C++,我看不出它破坏了 C++ 规范的任何部分......除了可能包括 stdio.h 而不是 cstdio 但至少在旧 C++ 版本中是允许的.您认为这个程序中的“畸形”是什么?
  • 我觉得可疑的是,那些 gcc 和 clang 编译器在 C 模式下生成完全相同的字节数和在C++ 模式下生成完全相同的字节数。您是否犯了转录错误?

标签: c++ c benchmarking


【解决方案1】:

堆使用来自 C++ 标准库。它在启动时分配内存供内部库使用。如果您不链接它,则 C 和 C++ 版本之间的差异应该为零。使用 GCC 和 Clang,您可以使用以下命令编译文件:

g++ -Wl,--根据需要 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”。)

【讨论】:

  • 看到这个答案,我的第一个想法是,“如果这个标志有助于减少不必要的开销,为什么默认不启用?”。有什么好的答案吗?
  • @Nat 我的猜测是在开发时编译速度较慢。当您准备好创建发布版本时,您将打开它。同样在普通/大型代码库中,差异可能很小(如果您使用大量 STD 库等)
  • @Nat -Wl,--as-needed 标志会删除您在 -l 标志中指定但您实际上并未使用的库。因此,如果您不使用库,则不要链接它。你不需要这个标志。但是,如果您的构建系统添加了太多库,并且清理它们并仅链接所需的库将需要大量工作,那么您可以改用此标志。标准库是一个例外,因为它是自动链接的。所以这是一个极端情况。
  • @Nat --as-needed 可能会产生不必要的副作用,它通过检查您是否使用库的任何符号并将那些未通过测试的符号踢出而起作用。但是:库也可以隐式地做各种事情,例如,如果库中有一个静态 C++ 实例,那么它的构造函数将被自动调用。在极少数情况下,您没有显式调用的库是必要的,但它们确实存在。
  • @NikosC。 Buildsystems 不会自动知道您的应用程序使用了哪些符号,以及哪些库实现了它们(编译器、archs、发行版和 c/c++ 库之间有所不同)。至少对于基本的运行时库来说,做到这一点相当麻烦。但是对于您需要一个库的极少数情况,您应该简单地使用 --no-as-needed 用于那个库,并将 --as-needed 留在其他任何地方。我看到的一个用例是用于跟踪/调试 (lttng) 的库和执行某种身份验证/连接的库。
【解决方案2】:

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

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

简而言之,导致占用空间增加的不是编译器,而是您通过选择 C++ 语言选择使用的内容的链接。

C++ 确实有“只为你使用的东西付费”的理念,但是通过使用该语言,你需要为它付费。您可以禁用部分语言(RTTI、异常处理),但您不再使用 C++。如另一个答案中所述,如果您根本不使用标准库,则可以指示驱动程序将其保留(--Wl,--根据需要),但如果您不打算使用任何功能对于 C++ 或其库,您为什么还要选择 C++ 作为编程语言?

【讨论】:

  • 启用异常处理是有代价的,即使您实际上不使用它也是一个问题。这对于一般的 C++ 特性来说是不正常的,这是 C++ 标准工作组正试图想办法解决的问题。请参阅 Herb Sutter 在 ACCU 2019 De-fragmenting C++: Making exceptions more affordable and usable 的主题演讲。然而,在当前的 C++ 中,这是一个不幸的事实。传统的 C++ 异常可能总是会产生这种成本,即使/当新异常的新机制添加了关键字时也是如此。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-03-15
  • 2010-10-10
  • 2018-07-22
相关资源
最近更新 更多