`inline fn` vs `callconv(.Inline)` vs `@call(.always_inline, ...)`

I believed that this was a trivial question but I failed to find any summary on Google, so sorry if the answer is already there but I overlooked.

But what are the commonalities and differences between the varied styles of declaring inline functions? By instinction it looks like the callconv approach is an approximate of force inline.

From the document:

For inline fn:

inline can be used to label a loop expression such that it will be unrolled at compile time. It can also be used to force a function to be inlined at all call sites.

For the callconv:

// The inline calling convention forces a function to be inlined at all call sites.
// If the function cannot be inlined, it is a compile-time error.
fn shiftLeftOne(a: u32) callconv(.Inline) u32 {
    return a << 1;
}

For the always inline modifier:

pub const CallModifier = enum {
    /// Guarantees that the call will inlined at the callsite.
    /// If this is not possible, a compile error is emitted instead.
    always_inline,

    ...
};

Here’s a similar question: Inlining functions

Thanks. It’s interesting that all three questions of my own are answered there. LoL.

Inline line is one of those keywords that can mean so many things in different languages, so I think this question gives us a chance to explore a bit. @dude_the_builder gave a good link to read more about the convention itself, but I’d like to add something from the software design perspective.

There is a value in having all three types of conventions and they each give unique opportunities. Let’s look at one example…


Keyword vs Parameter…

The nice thing about having a parameter is that it is comptime flexible. That enum value is, well… just a value. We can take it in as a parameter in another function. This allows you to pass in different calling options and then parameterize your calls further - this could be good for bulk testing or even just exploring performance options. So for instance…

fn foo(x: i32, y: i32) i32 {
    return x + y;   
}
fn bar(x: i32, y: i32) i32 {
    return x - y;
}

const CallModifier = @import("std").builtin.CallModifier;

fn fooBar(comptime modifier: CallModifier, x: i32, y: i32) i32 {
    const a = @call(modifier, foo, .{x, y});
    const b = @call(modifier, bar, .{x, y});
    return a * b;
}


test "Parametric Call Modifier" {
    // always inline foo and bar
    _ = fooBar(.always_inline, 42, 43);

    // never inline foo and bar
    _ = fooBar(.never_inline, 42, 43);
}

You can see how we get to control more options here than we otherwise could. You can do a lot of performance tuning with this sort of thing (for instance, we many not want something like a Lippincott function being inlined).

I hope this gives you some more ideas to explore :slight_smile:

2 Likes

It’s really innovative and refreshing that Zig tries to expose so much about the internals of the compiler, trying to reduce the size & volume of blackboxes, so that the flexibility in the language is greatly boosted. I am really enlightened by this point. I believe the excellent tool chain and the strong comptime capabilities are highly related, both rallied under the same philosophy.

1 Like