Better understanding of comptime and loop unrolling

Hi,

I’d like to reassure myself, that my assumptions about comptime and loop unrolling are correct.

Let’s say we have this (pointless) code:

const std = @import("std");

const Container = struct {
    uu1: u1 = 1,
    uu2: u2 = 2,
    uu3: u3 = 4,

    pub fn getPointer(self: *Container, T: type) *T {
        const fields = std.meta.fields(Container);

        inline for (fields) |fld| {
            if (fld.type == T) {
                return &@field(self, fld.name);
            }
        }
    }
};

pub fn main() !void {
    var c = Container{};

    const uu1 = c.getPointer(u1);
    try std.testing.expectEqual(1, uu1.*);

    const uu2 = c.getPointer(u2);
    try std.testing.expectEqual(2, uu2.*);

    const uu3 = c.getPointer(u3);
    try std.testing.expectEqual(4, uu3.*);
}

I would assume, that the compiler internally creates three functions for each type we call getPointer() for. So internally this would produce something like:

const Container = struct {
   [...]

    pub fn u1_getPointer(self: *Container, T: u1) *u1 { [...] }
    pub fn u2_getPointer(self: *Container, T: u2) *u2 { [...] }
    pub fn u3_getPointer(self: *Container, T: u3) *u3 { [...] }
};

And secondly the ‘inline for’ loop will internally be unrolled into (e: substituting fld.type and fld.name) :

	pub fn getPointer(self: *Container, T: type) *T {
        
        if (@TypeOf(Container.uu1) == T) {
            return &@field(self, 'uu1');
        }
        if (@TypeOf(Container.uu2) == T) {
            return &@field(self, 'uu2');
        }
        if (@TypeOf(Container.uu3) == T) {
            return &@field(self, 'uu3');
        }
   
    }

And if we take both these assumptions together, we get internally three functions with something like:

pub fn u1_getPointer(self: *Container, T: u1) *u1 { 
	
    if (@TypeOf(Container.uu1) == u1) {			// <- true
        return &@field(self, 'uu1');
    }
    if (@TypeOf(Container.uu2) == u1) {			// <- false
        return &@field(self, 'uu2');
    }
    if (@TypeOf(Container.uu3) == u1) {			// <- false
        return &@field(self, 'uu3');
    }
}

// The same for u2 and u3

If that is correct so far: when the compiler optimizes these functions, are the false if statements optimized away? Since the values are comptime known, I would expect the compiler to be clever enough to generate code for the container as if we wrote this:

const Container = struct {
    uu1: u1 = 1,
    uu2: u2 = 2,
    uu3: u3 = 4,

    pub fn u1_getPointer(self: *Container) *u1 { 
		return &self.uu1; 
	}
	pub fn u2_getPointer(self: *Container) *u2 { 
		return &self.uu2; 
	}
	pub fn u3_getPointer(self: *Container) *u3 { 
		return &self.uu3; 
	}
};

Are these ideas about comptime and optimizations correct?

Thanks!

yes they get optimised away, this is how zig does conditional compilation too

2 Likes

Ok, great! Thanks a lot! :slightly_smiling_face: