LANGUAGES Signal 412
Shrinking Ruby Hashes
Illustration only Photo by 4motions Werbeagentur on Unsplash
The article examines why Ruby Hash objects consume more memory than comparable Struct objects and explores historical changes to the hash implementation that increased internal overhead.
Memory usage of transient objects like Hashes affects Copy-on-Write efficiency and increases pressure during request or job cycles. Reducing the footprint of Hashes could lower overall memory consumption in Ruby applications, but any change must balance speed, compatibility, and interpreter complexity.
Written by elseif from the cluster below · every claim links back to a sourceThe three things worth knowing
An empty Ruby Hash uses 160 bytes, which is two to four times the memory of a Struct with the same number of elements.
For hash sizes up to eight entries the internal representation is a packed entry structure rather than a traditional hash table.
Past revisions of the hash implementation grew the C header from 48 bytes to 56 bytes, adding to the per-object overhead.
THE READ
What the cluster adds up to.
The article measures memory consumption with ObjectSpace.memsize_of, showing that an empty Hash occupies 160 bytes while a comparable Struct uses only 40 bytes. As the number of elements grows, the Hash remains two to four times larger than the Struct across the tested range. This disparity highlights a steady source of allocator pressure in applications that create many short-lived Hashes during request processing.
A key historical shift discussed is the move to an open-addressing hash table, which altered the underlying C structure. The header struct grew from 48 bytes in the earlier design to 56 bytes after the change, increasing the baseline memory cost of every Hash instance regardless of its payload size. This header growth is presented as a major contributor to the observed overhead.
For very small Hashes (zero to eight entries) Ruby does not allocate a full hash table; instead it uses a packed entry layout that stores elements directly in the header space. Consequently, the memory advantage of switching to a more compact representation is limited for these sizes, and any optimization must consider both the packed and table-based paths.
Adopting a slimmer Hash would require modifications to the Ruby interpreter’s core data structures and possibly to the garbage collector’s handling of these objects. Such changes could affect lookup performance, introduce incompatibilities with C extensions that rely on the current layout, and necessitate extensive testing across Ruby versions.
The benefits of reducing the header size diminish for large Hashes where the memory consumed by the bins array dominates total usage. Any shrink-effort must preserve the collision-resolution properties of open addressing, so the approach stops working effectively once the number of bins becomes the primary memory consumer.
Written by elseif from the cluster below · checked for specifics the sources never containedTHE CLUSTER