【问题标题】:Attempting to reduce executable size of Go program [duplicate]尝试减小 Go 程序的可执行文件大小 [重复]
【发布时间】:2019-07-20 18:39:24
【问题描述】:

编辑/澄清:

我似乎没有在这里解释自己。我不是在批评 Go,或者它的运行时,或者可执行文件很大的事实。我也不是想说 Go 不好,而 C 好。

我只是指出编译后的可执行文件似乎总是至少在 1MB 左右(大概这是运行时开销),并且无论使用情况如何,导入一个包似乎都会将整个包放入其中。

我的实际问题基本上是这 2 点是默认行为还是唯一行为?我给出了一些 C 程序示例,它们在代码方面与 Go 程序等效,但是我为它们仔细挑选了编译器和链接器标志以避免与任何外部 C 运行时链接(我用Dependency Walker 验证了这一点,只是为了确定)。 C 示例的目的是展示实际代码有多小,同时也展示了您确实需要某些东西并且只导入您需要的东西的情况。

我实际上认为这种行为(把所有东西都放在里面以防万一)是一个很好的默认设置,但我认为可能有一些编译器或链接器标志来改变它。现在,我认为说您不想要运行时或其中的一部分是不明智的。但是,我认为选择性地包含一个包的某些部分并不是一件奇怪的事情。让我解释一下:

假设我们使用 C/C++ 编写代码,并且包含一个包含大量函数的巨大头文件,但我们只使用了其中的一小部分。在这种情况下,可能会得到一个不包含该头文件中任何未使用代码的可执行文件。 (现实世界的例子:一个支持 2D/3D/4D 向量和矩阵、四元数等的数学库。所有这些都有 2 个版本,一个用于 32 位浮点数,一个用于 64 位浮点数 + 从一个到另一个的转换)

这就是我一直在寻找的东西。我完全理解这样做在某些情况下可能会导致问题,但仍然如此。并不是说 Go 没有其他可能导致严重问题的东西。他们有“不安全”的包,如果你需要它就在那里,但它就像“使用风险自负”的包。


原始问题:

在使用 Go (golang) 一段时间后,我决定研究它生成的可执行文件。我看到我的项目仅针对可执行文件就超过 4.5MB,而另一个在复杂性和范围上相似但用 C/C++ 编写(使用 MSVC 编译)的项目不到 100KB。

所以我决定尝试一些事情。我用 C 和 Go 编写了非常愚蠢和死的简单程序来比较输出。 对于 C,我使用 MSVC,在发布模式下编译 64 位可执行文件,而不与 C 运行时链接(据我了解,在我看来,Go 可执行文件仅在使用 CGO 时才链接)

第一次运行:一个简单的无限循环,就是这样。没有打印,没有与操作系统的交互,什么都没有。

C:

#include "windows.h"

int main() 
{
    while (true);
}

void mainCRTStartup()
{
    main();
}

去:

package main

func main() {
    for {
    }
}

结果:

C : 3KB 可执行文件。不依赖任何东西

GO:1,057 KB 可执行文件。依赖于 KERNEL32.DLL 中的 29 个过程

那里有很大的不同,但我认为这可能不公平。所以接下来的测试我决定去掉循环,只写一个程序,立即返回,退出代码为 13:

C:

    #include "windows.h"

    int main() 
    {
        return 13;
    }

    void mainCRTStartup()
    {
        ExitProcess(main());
    }

去:

package main

import "os"

func main() {
    os.Exit(13)
}



结果:

C:4KB 可执行文件。取决于 KERNEL32.DLL 中的 1 个过程

GO:1,281 KB 可执行文件。依赖于 KERNEL32.DLL 中的 31 个过程

Go 可执行文件似乎“臃肿”。据我了解,与 C 不同,Go 将相当多的运行时代码放入可执行文件中,这是可以理解的,但不足以解释大小。

此外,Go 似乎在包粒度中工作。我的意思是它不会塞进你不使用的可执行包中,但是如果你导入一个包,你会得到它的全部,即使你只需要一个小子集,即使你根本不使用它.例如,只导入“fmt”而不调用任何内容,会将之前的可执行文件从 1,281KB 扩展到 1,777 KB。

