Julia Community 🟣

Thomas R
Thomas R

Posted on

StaticCompiler - Generating small binaries

Static compilation with help of StaticCompiler

Here I'd like to share some experiences of using StaticCompiler to generate binaries out from Julia code.

Static compilation imposes some limitations to the julia code: GC allocations are not allowed. Additionally, the code has to be type-stable. Global variables are only allowed if they are constants. Additionally these constants should be non-composite; e.g. arrays as constants are not possible. Because allocations are also forbidden for return values of functions, these values must be of native types or unmodified structs, unless the respective functions or the function calls are inlined.

StaticTools provides allocation-free datatypes

StaticTools is a package that procides statically compilable datatypes for StaticCompiler. For example, strings are implemented as MallocString or StaticString, and can be coded using the string macros c"" and m"", respectively. Both types ensure 0-terminated strings, just like C strings. A StaticString object has a fixed length. For a representing a MallocString object, memory is being allocated. After declaring a MallocString, the user itself is responsible for freeing the allocated memory.

function f()
a = c"anne"
b = m"mike"
free(b)
0
end

Arrays are represented as MallocVector, MallocArray, MallocMatrix, StackVector or StackArray. The functions mfill and sfill can be used to initialize a Malloc- or StackVector, respectively. You can also use the constructors themselves, and have an undefined initialization.

a = mfill(m"", 2)
a[1] = m"anne"
a[2] = m"mike"

b = MallocVector{MallocString}(undef, 2)
b[1] = m"paul"
b[2] = m"john"

c = mfill(m"", 2, 3)
c[1, 1] = m"a"
c[1, 2] = m"b"

d = MallocMatrix{MallocString}(undef, 2, 3)
d[1, 1] = m"a"
d[1, 2] = m"b"

free(a)
free(b)
free(c)
free(d)

e = sfill(0,2)
e[1] = 1
e[2] = 2

f = StackVector{MallocString}(undef, 2)
f[1] = m"a"
f[2] = m"b"

g = StackMatrix{MallocString}(undef, 2, 3)
g[1, 1] = m"a"
g[1, 2] = m"b"

A composite type of a StaticString, e.g. StackVector{StaticString}(undef, 2) does not to seem possible.

Tuples, NamedTuples and Structs can be used almost in the usual way.

function f()
t = (2, 3.3, m"a")
a, b, c = t
printf(c"a: %d\n", a)
printf(c"b: %g\n", b)
printf(c"c: %s\n", c)
n = (x = 1, y = 1)
printf(c"n.x: %d\n", n.x)
printf(c"n.y: %d\n", n.y)
d = NTuple{3, MallocString}((m"a",m"b",m"c"))
for i in 1:3
printf(c"%s\n", d[i])
end
0
end

struct A
s :: MallocString
n :: Int64
end

function h()
a = A(m"a", 1)
0
end

When using mutable structs and calling a function that assignes a MallocString, you have to inline the called function or the function call.

mutable struct A
s :: MallocString
n :: Int64
end

function f()
a = A(m"a", 1)
a.s = m"b"
a.n = 2
g(a)
end

@inline function g(a::A)
a.s = m"c"
a.n = 3
b = m"a"
end

Dictionaries and Unions need heap allocation. Enabling static compilation for them is an open, promising project.

Input and Output

Printing to stdout can be done with help of print, println and printf, in C matter. Flushing (and a newline) is supplied by putchar().

a = m"a"
print(c"hello")
println(c"hello")
printf(c"a: %s\n", a)
putchar('\n')

Here are examples for reading and writing files. The function readline was adapted to Windows (I did a PR for StaticTools).

fp = fopen(file_name, m"r")
str = readline(fp)
fclose(fp)

fp = fopen(file_name, m"w")
printf(fp, c"hello")
fclose(fp)

StaticTools also supplies functions to run system functions, convert strings to numbers, measure time, sleep (adapted for Windows), allocate memory, and load dll’s and apply functions of them.

StaticTools.system(c"dir")
float = StaticTools.strtod(c"12.34")[1]
int = StaticTools.strtol(c"12")[1]
t_now = StaticTools.time()

StaticTools also provides the symbolcall-macro for calling a symbol via LLVM, and can be used to call further C functions. For example, the system function and the time function mentioned above, would be done with

function sys(s)
@symbolcall system(pointer(s)::Ptr{UInt8})::Int
end

function time()
@symbolcall time(C_NULL::Ptr{Nothing})::Int
end

If you want the time in milliseconds, you can use

function time_ms() :: Int64
@symbolcall clock()::Int
end

StaticTools provides the function usleep. However, it does not seem to work in Windows. With help of symbolcall it can be adapted.

function sleep_win(secs::Real)
millisecs = round(Int, secs * 1000)
@symbolcall Sleep(millisecs :: Int) :: Int
end

Debugging - some guide lines

When there is an error during compiling, the following error messages are typical:
`f() did not infer to a concrete type. Got Union{}, an error when linking (ld.lld: error: undefined symbol), orclang: error: unable to execute command: program not executable`, or a huge error message regarding the LLVM IR.

From those error message it is hardly possible to distinguish, whether the error is due to some restriction of the static compilation, or simply due to an program error in the Julia code, much less to debug this code.

Thus it is a good practice, to:

Firstly, let run the Julia code without compiling.
Here you can use your favourite debug and trace tools, e.g. @info and @show. Writing in file seems to disturb the printing on stdout. So you would have to leave out any writing to a file.

Second, check whether any GC allocations occur.

You can do this, e.g. with help of the macro @time, or some tool like Allocations. If allocations occur, identify the code segments that cause allocations, and find allocation-free alternatives.

Third, compile using compile_executable
When here an error message occurs, then locate the code part, leading to the compilation error. After that, find altenatives for the error-causing lines of code.

In general, it is of course helpful to start compiling smaller code pieces, and then getting more and more code to compile.

Conclusion

StaticCompiler is a great tool to compile StaticCompiler small, stand-alone, executable binaries. The generated binaries are in the range usually between around 90 and 300 kB (e.g. for 1500 lines Julia code I got a binary of 210 KB). In contrast to StaticCompiler, PackageCompiler would produce a system image, taking hundreds of MB.

StaticTools provides a lot of allocation-free functions and data structures. A few have to be adapted to the respective operating system. Dictionaries and unions cannot be compiled so far. It would be a promising project, to extend StaticTools, implementing allocation-free alternatives of these data structures.

Top comments (0)