Skip to content

runtime: use zero byte as empty control word in maps (potential performance improvement) #70966

@colega

Description

@colega

Proposal Details

Context

The implementation of Swiss maps brought in Go 1.24 is more complex than previously seen implementations, but it retains one of the original details that everyone seems to implement in the same way: the control word used to denote an empty slot is 0b10000000 (and the one for deleted items is 0b11111110.

Problem

Every time a new table is allocated or it grows, it has to be filled with this specific "empty control word" pattern. For large maps, this is quite a lot of work.

Proposal

I propose to:

  • Use zero-byte 0b00000000 to denote an empty slot.
  • Use byte 1 (0b00000001) for deleted slots.
  • Add 2 to h2 to ensure that it always has some non-zero bit set in bits 1-7.

While writing this issue I saw a similar proposal in a TODO comment in the implementation.

// TODO(prattmic): Consider inverting the top bit so that the zero value is empty.
type ctrl uint8

What does this mean:

  • The most important thing: an zeroed group is filled with empty control words out of the box, so we don't have to do that manually on each allocation/growth of the table.
  • There's an extra operation to be done when splitting a key into the h1/h2 pair (plus two, my guess is no modern CPU will have a noticeable impact of that, but it's worth noting).
  • Depending on how the SIMD code is written, this may result in less operations needed.
    • When I proposed this to github.com/dolthub/swiss, I was able to use 2 less operations, although their version appears to be longer than the one implemented here in stdlib, but it's also designed for 16-element groups, which isn't done in Go yet.
    • I checked the code in stdlib and I don't even understand why it works yet1.

I previously sent a PR to dolthub's swiss map implementation implementing this change, and the benchmarks were quite promising (although I didn't test the SIMD path, since I was benchmarking on arm64).

Given that Go's version is quite more complex than that one, I decided to drop an issue before attempting to hack a proof-of-concept.

cc @prattmic

Footnotes

  1. The comment says that Empty slots are negated, becoming 1000 0000 (unchanged!)., but negating something should change it, right? I didn't find any docs regarding this behaviour, only ChatGPT explained me that since it's already the most negative value, it remains unchanged.

Metadata

Metadata

Assignees

No one assigned

    Labels

    NeedsInvestigationSomeone must examine and confirm this is a valid issue and not a duplicate of an existing one.Performancecompiler/runtimeIssues related to the Go compiler and/or runtime.

    Type

    No type

    Projects

    Status

    Todo

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions