how to get the smallest ocamlopt compiled native binary?
Asked Answered
C

1

5

I was quite surprised to see that even a simple program like:

print_string "Hello world !\n";

when statically compiled to native code through ocamlopt with some quite aggressive options (using musl), would still be around ~190KB on my system.

$ 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

How to get the smallest binary from ocamlopt ?

A size of 190KB is way too much for a simple program like that in today's constraints (iot, android, alpine VM...), and compares badly with simple C program (around ~6KB, or directly coding ASM and tweaking things to get a working binary that could be around 150B). I naïvely thought that I could simply ditch C to write simple static program that would do trivial things and after compilation I would get some simple assembly code that wouldn't be so far in size with the equivalent C program. Is that possible ?

What I think I understand:

When removing gcc's -s to have some hints about what is left in the binary, I can notice a lot of ocaml symbols, and I also kinda read that some environment variable of ocamlrun are meant to be interpreted even in this form. It is as if what ocamlopt calls "native compilation" is about packing ocamlrun and the not-native bytecode of your program in one file and make it executable. Not exactly what I would have expected. I obviously missed some important point. But if that is the case, I'll be interested why it isn't as I expected.

Other languages compiling to native code having the same issue: leaving some naïve user (as myself) with roughly the same questions:

I've tested also with Haskell, and without tweaks, all languages compilers are making binaries above 700KB for the "hello world" program (it was the same for Ocaml before the tweaks).

Coroner answered 20/9, 2019 at 12:55 Comment(2)
Though it doesn't answer the question, but I believe that the following work might be interesting to potential viewers of this posting (note, no affiliation with the work) algo-prog.info/ocapic/web/index.php?id=ocapicHarbird
Note for current readers: 4 years later, running ocamlc version 4.14.1 with no flags on a file with just print_string "hello world!\n", I get a 21K binary as the outputKellam
H
11

Your question is very broad and I'm not sure that it fits the format of Stackoverflow. It deserves a thorough discussion.

A size of 190KB is way too much for a simple program like that in today's constraints (iot, android, alpine VM...), and compares badly with simple C program (around ~6KB, or directly coding ASM and tweaking things to get a working binary that could be around 150B)

First of all, it is not a fair comparison. Nowadays, a compiled C binary is an artifact that is far from being a standalone binary. It should be seen more like a plugin in a framework. Therefore, if you would like to count how many bytes a given binary actually uses, we shall count the size of the loader, shell, the libc library, and the whole linux or windows kernel - which in total form the runtime of an application.

OCaml, unlike Java or Common Lisp, is very friendly to the common C runtime and tries to reuse most of its facilities. But OCaml still comes with its own runtime, in which the biggest (and most important part) is the garbage collector. The runtime is not extremely big (about 30 KLOC) but still contributes to the weight. And since OCaml uses static linking every OCaml program will have a copy of it.

Therefore, C binaries have a significant advantage as they are usually run in systems where the C runtime is already available (therefore it is usually excluded from the equation). There are, however, systems where there is no C runtime at all, and only OCaml runtime is present, see Mirage for example. In such systems, OCaml binaries are much more favorable. Another example is the OCaPic project, in which (after tweaking the compiler and runtime) they managed to fit OCaml runtime and programs into 64Kb Flash (read the paper it is very insightful about the binary sizes).

How to get the smallest binary from ocamlopt?

When it is really necessary to minimize the size, use Mirage Unikernels or implement your own runtime. For general cases, use strip and upx. (For example, with upx --best I was able to reduce the binary size of your example to 50K, without any more tricks). If performance doesn't matter that much, then you can use bytecode, which is usually smaller than the machine code. Thus you will pay once (about 200k for the runtime), and few bytes for each program (e.g., 200 bytes for your helloworld).

Also, do not create many small binaries, but create one binary. In your particular example, the size of the helloworld compilation unit is 200 bytes in bytecode and 700 bytes in machine code. The rest 50k is the startup harness which should be included only once. Moreover, since OCaml supports dynamic linking in runtime, you can easily create a loader that will load modules when needed. And in this scenario, the binaries will become very small (hundreds of bytes).

It is as if what ocamlopt calls "native compilation" is about packing ocamlrun and the not-native bytecode of your program in one file and make it executable. Not exactly what I would have expected. I obviously missed some important point. But if that is the case, I'll be interested why it isn't as I expected.

No-no, it is completely wrong. Native compilation is when a program is compiled to the machine code, whether it is x86, ARM, or whatever. The runtime is written in C, compiled to machine code, and is also linked. The OCaml Standard Library is written mostly in OCaml, also compiled to machine code, and is also linked into the binary (only those modules that are used, OCaml static linking is very efficient, provided that the program is split into modules (compilation units) fairly well).

Concerning the OCAMLRUNPARAM environment variable, it is just an environment variable that parameterizes the behavior of the runtime, mostly the parameters of the garbage collector.

Harbird answered 20/9, 2019 at 15:18 Comment(4)
That's probably my english comprehension showing its limit, but "while unlike [other lang], OCaml is very friendly to C runtime" is hard to parse for me. I don't think you mean "unlike [other lang], OCaml...", because otherwise you would have written so, I guess; but the other meaning I can imagine is "even though OCaml is unlike [other lang], it still is very friendly to the common C runtime", which I am not sure this is what you are saying either. Could you clarify this part?Chlorite
Thanks, this was indeed a very bad wording :) I just split the sentence into several smaller sentences, hope it is now much more clear.Harbird
thanks, this is better. I am not disagreeing, I'd just like to mention that for Java/CL, judging by the language spec alone is not enough in practice, as there are multiple implementations (e.g. ECL is as close to C as possible, with libecl.so and static linking). Nice answer btw.Chlorite
A github project showing how you got to your answers would be very helpful.Atmo

© 2022 - 2024 — McMap. All rights reserved.