Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

compile-time performance formatting large enums #22229

Open
marler8997 opened this issue Dec 14, 2024 · 2 comments
Open

compile-time performance formatting large enums #22229

marler8997 opened this issue Dec 14, 2024 · 2 comments
Labels
bug Observed behavior contradicts documented or intended behavior

Comments

@marler8997
Copy link
Contributor

marler8997 commented Dec 14, 2024

Zig Version

0.13.0

Steps to Reproduce and Observed Behavior

Run the following zig program to observe performance of the compiler when the program contains code that formats an enum that contains many values with long names.

const std = @import("std");

var arena_instance = std.heap.ArenaAllocator.init(std.heap.page_allocator);
const arena = arena_instance.allocator();

pub fn main() !void {
    for ([_]usize{ 1, 2000, 3000, 4000 }) |value_count| {
        try testPerf(value_count, .{ .exhaustive_enum = true });
        try testPerf(value_count, .{ .exhaustive_enum = false });
    }
}

fn testPerf(value_count: usize, opt: struct {
    exhaustive_enum: bool,
}) !void {
    var file = try std.fs.cwd().createFile("bigenum.zig", .{});
    try file.writer().writeAll(
        \\const std = @import("std");
        \\pub fn main() !void {
        \\    const a = try std.process.argsAlloc(std.heap.page_allocator);
        \\    std.log.info("{}", .{@as(E, @enumFromInt(a.len))});
        \\}
        \\extern fn getenum() E;
        \\const E = enum(u32) {
        \\
    );
    for (0..value_count) |i| {
        try file.writer().print("    _{}_______________________________________,\n", .{i});
    }
    if (!opt.exhaustive_enum) {
        try file.writer().writeAll("    _,\n");
    }
    try file.writer().writeAll("};\n");
    file.close();

    const start_time = std.time.timestamp();
    try std.io.getStdOut().writer().print("starting zig...\n", .{});
    const result = try std.process.Child.run(.{
        .allocator = arena,
        .argv = &.{ "zig", "build-exe", "bigenum.zig", "-OReleaseSafe" },
    });
    try std.io.getStdOut().writer().writeAll(result.stdout);
    try std.io.getStdErr().writer().writeAll(result.stderr);
    const duration = std.time.timestamp() - start_time;
    switch (result.term) {
        .Exited => |c| if (c != 0) std.debug.panic("zig exited with {}", .{c}),
        else => |sig| std.debug.panic("zig {s} code {}", .{ @tagName(result.term), sig }),
    }
    try std.io.getStdOut().writer().print(
        "zig took {} second(s) ({} values, exhaustive_enum={})\n",
        .{ duration, value_count, opt.exhaustive_enum },
    );
}

Expected Behavior

I'd expect zig to be able to compile the program above faster. In the worst case (a non-exhaustive enum with 4000 values) it takes 94 seconds to compile. Also notes that 3000 values takes 43 seconds so the performance growth is super-linear. Here is the output from running on an M1 macbook air:

starting zig...
zig took 4 second(s) (1 values, exhaustive_enum=true)
starting zig...
zig took 4 second(s) (1 values, exhaustive_enum=false)
starting zig...
zig took 8 second(s) (2000 values, exhaustive_enum=true)
starting zig...
zig took 16 second(s) (2000 values, exhaustive_enum=false)
starting zig...
zig took 7 second(s) (3000 values, exhaustive_enum=true)
starting zig...
zig took 43 second(s) (3000 values, exhaustive_enum=false)
starting zig...
zig took 7 second(s) (4000 values, exhaustive_enum=true)
starting zig...
zig took 94 second(s) (4000 values, exhaustive_enum=false)

Note that this issue was discovered when I was printing an win32 error code which comes from an enum with almost 4000 values. I normally call a fmt function that uses a code-generated switch to avoid having to use the inline for statement that std.fmt uses (see https://github.com/marlersoft/zigwin32/blob/32d085ee67374ad2ec24fc012e6876ca27958fb7/win32/foundation.zig#L9737), however in one place I accidently printed the enum value instead of calling the function that uses the switch and I thought the compiler was hanging, but turns it it was just taking multiple minutes to compile with that one line change.

@marler8997 marler8997 added the bug Observed behavior contradicts documented or intended behavior label Dec 14, 2024
@Rexicon226
Copy link
Contributor

Rexicon226 commented Dec 14, 2024

if (opt.exhaustive_enum) {
    try file.writer().writeAll("    _,\n");
}

I assume this is backwards? Adding _ makes it a non-exhaustive enum which triggers the inline for in std.fmt.format to run.

@marler8997
Copy link
Contributor Author

Thanks @Rexicon226, I've corrected the example and output. This time I tested it on my windows machine with an i9-10900K (base speed is 3.7 GHz turbos up to 5.3 GHz) desktop which is almost twice as bad as my little M1 macbook air:

starting zig...
zig took 6 second(s) (1 values, exhaustive_enum=true)
starting zig...
zig took 6 second(s) (1 values, exhaustive_enum=false)
starting zig...
zig took 13 second(s) (2000 values, exhaustive_enum=true)
starting zig...
zig took 25 second(s) (2000 values, exhaustive_enum=false)
starting zig...
zig took 10 second(s) (3000 values, exhaustive_enum=true)
starting zig...
zig took 71 second(s) (3000 values, exhaustive_enum=false)
starting zig...
zig took 11 second(s) (4000 values, exhaustive_enum=true)
starting zig...
zig took 162 second(s) (4000 values, exhaustive_enum=false)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Observed behavior contradicts documented or intended behavior
Projects
None yet
Development

No branches or pull requests

2 participants