No niche optimization for optional enums

I hit this writing a chess engine: my board’s mailbox is [64]?Piece, and it’s 128 bytes instead of 64, because ?Piece is 2 bytes — even though Piece only uses 12 of the 256 possible tag values.

An exhaustive enum(u8) with fewer than 256 variants has unused tag values, i.e. a niche that could encode null. But Zig doesn’t use it:

const std = @import("std");

const Piece = enum(u8) {
    wp, wn, wb, wr, wq, wk,
    bp, bn, bb, br, bq, bk,
}; // 12 variants → tags 12..=255 are invalid

pub fn main() void {
    std.debug.print("@sizeOf(Piece)   = {d}\n", .{@sizeOf(Piece)});   // 1
    std.debug.print("@sizeOf(?Piece)  = {d}\n", .{@sizeOf(?Piece)});  // 2
    std.debug.print("@sizeOf(?*Piece) = {d}\n", .{@sizeOf(?*Piece)}); // 8
}

Output (0.16):

@sizeOf(Piece)   = 1
@sizeOf(?Piece)  = 2
@sizeOf(?*Piece) = 8

What makes me think this is “could be better” rather than fundamental: ?*Piece is 8 bytes — the same as *Piece — because optional pointers already get a niche (null = address 0). So the optional-niche machinery exists; it just isn’t applied to the spare discriminant values of an enum, where ?Piece instead pays a full extra flag byte (plus padding for larger payloads).

For reference, the analogous type in Rust folds None into an unused tag (Option<Piece> is 1 byte, and it even nests — Option<Option<Piece>> is still 1), so the optimization is at least known to be feasible.

#[repr(u8)]
enum Piece { Wp, Wn, Wb, Wr, Wq, Wk, Bp, Bn, Bb, Br, Bq, Bk }

fn main() {
    println!("{}", std::mem::size_of::<Piece>());               // 1
    println!("{}", std::mem::size_of::<Option<Piece>>());       // 1  <- niche
    println!("{}", std::mem::size_of::<Option<Option<Piece>>>()); // 1  <- nested niches!
}

My questions:

  1. Is this an intentional design decision (layout stability, keeping ?T predictable, comptime simplicity…), or just not implemented yet?
  2. Is there an existing issue or proposal tracking it? I’d rather read the rationale than assume it’s an oversight.
  3. Is there an idiomatic way to opt in today? For now I hand-rolled a sentinel — const Slot = enum(u8) { empty = 12, _ }; with an accessor that returns ?Piece — which gets the mailbox back to 64 bytes, but it’d be nice if ?Piece just did this.

Thanks!

1 Like

It’s meant to be implemented eventually but not currently a priority for the team, as far as I understand. For an enum just add a none variant.

3 Likes

niche optimisation currently only exists for pointers, and that is manually implemented. I would expect that zig eventually implement it more broadly, but I am unaware if there are actual plans to do so.

But you can implement that optimisation yourself with an extra enum { null } enum field.

3 Likes
4 Likes

I tried every trick on earth in my chessprogram.
And this stayed the clear winner of at least 5 different approaches.

The board is defined as:

mailbox: [64]Piece,

And the piece like this:

pub const Piece = packed union {
    e: E,
    u: u4,

    pub const count: usize = 12;

    pub const E = enum(u4) {
        white_pawn   = 0,
        white_knight = 1,
        white_bishop = 2,
        white_rook   = 3,
        white_queen  = 4,
        white_king   = 5,
        black_pawn   = 6,
        black_knight = 7,
        black_bishop = 8,
        black_rook   = 9,
        black_queen  = 10,
        black_king   = 11,
        no_piece     = 12,
    };

    /// All valid pieces.
    pub const all: [12]Piece = .{
        white_pawn, white_knight, white_bishop, white_rook, white_queen, white_king,
        black_pawn, black_knight, black_bishop, black_rook, black_queen, black_king,
    };

    pub const white_pawn   : Piece = .{ .e = .white_pawn };
    pub const white_knight : Piece = .{ .e = .white_knight };
    pub const white_bishop : Piece = .{ .e = .white_bishop };
    pub const white_rook   : Piece = .{ .e = .white_rook };
    pub const white_queen  : Piece = .{ .e = .white_queen };
    pub const white_king   : Piece = .{ .e = .white_king };
    pub const black_pawn   : Piece = .{ .e = .black_pawn };
    pub const black_knight : Piece = .{ .e = .black_knight };
    pub const black_bishop : Piece = .{ .e = .black_bishop };
    pub const black_rook   : Piece = .{ .e = .black_rook };
    pub const black_queen  : Piece = .{ .e = .black_queen };
    pub const black_king   : Piece = .{ .e = .black_king };
    pub const no_piece     : Piece = .{ .e = .no_piece };
};
1 Like

What is u

u is the direct int with which array indexing is easily done. As well as math operations.
The same trick I use with Square which is otherwise a @enum_conversion hell.

What’s wrong with @intFromEnum (I’m on mobile so forgive if I misremembered the name)

Nothing but:

  • it adds a useless and instruction. Slower than my u. (for other types than u8, 16 etc.)
  • you have to write functions around it, to keep readability.

The problem with this is that it doesn’t work with orelse.