#Zig equivalent of automatic performance counters (handmade hero)

1 messages · Page 1 of 1 (latest)

broken rock
#

I'd like to replicate Casey's automatic performance counters in zig https://www.youtube.com/watch?v=uHSLHvWFkto&t=3477s.

However, the process requires generating unique comptime ids , such that when the code hits a counter, the counter is able to find itself in an array of counters, so it can update hits, time spent etc. In C/C++ this is achievable using some pre-processor magic.

There are 2 parts to my question,

  1. is it possible to generate a comptime counter in zig. It seems like no. I've tried this https://ziggit.dev/t/c-c-macro-challenge-1-boost-pp-counter/2235 but it no longer works - I believe because of changes in the compiler.
  2. Are there alternative ways to do simple performance measurements? What's nice about Casey's approach is that there's minimal overhead.

Thanks in advance

cosmic kindle
#

you could use @src as unique code location marker but the counting would need to be done at runtime yeah

#

so at some location, where you'd want to install a counter you'd have something like:

debugCounter(@src());

optionally can accept comptime string for documenting purposes, but at least you know source file and line with this one by default

broken rock
#

Can't think of a way to get the counter even at runtime, as when each counter is constructed, it'll increment the counter for the array index. Which isn't what you want.

I can only think if its possible to create a hash from @src and then use a hashmap to get the id. Then it'll depend on how much of that can be comptime'd.

plush helm
# broken rock I'd like to replicate Casey's automatic performance counters in zig https://www...
pub inline fn zoneOptions(comptime label: []const u8, byte_count: u64) Zone {
    if (!enable) return .{};

    const unique_index_namespace = struct {
        var index: u32 = 0;
        comptime {
            _ = label.ptr;
        }
    };

    if (unique_index_namespace.index == 0) {
        unique_index_namespace.index = data.anchor_count + 1;
        data.anchor_count += 1;
    }

    const anchor_index = unique_index_namespace.index;

    var result: Zone = undefined;

    result.parent_index = data.active_anchor_index;

    result.anchor_index = anchor_index;
    result.label = label;

    const anchor: *Anchor = &data.anchors[anchor_index];
    result.old_tsc_elapsed_inclusive = anchor.tsc_elapsed_inclusive;
    anchor.processed_byte_count += byte_count;

    data.active_anchor_index = anchor_index;
    result.start_tsc = cpu_time.rdtsc();

    return result;
}

this is how I did it, I dont remember the tradeoffs of this though

#

I think you have to pass a unique label to each zone. @src could solve this

#

ha wait I thought you were following computer-enhance, my code is for a zone based profiler

broken rock
#

@plush helm thank you for sharing, although I don't know what a zone profiler is, so I don't really understand what this is trying to achieve 😅

#

My work in progress solution currently looks like:

const std = @import("std");

const DebugCounter = struct {
    const Self = @This();
    src: std.builtin.SourceLocation,
    hash: u32,
    hit: u16,
    stop: u16,
    pub fn reset(self: *Self) void {
        self.hit = 0;
        self.stop = 0;
    }
};

pub fn startDebugCounter(comptime src: std.builtin.SourceLocation) !u32 {
    const hash = comptime hashSrc(src);
    const gop = try map.getOrPut(hash);
    if (gop.found_existing) {
        gop.value_ptr.*.hit += 1;
    } else {
        gop.value_ptr.* = .{
            .src = src,
            .hash = hash,
            .hit = 1,
            .stop = 0,
        };
    }
    return hash;
}

pub fn stopDebugCounter(hash: u32) void {
    const entry = map.getPtr(hash);
    if (entry) |e| {
        e.*.stop += 1;
    } else {
        @panic("Stopping a debug counter that hasn't been created");
    }
}

fn hashSrc(comptime src: std.builtin.SourceLocation) u32 {
    var hasher = std.hash.Adler32.init();
    hasher.update(src.file);
    hasher.update(src.fn_name);

    var buffer: [4]u8 = undefined;
    buffer[0] = @intCast((src.line >> 0) & 0xFF);
    buffer[1] = @intCast((src.line >> 8) & 0xFF);
    buffer[2] = @intCast((src.line >> 16) & 0xFF);
    buffer[3] = @intCast((src.line >> 24) & 0xFF);

    hasher.update(&buffer);
    return hasher.final();
}

pub fn initDebugMemory() void {
    //
    gpa = std.heap.GeneralPurposeAllocator(.{}){};
    map = std.AutoArrayHashMap(u32, DebugCounter).init(gpa.allocator());
}

pub fn freeDebugMemory() void {
    //
    map.deinit();
    _ = gpa.deinit();
}

var gpa: std.heap.GeneralPurposeAllocator(.{}) = undefined;
var map: std.AutoArrayHashMap(u32, DebugCounter) = undefined;

pub fn getCountersMap() *std.AutoArrayHashMap(u32, DebugCounter) {
    return ↦
}

#

Usage:

test "usage" {
    initDebugMemory();

    const _1 = try startDebugCounter(@src());
    defer stopDebugCounter(_1);

    for (0..3) |_| {
        const _2 = try startDebugCounter(@src());
        defer stopDebugCounter(_2);
    }

    const counters = getCountersMap();
    var it = counters.iterator();
    while (it.next()) |*entry| {
        std.debug.print("{}\n", .{entry.value_ptr.*});
    }

    freeDebugMemory();
}
#

In summary it calculates a hash at comptime using the src information. Subsequent calls to startDebugCounter and stopDebugCounter, then access the DebugCounter struct using an AutoArrayHashMap(u32, DebugCounter).

#

There's a lot that I don't like, but I think since the hash is calculated at comptime, and using an array hashmap, it will have ok performance. Although not ideal.

plush helm
# broken rock <@573168799512133653> thank you for sharing, although I don't know what a zone p...

Looking at the handmade hero video it looks like its the same
mine is used like this:

pub fn main() !void {
    prof.beginProfile();
    {
        const zone = prof.zone(@src().fn_name);
        defer zone.end();

        std.time.sleep(1e6);
        {
            const inner_zone = prof.zone("inner");
            defer inner_zone.end();

            const inner_zone_2 = prof.zone("inner2");
            std.time.sleep(1e6);
            inner_zone_2.end();
            std.time.sleep(1e6);
        }
        std.debug.print("\n", .{});
    }
    prof.endAndPrintProfile();
}

wish yea is the same
it doesnt do allocations and doesnt use a hashmap

#

outputs results like this: (not the output of program above to show more)

Total time: 514.133ms (CPU freq 3593232000)
load file       [1]: 210480335 (11.393%) 168.541mb at 2.810gb/s    
parse           [1]: 72 (0.000%, 73.053% w/children)
struct parse    [1000001]: 1268599555 (68.669%, 73.053% w/children)
array parse     [1]: 80981477 (4.384%, 73.052% w/children)
compute average [1]: 285540264 (15.456%) 15.259mb at 0.188gb/s
broken rock
#

@plush helm so am I right in saying these are performance counters that maintain a hierarchical relationship?

cosmic kindle
#

pretty much + timing of the zone since you track zone start time and end time

plush helm