Now I have programmed with StaticCompiler for one year, and I’d like to share some experiences. My main conclusion is that a lot of Julia code can be compiled, perhaps more than one might think.
Static compilation imposes some limitations to the julia code. GC allocations are not allowed. Additionally, the code should be type-stable. You cannot use non-costant global variables. All functions must return only native types, or must be inlined.
StaticTools provides a lot of alternative data types and functions to ensure allocation-free Julia code. In the following are some examples.
Strings are implemented as MallocString
or StaticString
, and can 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 iteself, 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.
Printing to stdout can be done with print
, println
and printf
(in C matter). Flushing (and a newline) does 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, converting strings to numbers, 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_stc(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
), or clang: 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 the source code.
Thus it is a good practice, to
-
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 comment out any writing to a file.
-
check whether any GC allocations occur.
-
You can do this, e.g. with help of the tool @time
, or some tool like Allocations. If allocations occur, identify the code segments that cause allocations, and find allocation-free alternatives.
-
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 pieces of code, and then getting more and more code to compile.
My conclusion is that StaticCompiler a 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.