kind of. For a basic example, consider the code
julia> function fib(x)
x < 1 && return 1
return fib(x-1) + fib(x-2)
end
When a user writes fib(10)
the function gets compiled for an Int
input type. The first step here is parsing and lowering, which converts the code to
julia> @code_lowered fib(10)
CodeInfo(
1 ─ %1 = x < 1
└── goto #3 if not %1
2 ─ return 1
3 ─ %4 = x - 1
│ %5 = Main.fib(%4)
│ %6 = x - 2
│ %7 = Main.fib(%6)
│ %8 = %5 + %7
└── return %8
)
The main things that change in this step are that the code is changed to single assignment (SSA) form. Next comes type inference which produces
julia> @code_typed optimize=false fib(10)
CodeInfo(
1 ─ %1 = (x < 1)::Bool
└── goto #3 if not %1
2 ─ return 1
3 ─ %4 = (x - 1)::Int64
│ %5 = Main.fib(%4)::Int64
│ %6 = (x - 2)::Int64
│ %7 = Main.fib(%6)::Int64
│ %8 = (%5 + %7)::Int64
└── return %8
) => Int64
This is the result of Julia doing an abstract interpretation of the code and tracking what possibilities of types there are. After this, some optimizations like inlining are applied to this typed code.
CodeInfo(
1 ─ %1 = Base.slt_int(x, 1)::Bool
└── goto #3 if not %1
2 ─ return 1
3 ─ %4 = Base.sub_int(x, 1)::Int64
│ %5 = Base.slt_int(%4, 1)::Bool
└── goto #5 if not %5
4 ─ goto #6
5 ─ %8 = Base.sub_int(%4, 1)::Int64
│ %9 = invoke Main.fib(%8::Int64)::Int64
│ %10 = Base.sub_int(%4, 2)::Int64
│ %11 = invoke Main.fib(%10::Int64)::Int64
│ %12 = Base.add_int(%9, %11)::Int64
└── goto #6
6 ┄ %14 = φ (#4 => 1, #5 => %12)::Int64
│ %15 = Base.sub_int(x, 2)::Int64
│ %16 = Base.slt_int(%15, 1)::Bool
└── goto #8 if not %16
7 ─ goto #9
8 ─ %19 = Base.sub_int(%15, 1)::Int64
│ %20 = invoke Main.fib(%19::Int64)::Int64
│ %21 = Base.sub_int(%15, 2)::Int64
│ %22 = invoke Main.fib(%21::Int64)::Int64
│ %23 = Base.add_int(%20, %22)::Int64
└── goto #9
9 ┄ %25 = φ (#7 => 1, #8 => %23)::Int64
│ %26 = Base.add_int(%14, %25)::Int64
└── return %26
) => Int64
Here, the main changes are that simple functions get removed and replaced by their definition. This is the level that precompilation stores. After this, the typed code gets passed to LLVM (a C/C++ compiler) which produces LLVM IR
julia> @code_llvm debuginfo=:none fib(10)
define i64 @julia_fib_514(i64 signext %0) #0 {
top:
%1 = icmp sgt i64 %0, 0
br i1 %1, label %L4, label %common.ret
common.ret: ; preds = %L4, %top
%common.ret.op = phi i64 [ %6, %L4 ], [ 1, %top ]
ret i64 %common.ret.op
L4: ; preds = %top
%2 = add nsw i64 %0, -1
%3 = call i64 @julia_fib_514(i64 signext %2) #0
%4 = add nsw i64 %0, -2
%5 = call i64 @julia_fib_514(i64 signext %4) #0
%6 = add i64 %5, %3
br label %common.ret
}
and then turns into native code (which is what sysimages store)
julia> @code_native debuginfo=:none fib(10)
.text
.file "fib"
.globl julia_fib_538 # -- Begin function julia_fib_538
.p2align 4, 0x90
.type julia_fib_538,@function
julia_fib_538: # @julia_fib_538
.cfi_startproc
# %bb.0: # %top
testq %rdi, %rdi
jle .LBB0_1
# %bb.3: # %L4
pushq %r15
.cfi_def_cfa_offset 16
pushq %r14
.cfi_def_cfa_offset 24
pushq %rbx
.cfi_def_cfa_offset 32
.cfi_offset %rbx, -32
.cfi_offset %r14, -24
.cfi_offset %r15, -16
movq %rdi, %rbx
decq %rdi
movabsq $julia_fib_538, %r15
callq *%r15
movq %rax, %r14
addq $-2, %rbx
movq %rbx, %rdi
callq *%r15
addq %r14, %rax
popq %rbx
.cfi_def_cfa_offset 24
popq %r14
.cfi_def_cfa_offset 16
popq %r15
.cfi_def_cfa_offset 8
.cfi_restore %rbx
.cfi_restore %r14
.cfi_restore %r15
retq
.LBB0_1:
movl $1, %eax
retq
.Lfunc_end0:
.size julia_fib_538, .Lfunc_end0-julia_fib_538
.cfi_endproc
# -- End function
.section ".note.GNU-stack","",@progbits