如何获得最小的ocamlopt编译本地二进制文件?

5

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

print_string "Hello world !\n";

当使用一些相当激进的选项(使用 musl)通过 ocamlopt 静态编译为本机代码时,在我的系统上仍然约为 ~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获得最小的二进制文件?

像今天的限制(iot,android,alpine VM等)这样简单的程序的大小为190KB太大了,并且与简单的C程序相比(约为~6KB),或者直接编写ASM并调整一些东西以获得可工作的二进制文件,可以约为150B。我天真地认为,我可以简单地放弃C来编写简单的静态程序,执行微不足道的操作,并在编译后获得一些简单的汇编代码,其大小不会比等效的C程序差太远。这是可能的吗?

我认为我理解的内容:

当删除gcc的-s以获得有关二进制文件中剩余内容的一些提示时,我可以注意到许多ocaml符号,并且我也有点读到ocamlrun的某些环境变量即使在这种形式下也应该被解释。就像ocamlopt所说的“本地编译”是将ocamlrun和程序的非本地bytecode打包在一个文件中并使其可执行。这不完全是我预期的。显然,我错过了一些重要的要点。但如果是这种情况,我会很感兴趣为什么它不是我预期的。

其他编译成本地代码的语言也有相同的问题:留下一些天真的用户(如我自己),他们大致上有着相同的问题:

我也尝试了Haskell,但是没有任何修改,所有语言的编译器都会为“hello world”程序生成超过700KB的二进制文件(在进行调整之前,Ocaml也是如此)。


1
虽然它没有回答问题,但我相信以下工作可能对此帖子的潜在观众很有趣(请注意,与这项工作无关): http://www.algo-prog.info/ocapic/web/index.php?id=ocapic - ivg
1
注意:对于当前的读者来说,经过4年的时间,我使用的是ocamlc版本4.14.1,在一个只有print_string "hello world!\n"的文件上,没有使用任何标志,我得到的输出是一个21K的二进制文件。 - undefined
1个回答

9
你的问题非常广泛,我不确定它是否符合Stackoverflow的格式。它值得进行讨论

像这样的简单程序,190KB的大小在当今的限制(物联网、安卓、阿尔卑斯山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 Flash中(阅读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环境变量,它只是一个环境变量,参数化运行时的行为,主要是垃圾收集器的参数。

1
这可能是我英语理解能力的极限,但“while unlike [other lang], OCaml is very friendly to C runtime”很难解析。我不认为你的意思是“与[其他语言]不同,OCaml...”,因为否则你应该会这样写;但我能想象的另一种意思是“尽管OCaml与[其他语言]不同,它仍然非常友好地支持常见的C运行时”,我也不确定这是否是你的意思。你能澄清一下吗? - coredump
2
谢谢,确实这个措辞很糟糕 :) 我将句子拆分成了几个更小的句子,希望现在更加清晰明了。 - ivg
3
谢谢,这个更好了。我并不是在反驳,只是想提一下,在实践中,仅仅根据Java/CL的语言规范来判断是不够的,因为有多种实现方式(例如ECL尽可能接近C,具有libecl.so和静态链接)。顺便说一句,回答得很好。 - coredump
一个展示你如何得出答案的 GitHub 项目会非常有帮助。 - silvalli

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