我是否遗漏了一些类似 Go 编译器的标志来告诉它不那么臃肿(我知道有很多标志可以设置,也可以给本机编译器和链接器提供标志,但我还没有找到这个具体来说)或者它只是在 2019 年不再有人关心的事情,因为几兆字节真的是什么?

【问题讨论】:

  • "Go 将相当多的运行时代码放入可执行文件中,这可以理解,但不足以解释大小。"你怎么看?您确定运行时“应该”采用什么大小?
  • 虽然一般来说 - 是的,按照现代标准,1MB 很小,与 .NET Core 应用程序的独立构建(例如 50MB+)相比是有利的。
  • 这也不是真正的“在 Windows 上”。 Go 特定的所有内容都静态链接在每个平台上。
  • 简短的回答是 Go 总是包含 整个 运行时,包括像 GC 和堆栈跟踪这样的东西。它没有专门的优化模式,旨在使"Hello, World" 更小。
  • @Adrian 抱歉,如果我不清楚。我并不是在批评运行时的大小。我的意思是,除了运行时大小本身之外,与 C 等效项相比,Go 可执行文件似乎仍然很大。

标签: go binaryfiles


【解决方案1】:

以下是 Go 程序包含而 C 程序不包含的一些内容:

  • 容器类型,例如哈希映射和数组,以及它们的关联函数

  • 内存分配器,针对多线程程序进行了优化

  • 并发垃圾收集器

  • 线程的类型和函数,例如互斥体、条件变量、通道和线程

  • 堆栈跟踪转储和 SIGQUIT 处理程序等调试工具

  • 反射码

(如果您想知道具体包含什么,可以使用调试工具查看二进制文件中的符号。在 macOS 和 Linux 上,您可以使用 nm 转储程序中的符号。)

问题是——大多数 Go 程序都使用了所有这些功能!很难想象一个不使用垃圾收集器的 Go 程序。所以 Go 的创造者并没有创造出一种特殊的方法来从程序中删除这些代码——因为没有人需要这个特性。毕竟,你真的关心"Hello, world!" 有多大吗? 不,你没有。

来自常见问题解答Why is my trivial program such a large binary?

gc 工具链中的链接器默认创建静态链接的二进制文件。因此,所有 Go 二进制文件都包含 Go 运行时,以及支持动态类型检查、反射甚至恐慌时间堆栈跟踪所需的运行时类型信息。

另外请记住,如果您在 Windows 上使用 MSVC 进行编译,您可能会使用 DLL 运行时,例如 MSVCR120.DLL... 大约 1 MB。

【讨论】:

  • 我知道这是默认行为,并且我知道在大多数情况下这可能是一个很好的默认值。默认情况下,MSVC 还会将大量未使用的代码放入可执行文件中,并链接很多我不想要的东西,但我可以选择告诉它不要这样做。例如,我特别确保在我的 C 示例中不链接任何 C 运行时,因此没有 MSVCRT.DLL 或任何其他 C 运行时。但似乎 GO 没有这样的选项。
  • @Bob:你能给我一个你想用 Go 编写但没有运行时的程序的例子吗?我很难理解你想要完成什么。
  • @Bob:请记住,当您将 C 程序计算为 4KB 时,您所做的是将其拆分为单独的 DLL 和 EXE,然后只计算 EXE。
  • 我没有将 C 程序分成 EXE 和 DLL。我的 C 程序只依赖于 KERNEL32.DLL 中的一个 DLL(GO 程序也依赖于它)。这是因为我从中剥离了所有 C 运行时 (/nodefaultlibs)。我并不是说我要删除整个 go 运行时,这很可笑,如果我根本不需要运行时,我只会用 C 编写。但我正在寻找删除不需要的东西的方法。例如我没有使用反射,我可能只使用了数学包和文件系统包的一小部分。
  • 我在对问题本身的评论中给出了一个与运行时无关的“膨胀”的具体示例。如果我使用外部 DLL(在本例中是 SDL2),在 C 中我使用它的可执行文件很小,但在 GO 中,因为绑定实际上会为每个函数生成代码,即使我不使用它,可执行文件也会增长超过2MB,即使 DLL 是 1.5MB。也许这只是吹毛求疵,因为按照 2019 年的标准,几 MB 不算什么,但实际上加载大量永远不会使用的代码似乎是一种浪费
猜你喜欢
  • 2010-09-17
  • 2010-10-01
  • 2022-01-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多