Skip to content

A Simple Function

An inline function is a function where the compiler may place the function’s code directly at the call site instead of performing a normal function call.

Inline Functions

An inline function is a function where the compiler may place the function’s code directly at the call site instead of performing a normal function call.

Normally, calling a function works like this:

caller
  -> jump to function
  -> run function
  -> return back

Inlining changes this.

Instead of jumping to the function, the compiler copies the function body directly into the caller.

Conceptually:

replace function call with function code

Inlining is mainly a performance optimization.

A Simple Function

Example:

fn square(x: i32) i32 {
    return x * x;
}

Using it:

const result = square(5);

Normally, this creates a function call.

With inlining, the compiler may transform it conceptually into:

const result = 5 * 5;

No actual function call happens at runtime.

Why Function Calls Cost Something

A normal function call usually involves:

  • saving return information
  • jumping to another memory location
  • creating a stack frame
  • returning afterward

Modern CPUs are fast, but function calls still have overhead.

Inlining removes much of this overhead.

Zig and Inlining

Zig gives the compiler strong freedom to optimize.

The compiler may inline small functions automatically.

You can also request inlining explicitly.

Example:

inline fn square(x: i32) i32 {
    return x * x;
}

The keyword:

inline

requests inlining.

Inline Is a Request

Important:

inline does not mean “guaranteed faster”

Inlining can improve performance, but excessive inlining can also increase binary size.

Large binaries may reduce instruction-cache efficiency.

Optimization always involves tradeoffs.

A Complete Example

const std = @import("std");

inline fn add(a: i32, b: i32) i32 {
    return a + b;
}

pub fn main() void {
    const result = add(10, 20);

    std.debug.print("{}\n", .{result});
}

Output:

30

The program behaves normally whether the compiler inlines the function or not.

Inlining changes implementation details, not program meaning.

Inline Functions and Small Helpers

Inlining is most useful for:

  • tiny helper functions
  • math operations
  • wrappers
  • hot code paths
  • compile-time specialization

Example:

inline fn max(a: i32, b: i32) i32 {
    return if (a > b) a else b;
}

This is a good inline candidate because the function body is extremely small.

Large Inline Functions

Large functions are poor inline candidates.

Bad example:

inline fn processHugeDatabase() void {

    // 2000 lines

}

Inlining very large functions can:

  • increase binary size dramatically
  • reduce cache efficiency
  • increase compile times

Inlining is usually best for small operations.

Inline Loops

Zig also supports inline loops.

Example:

inline for (.{ 1, 2, 3 }) |value| {
    std.debug.print("{}\n", .{value});
}

This loop executes at compile time.

The compiler expands it conceptually into:

std.debug.print("{}\n", .{1});
std.debug.print("{}\n", .{2});
std.debug.print("{}\n", .{3});

Inline loops are extremely important in Zig metaprogramming.

Inline Branches

Example:

inline if (condition) {

}

This allows compile-time branch evaluation.

Usually this appears with comptime.

Compile-Time Specialization

Inlining is closely related to compile-time programming.

Example:

fn add(
    comptime T: type,
    a: T,
    b: T,
) T {
    return a + b;
}

The compiler generates specialized versions for each type.

Example calls:

add(i32, 1, 2);
add(f64, 1.5, 2.5);

This often combines naturally with inlining.

Inlining and Generics

Generic code frequently becomes inline-expanded.

Why?

Because specialized code is easier to optimize aggressively.

This is one reason Zig generic abstractions can remain very fast.

Function Call Overhead Example

Suppose:

fn increment(x: i32) i32 {
    return x + 1;
}

Used millions of times:

while (i < 1000000) : (i += 1) {
    total += increment(i);
}

Inlining may remove repeated function-call overhead.

The compiler can often optimize further afterward.

Inlining Enables Other Optimizations

Inlining is powerful because it exposes more code to the optimizer.

Example:

inline fn multiplyByTwo(x: i32) i32 {
    return x * 2;
}

After inlining:

value * 2

The compiler may then simplify further.

Without inlining, some optimizations are harder.

Recursive Inline Functions

Inlining recursive functions is difficult.

Example:

inline fn factorial(n: u32) u32 {
    if (n == 0) {
        return 1;
    }

    return n * factorial(n - 1);
}

Unlimited expansion is impossible.

Compilers must handle recursion carefully.

Too Much Inlining

Excessive inlining can hurt performance.

Possible problems:

ProblemExplanation
Larger binariesmore duplicated code
Worse cache behaviorinstruction cache pressure
Longer compile timesmore generated machine code
Harder debuggingcall structure disappears

Inlining is not automatically beneficial everywhere.

Debugging Inline Functions

Inlining can make debugging harder.

Why?

Because the original function call may disappear after optimization.

Stepping through code in a debugger may feel different between:

  • debug builds
  • optimized builds

This is normal in systems programming.

Inline and Readability

Do not inline functions just because they are small.

This is unnecessary:

inline fn getZero() i32 {
    return 0;
}

Inlining should solve a real problem.

Usually:

clarity first
performance second

Measure performance before optimizing aggressively.

Zig Often Optimizes Automatically

Modern Zig compilers already perform many automatic optimizations.

Small functions are frequently inlined automatically even without the inline keyword.

Therefore:

explicit inline is often unnecessary

Use it deliberately.

Inline and comptime

One of Zig’s most important ideas:

compile-time execution

Inlining often interacts with comptime.

Example:

inline for (@typeInfo(T).Struct.fields)
    |field|
{

}

This allows code generation during compilation.

You will encounter this style heavily in advanced Zig code.

A Complete Example

const std = @import("std");

inline fn clamp(
    value: i32,
    min: i32,
    max: i32,
) i32 {
    if (value < min) {
        return min;
    }

    if (value > max) {
        return max;
    }

    return value;
}

pub fn main() void {
    const a = clamp(5, 0, 10);
    const b = clamp(-5, 0, 10);
    const c = clamp(50, 0, 10);

    std.debug.print(
        "{} {} {}\n",
        .{ a, b, c },
    );
}

Output:

5 0 10

This function is a good inline candidate because:

  • very small
  • called frequently
  • performance-sensitive
  • simple branching

Mental Model

Inlining means:

replace a function call with the function body itself

Instead of:

jump to function

the compiler may produce:

direct embedded instructions

Inlining can:

  • reduce function-call overhead
  • enable more optimizations
  • improve performance

But excessive inlining can also create larger, slower binaries.

Good systems programming balances:

  • readability
  • maintainability
  • performance
  • code size

Zig gives you explicit control when needed, while still allowing the compiler to optimize aggressively automatically.