Overview
The Zig compiler can use comptime-known information to produce optimized assembly code. These optimizations can involve (but are not limited to) removing read/store instructions and pre-calculating results.
Example 1: Const vs Var Integers
In this example, we’ll calculate the square of a value x
and return it to the user using different qualifiers for x
. This example does not have any compiler optimizations applied.
We begin with var
:
var x: i32 = 24;
export fn foo() i32 {
return x * x;
}
foo:
push rbp
mov rbp, rsp
sub rsp, 16
mov eax, dword ptr [example.x]
imul eax, dword ptr [example.x]
mov dword ptr [rbp - 4], eax
seto al
jo .LBB0_1
jmp .LBB0_2
We can see that example.x
is being loaded and a multiplication operations occurs.
Now for const
:
const x: i32 = 24;
export fn foo() i32 {
return x * x;
}
foo:
push rbp
mov rbp, rsp
mov eax, 576
pop rbp
ret
Here we see that the direct value 576
(which is the square of 24) has been pre-computed and any reference to example.x
has been removed.
Example 2: Comptime Keyword
Comptime-known information can also be subject to optimizations similar to example 1. In this case, we will make a function that takes a comptime
parameter and observe similar results. To begin, no compiler optimizations are applied:
fn bar(comptime x: i32) i32 {
return x * x;
}
export fn foo() i32 {
return bar(11) * bar(12);
}
This generates 3 segments of interest:
example.bar__anon_861:
push rbp
mov rbp, rsp
mov eax, 121
pop rbp
ret
example.bar__anon_862:
push rbp
mov rbp, rsp
mov eax, 144
pop rbp
ret
foo:
push rbp
mov rbp, rsp
sub rsp, 16
call example.bar__anon_861
mov dword ptr [rbp - 8], eax
call example.bar__anon_862
mov ecx, eax
mov eax, dword ptr [rbp - 8]
imul eax, ecx
mov dword ptr [rbp - 4], eax
seto al
jo .LBB0_1
jmp .LBB0_2
Without additional optimization flags, we can see that both bar(11)
and bar(12)
behave similar to our first example while foo
has call and multiply operations.
With ReleaseFast
we see this:
foo:
mov eax, 17424
ret
All calls to bar
were elminated and a single number is pre-calculated and returned. In fact, bar
does not even appear in the assembly output. This may be due to factors such as inlining.
The comptime keyword can also be used in front of a function call to create an effect similar to using comptime parameters without generating anonymous function overloads (no compiler optimizations are applied):
fn bar(x: i32) i32 {
return x * x;
}
export fn foo() i32 {
return comptime bar(11) * bar(12);
}
foo:
push rbp
mov rbp, rsp
mov eax, 17424
pop rbp
ret
Example 3: Struct of Integers
In this example, we’ll make a user defined type with integers and see if it also picks up similar optimizations based on const
vs var
. We begin with var
and no additional optimizations.
var data: struct { x: i32, y: i32 } = .{ .x = 41, .y = 42 };
export fn foo() i32 {
return data.x * data.y;
}
foo:
push rbp
mov rbp, rsp
sub rsp, 16
mov eax, dword ptr [example.data]
imul eax, dword ptr [example.data+4]
mov dword ptr [rbp - 4], eax
seto al
jo .LBB0_1
jmp .LBB0_2
Here, we see the a similar pattern with both integer members involved in a multiplication instruction.
With const
:
const data: struct { x: i32, y: i32 } = .{ .x = 41, .y = 42 };
export fn foo() i32 {
return data.x * data.y;
}
foo:
push rbp
mov rbp, rsp
mov eax, 1722
pop rbp
ret
All references to our data
struct has been removed and the result has also been pre-calculated. This shows that similar optimizations are applied to user defined types as well as fundamental types.