Skip to content
  • John Hawthorn's avatar
    57b6a750
    Lock-free hash set for fstrings [Feature #21268] · 57b6a750
    John Hawthorn authored
    This implements a hash set which is wait-free for lookup and lock-free
    for insert (unless resizing) to use for fstring de-duplication.
    
    As highlighted in https://bugs.ruby-lang.org/issues/19288, heavy use of
    fstrings (frozen interned strings) can significantly reduce the
    parallelism of Ractors.
    
    I tried a few other approaches first: using an RWLock, striping a series
    of RWlocks (partitioning the hash N-ways to reduce lock contention), and
    putting a cache in front of it. All of these improved the situation, but
    were unsatisfying as all still required locks for writes (and granular
    locks are awkward, since we run the risk of needing to reach a vm
    barrier) and this table is somewhat write-heavy.
    
    My main reference for this was Cliff Click's talk on a lock free
    hash-table for java https://www.youtube.com/watch?v=HJ-719EGIts. It
    turns out this lock-free hash set is made easier to implement by a few
    properties:
    
     * We only need a hash set rather than a hash table (we only need keys,
       not values), and so the full entry can be written as a single VALUE
     * As a set we only need lookup/insert/delete, no update
     * Delete is only run inside GC so does not need to be atomic (It could
       be made concurrent)
     * I use rb_vm_barrier for the (rare) table rebuilds (It could be made
       concurrent) We VM lock (but don't require other threads to stop) for
       table rebuilds, as those are rare
     * The conservative garbage collector makes deferred replication easy,
       using a T_DATA object
    
    Another benefits of having a table specific to fstrings is that we
    compare by value on lookup/insert, but by identity on delete, as we only
    want to remove the exact string which is being freed. This is faster and
    provides a second way to avoid the race condition in
    https://bugs.ruby-lang.org/issues/21172.
    
    This is a pretty standard open-addressing hash table with quadratic
    probing. Similar to our existing st_table or id_table. Deletes (which
    happen on GC) replace existing keys with a tombstone, which is the only
    type of update which can occur. Tombstones are only cleared out on
    resize.
    
    Unlike st_table, the VALUEs are stored in the hash table itself
    (st_table's bins) rather than as a compact index. This avoids an extra
    pointer dereference and is possible because we don't need to preserve
    insertion order. The table targets a load factor of 2 (it is enlarged
    once it is half full).
    57b6a750
    Lock-free hash set for fstrings [Feature #21268]
    John Hawthorn authored
    This implements a hash set which is wait-free for lookup and lock-free
    for insert (unless resizing) to use for fstring de-duplication.
    
    As highlighted in https://bugs.ruby-lang.org/issues/19288, heavy use of
    fstrings (frozen interned strings) can significantly reduce the
    parallelism of Ractors.
    
    I tried a few other approaches first: using an RWLock, striping a series
    of RWlocks (partitioning the hash N-ways to reduce lock contention), and
    putting a cache in front of it. All of these improved the situation, but
    were unsatisfying as all still required locks for writes (and granular
    locks are awkward, since we run the risk of needing to reach a vm
    barrier) and this table is somewhat write-heavy.
    
    My main reference for this was Cliff Click's talk on a lock free
    hash-table for java https://www.youtube.com/watch?v=HJ-719EGIts. It
    turns out this lock-free hash set is made easier to implement by a few
    properties:
    
     * We only need a hash set rather than a hash table (we only need keys,
       not values), and so the full entry can be written as a single VALUE
     * As a set we only need lookup/insert/delete, no update
     * Delete is only run inside GC so does not need to be atomic (It could
       be made concurrent)
     * I use rb_vm_barrier for the (rare) table rebuilds (It could be made
       concurrent) We VM lock (but don't require other threads to stop) for
       table rebuilds, as those are rare
     * The conservative garbage collector makes deferred replication easy,
       using a T_DATA object
    
    Another benefits of having a table specific to fstrings is that we
    compare by value on lookup/insert, but by identity on delete, as we only
    want to remove the exact string which is being freed. This is faster and
    provides a second way to avoid the race condition in
    https://bugs.ruby-lang.org/issues/21172.
    
    This is a pretty standard open-addressing hash table with quadratic
    probing. Similar to our existing st_table or id_table. Deletes (which
    happen on GC) replace existing keys with a tombstone, which is the only
    type of update which can occur. Tombstones are only cleared out on
    resize.
    
    Unlike st_table, the VALUEs are stored in the hash table itself
    (st_table's bins) rather than as a compact index. This avoids an extra
    pointer dereference and is possible because we don't need to preserve
    insertion order. The table targets a load factor of 2 (it is enlarged
    once it is half full).
Loading