【问题标题】:how to get the smallest ocamlopt compiled native binary?如何获得最小的 ocamlopt 编译的本机二进制文件?
【发布时间】:2020-01-21 12:56:50
【问题描述】:

我很惊讶地发现,即使是这样的简单程序:

print_string "Hello world !\n";

当通过ocamlopt 使用一些非常激进的选项(使用musl)静态编译为本机代码时,在我的系统上仍然约为 190KB。

$ ocamlopt.opt -compact -verbose -o helloworld \
    -ccopt -static \
    -ccopt -s \
    -ccopt -ffunction-sections \
    -ccopt -fdata-sections \
    -ccopt -Wl \
    -ccopt -gc-sections \
    -ccopt -fno-stack-protector \
    helloworld.ml && { ./helloworld ; du -h helloworld; }
+ as -o 'helloworld.o' '/tmp/camlasm759655.s'
+ as -o '/tmp/camlstartupfc4271.o' '/tmp/camlstartup5a7610.s'
+ musl-gcc -Os -o 'helloworld'   '-L/home/vaab/.opam/4.02.3+musl+static/lib/ocaml' -static -s -ffunction-sections -fdata-sections -Wl -gc-sections -fno-stack-protector '/tmp/camlstartupfc4271.o' '/home/vaab/.opam/4.02.3+musl+static/lib/ocaml/std_exit.o' 'helloworld.o' '/home/vaab/.opam/4.02.3+musl+static/lib/ocaml/stdlib.a' '/home/vaab/.opam/4.02.3+musl+static/lib/ocaml/libasmrun.a' -static  -lm 
Hello world !
196K    helloworld

如何从 ocamlopt 获取最小的二进制文件?

190KB 的大小对于像今天的限制(iot、android、alpine VM...)这样的简单程序来说太过分了,并且与简单的 C 程序(大约 6KB,或直接编码 ASM)相比很差并调整一些东西以获得可能在 150B 左右的工作二进制文件)。我天真地认为我可以简单地放弃C 来编写简单的静态程序来做一些琐碎的事情,编译后我会得到一些简单的汇编代码,这些代码与等效的 C 程序相比不会那么大。那可能吗 ?

我认为我理解的内容:

当删除 gcc 的 -s 以获取有关二进制文件中剩余内容的一些提示时,我可以注意到很多 ocaml 符号,并且我还读到了 ocamlrun are meant to be interpreted even in this form 的一些环境变量。就好像ocamlopt 所说的“本地编译”是将程序的ocamlrun 和非本地bytecode 打包到一个文件中并使其可执行。不完全是我所期望的。我显然错过了一些重要的点。但如果是这样的话,我会很感兴趣为什么它不像我预期的那样。

其他编译为本机代码的语言也有同样的问题:给一些天真的用户(比如我自己)留下大致相同的问题:

我也用 Haskell 进行了测试,并且在没有调整的情况下,所有语言的编译器都会为“hello world”程序生成 700KB 以上的二进制文件(调整之前的 Ocaml 也是如此)。

【问题讨论】:

标签: compilation size ocaml executable minimum


【解决方案1】:

您的问题非常广泛,我不确定它是否符合 Stackoverflow 的格式。它值得一个彻底的discussion

190KB 的大小对于像今天这样的简单程序(iot、android、alpine VM...)来说太大了,并且与简单的 C 程序(大约 6KB,或直接编码 ASM 并进行调整)相比也很糟糕获得可能在 150B 左右的工作二进制文件的事情)

首先,这不是一个公平的比较。如今,已编译的 C 二进制文件远非独立二进制文件。它应该更像是框架中的插件。因此,如果您想计算给定二进制文件实际使用了多少字节,我们将计算加载程序、shell、libc 库以及整个 linux 或 windows 内核的大小——它们总共构成了应用程序的运行时。

OCaml 与 Java 或 Common Lisp 不同,它对通用 C 运行时非常友好,并尝试重用其大部分功能。但是 OCaml 仍然带有自己的运行时,其中最大(也是最重要的部分)是垃圾收集器。运行时间不是很大(大约 30 KLOC),但仍然会增加重量。而且由于 OCaml 使用静态链接,每个 OCaml 程序都会有一份它的副本。

因此,C 二进制文件具有显着优势,因为它们通常在 C 运行时已经可用的系统中运行(因此它通常被排除在等式之外)。但是,有些系统根本没有 C 运行时,只有 OCaml 运行时,例如,参见 Mirage。在这样的系统中,OCaml 二进制文件更为有利。另一个例子是OCaPic 项目,其中(在调整编译器和运行时)他们设法将 OCaml 运行时和程序放入 64Kb 闪存中(阅读paper,它对二进制大小非常有洞察力)。

如何从 ocamlopt 获取最小的二进制文件?

当确实需要最小化大小时,使用 Mirage Unikernels 或实现您自己的运行时。对于一般情况,请使用stripupx。 (例如,使用 upx --best 我能够将示例的二进制大小减少到 50K,而无需任何技巧)。如果性能不那么重要,那么您可以使用字节码,它通常比机器码小。因此,您只需支付一次(运行时大约 200k),每个程序只需几个字节(例如,您的 helloworld 需要 200 个字节)。

另外,不要创建很多小的二进制文件,而是创建一个二进制文件。在您的特定示例中,helloworld 编译单元的大小在字节码中为 200 字节,在机器码中为 700 字节。剩下的 50k 是启动线束,应该只包含一次。此外,由于 OCaml 在运行时支持动态链接,您可以轻松创建一个加载器,在需要时加载模块。在这种情况下,二进制文件会变得非常小(数百字节)。

就好像 ocamlopt 所说的“本机编译”是将 ocamlrun 和程序的非本机字节码打包到一个文件中并使其可执行。不完全是我所期望的。我显然错过了一些重要的点。但如果是这样的话,我会很感兴趣为什么它不像我预期的那样。

不,不,这是完全错误的。本机编译是指将程序编译为机器代码,无论是 x86、ARM 还是其他。运行时是用 C 语言编写的,编译为机器代码,并且也是链接的。 OCaml标准库大部分是用OCaml写的,也编译成机器码,也链接成二进制(只有那些用到的模块,OCaml静态链接效率很高,前提是程序拆分成模块(编译单元)相当好)。

关于OCAMLRUNPARAM环境变量,它只是一个参数化运行时行为的环境变量,主要是垃圾收集器的参数。

【讨论】:

  • 这可能是我的英语理解能力有限,但是“虽然与 [其他语言] 不同,OCaml 对 C 运行时非常友好”对我来说很难解析。我不认为你的意思是“不像 [其他语言],OCaml ......”,因为否则你会这样写,我猜;但我可以想象的另一个含义是“即使 OCaml 与 [其他语言] 不同,它仍然对常见的 C 运行时非常友好”,我也不确定这就是你所说的。你能澄清一下这部分吗?
  • 谢谢,这确实是一个非常糟糕的措辞:) 我只是把句子分成几个更小的句子,希望现在更清楚了。
  • 谢谢,这样更好。我并不反对,我只想提一下,对于 Java/CL,仅从语言规范判断在实践中是不够的,因为有多种实现(例如 ECL 尽可能接近 C,使用 libecl.so和静态链接)。顺便说一句,很好的答案。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2020-12-26
  • 2013-06-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-10-03
  • 2014-06-08
相关资源
最近更新 更多