如何减小可执行文件的大小?

3
当我使用{fmt}库编译这段代码时,可执行文件的大小为255 KiB,而只使用iostream头文件时,大小为65 KiB(使用GCC v11.2)。 time_measure.cpp
#include <iostream>
#include "core.h"
#include <string_view>

int main( )
{
    // std::cout << std::string_view( "Oh hi!" );
    fmt::print( "{}", std::string_view( "Oh hi!" ) );

    return 0;
}

这是我的构建命令:

g++ -std=c++20 -Wall -O3 -DNDEBUG time_measure.cpp -I include format.cc -o runtime_measure.exe

{fmt} 库不是应该比 iostream 轻量级吗?还是我做错了什么?

编辑:通过在命令中添加 -s 以从可执行文件中删除所有符号表和重定位信息,使其大小为 156 KiB。但仍然比 iostream 版本多大约2.5倍。


我认为这样做不会有太大的作用,只需将“core.h”替换为<format>,并且只包含您使用的内容。 - Pepijn Kramer
@Pepijn Kramer 你是指在C++20中添加的头文件吗?但我想用fmt::print替换cout,这可能会更快一些。 - digito_evo
这是关于最小化你所包含的代码的问题,"core.h" 可能会包含比你实际需要的更多内容。通过包含 <format>,你只会包含实际需要编译代码所需的头文件。在这种情况下,如果你只包含 <format>,你的代码应该可以编译(如果我没记错的话,<format> 会为你暴露 string_view 作为其 API 的一部分)。 - Pepijn Kramer
一个轻量级的库并不一定意味着“更小的可执行文件大小”。它也可以指运行时减少内存使用或 CPU 循环(以获得类似的可观察效果) - 这些可能会增加可执行文件的大小 - 或者开发人员想到的任何其他东西。无论如何,关于一个库是否导致更大/更小的可执行文件的具体细节取决于许多因素。 - Peter
@Peter 正如您所看到的,与iostreams相比,{fmt}在生成的二进制代码大小方面减少了60%,并且非常接近printf。 这是在https://github.com/fmtlib/fmt/blob/master/README.rst中写的。 - digito_evo
3个回答

6
与其他库一样,{fmt}库有固定成本和每次调用的成本。{fmt}库的固定成本确实为约100-150k(不包括调试信息)(这取决于编译器标志)。在您的示例中,您正在比较链接到库的固定成本以及iostreams之所以似乎较小的原因是它包含在标准库本身中,该标准库是动态链接的,并且不计入可执行文件的二进制大小。
请注意,这种大小的很大一部分来自浮点格式化功能,iostreams中甚至不存在(最短回路表示)。
如果您想比较每次调用的二进制大小,这对于具有大量格式化函数调用的实际代码更为重要,则可以查看对象文件或生成的汇编代码。例如:
#include <fmt/core.h>

int main() {
  fmt::print("Oh hi!");
}

生成(https://godbolt.org/z/qWTKEMqoG
.LC0:
        .string "Oh hi!"
main:
        sub     rsp, 24
        pxor    xmm0, xmm0
        xor     edx, edx
        mov     edi, OFFSET FLAT:.LC0
        mov     rcx, rsp
        mov     esi, 6
        movaps  XMMWORD PTR [rsp], xmm0
        call    fmt::v8::vprint(fmt::v8::basic_string_view<char>, fmt::v8::basic_format_args<fmt::v8::basic_format_context<fmt::v8::appender, char> >)
        xor     eax, eax
        add     rsp, 24
        ret

#include <iostream>

int main() {
  std::cout << "Oh hi!";
}

生成 (https://godbolt.org/z/frarWvzhP)。
.LC0:
        .string "Oh hi!"
main:
        sub     rsp, 8
        mov     edx, 6
        mov     esi, OFFSET FLAT:.LC0
        mov     edi, OFFSET FLAT:_ZSt4cout
        call    std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long)
        xor     eax, eax
        add     rsp, 8
        ret
_GLOBAL__sub_I_main:
        sub     rsp, 8
        mov     edi, OFFSET FLAT:_ZStL8__ioinit
        call    std::ios_base::Init::Init() [complete object constructor]
        mov     edx, OFFSET FLAT:__dso_handle
        mov     esi, OFFSET FLAT:_ZStL8__ioinit
        mov     edi, OFFSET FLAT:_ZNSt8ios_base4InitD1Ev
        add     rsp, 8
        jmp     __cxa_atexit

除了对cout进行静态初始化之外,因为这里几乎没有格式化,所以两种情况下都只有一个函数调用。一旦添加格式化,您将很快看到{fmt}的好处,例如,请参见http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p0645r10.html#BinaryCode

如果我将 {fmt} 构建为共享库,然后再使用它,会减小二进制文件的大小吗?我能在另一台计算机上运行程序吗?还是需要访问共享库? - digito_evo
1
@digito_evo,这将需要访问共享库libfmt.so。就像iostreams变体需要访问兼容版本的libstdc++.so一样。区别可能在于您无论如何都将拥有libstdc++.so。 - Marcus Müller
请注意,如果您想将工具发送到其他计算机,则需要发送它使用的所有库(包括 x86_64 2.2 MB libstdc++.so 和 libfmt.so 贡献了一些相当微小的 150 kB),或者您需要进行静态链接,在这种情况下,我不确定使用 iostreams 在大小方面是否更糟糕。 - Marcus Müller
@Marcus Müller 静态链接到底是什么?我该怎么做? - digito_evo
1
@digito_evo,你提出了一个非常重要的问题。我相信你可以在互联网上找到大量关于这方面的文档,但如果你阅读了一些文档,却无法完全理解它们,那就别犹豫,开启一个新问题吧! - Marcus Müller

3

您忘记了iostreams已经包含在stdlibc++.so中,因为它是一个共享库(通常不计入二进制文件大小)。我相信fmt默认情况下构建为静态库文件,因此会增加二进制文件的大小。您需要按照构建说明,将fmt编译为共享库文件,使用-DBUILD_SHARED_LIBS=TRUE


这样会减小二进制文件的大小吗? - digito_evo
你是在构建 fmt 库还是从发行版中包含它? - user8143588
我刚刚从它的文件夹中包含了 core.h。我已经从他们的网站下载了压缩文件。 - digito_evo
我希望二进制文件的大小尽可能小。这意味着我需要使用CMake构建fmt库,然后在我的代码中使用它? - digito_evo
我正在编写一个小型沙盒应用程序。我希望它非常小且优化(90KiB到110 KiB),不要太大。 - digito_evo
显示剩余3条评论

0
在您的构建/链接命令中,为什么不使用-Os选项(优化大小)?

那不会有太大的影响。 - digito_evo

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