(You can safely skip this section on a first reading.) Julia works with an LLVM JIT compiler framework that is used for JIT generation of machine code. The first time you run a Julia function, it is parsed, and the types are inferred. Then, LLVM code is generated by the JITÂ compiler, which is then optimized and compiled down to native code. The second time you run a Julia function, the native code that's already generated is called. This is the reason why, the second time you call a function with arguments of a specific type, it takes much less time to run than the first time (keep this in mind when doing benchmarks of Julia code).
This generated code can be inspected. Suppose, for example, that we have defined a f(x) = 2x + 5 function in a REPL session. Julia responds with the message f (generic function with one method); the code is dynamic because we didn't have to specify the type of x or f. Functions are, by default, generic in Julia because they are ready to work with different data types for their variables.
The code_llvm function can be used to see the JIT bytecode. This is the bytecode generated by LLVM, and it will be different for each target platform. For example, for the Intel x64 platform, if the x argument is of type Int64, it will be as follows:
julia> code_llvm(f, (Int64,))
; Function f
; Location: REPL[7]:1
; Function Attrs: uwtable
define i64 @julia_f_33833(i64) #0 {
top:
; Function *; {
; Location: int.jl:54
%1 = shl i64 %0, 1
;}
; Function +; {
; Location: int.jl:53
%2 = add i64 %1, 5
;}
ret i64 %2
}
The code_native function can be used to see the assembly code that was generated for the same type of x:
julia> code_native(f, (Int64,))
.text
; Function f {
; Location: REPL[7]:1
pushq %rbp
movq %rsp, %rbp
; Function +; {
; Location: int.jl:53
leaq 5(%rcx,%rcx), %rax
;}
popq %rbp
retq
nopl (%rax,%rax)
;}
Compare this with the code generated when x is of type Float64:
julia> code_native(f, (Float64,))
.text
; Function f {
; Location: REPL[7]:1
pushq %rbp
movq %rsp, %rbp
; Function *; {
; Location: promotion.jl:314
; Function *; {
; Location: float.jl:399
vaddsd %xmm0, %xmm0, %xmm0
movabsq $424735072, %rax # imm = 0x1950F160
;}}
; Function +; {
; Location: promotion.jl:313
; Function +; {
; Location: float.jl:395
vaddsd (%rax), %xmm0, %xmm0
;}}
popq %rbp
retq
nopl (%rax,%rax)
;}
Julia code is fast because it generates specialized versions of functions for each data type. Julia also implements automatic memory management. The user doesn't have to worry about allocating and keeping track of the memory for specific objects. Automatic deletion of objects that are not needed anymore (and hence, reclamation of the memory associated with those objects) is done using a garbage collector (GC).
The GC runs at the same time as your program. Exactly when a specific object is garbage collected is unpredictable. The GC implements an incremental mark-and-sweep algorithm. You can start garbage collection yourself by calling GC.gc(), or if you don't need it, you can disable it by calling GC.enable(false).
The standard library is implemented in Julia itself. The I/O functions rely on the libuv library for an efficient, platform-independent I/O. The standard library is contained in a package called Base, which is automatically imported when starting Julia.