[ccan] htable license change request for Ruby
Eric Wong
normalperson at yhbt.net
Wed May 21 18:32:06 EST 2014
Rusty Russell <rusty at rustcorp.com.au> wrote:
> Eric Wong <normalperson at yhbt.net> writes:
> > Hello Rusty! I am interested in using htable with mainline Ruby.
> > Ruby is 2-clause BSD-licensed nowadays[1].
> >
> > My preliminary switch of one big st_table to htable beat out my proposed
> > container_of-based hash implementation (ihash) in memory usage:
> >
> > ihash: ~100K reduction
> > htable: ~150K reduction
> > https://bugs.ruby-lang.org/issues/9841#change-46823
>
> Do you have performance measurements? It's easy to make a smaller hash
> table, by filling it more :)
Thanks for the response!
I haven't looked too hard at performance, yet. Most of the paths I'm
modifying are not extremely speed-critical and not easy to measure for
speed. However, reducing overall allocations (and fragmentation) should
help speed in some cases.
The current hash table, st, is order-preserving(!) and requires a
48-byte allocation for every entry in "unpacked" tables with more
than 5 entries.
My proposed ihash implementation is chained using a singly linked-list
via container_of, so this adds 8-byte overhead to some elements and
requires no extra malloc in the Ruby method/constant tables.
st and ihash only double in size when load factor is 5(!) or more. I'm
new to htable, but it looks like htable cannot have a load factor beyond
one, so htable even uses more space for the table itself, but introduces
no chaining pointer overhead for each element.
> The other considerations with htable:
> - It grows atomically, which can suck if you're after RT response.
> This one is possible to fix, but tradeoffs abound. You may also have
> simply hit a sweet spot for htable: do you have reliable memory
> reports for a range of uses?
st and ihash have the same growth behavior. I just got a rough idea of
the savings based on using time(1). However, I know the data structures
of Ruby and the numbers of what I'm changing (see below).
> - It doesn't shrink.
Also true of st and ihash. Not an issue as I'm using it for tables that
are mostly mostly static and long-lived.
> - Assuming you use a nicely randomly seeded universal hash function :)
Yep, we're currently on SipHash-2-4 for strings. This was designed to
be hash well even when given malicious inputs.
For ID/symbol lookups, I already needed to do some tuning of the hash
functions to get acceptable performance with st when we switched to
power-of-two sizing.
> > I noticed in commit d4941bf8047d16007f19a3b5b2211e1e7571f068
> > (htable: relicense under LGPL), there is a chance of relicensing
> > htable under public domain or BSD:
> >
> > So this is the simplest fix. I might relicense under PD or BSD later,
> > since the likely module should probably have an even more liberal
> > license.
> >
> > Therefore I kindly request the htable license be changed to a permissive
> > license so it may be considered for use with Ruby.
>
> Sure, but let's make sure it's actually a good idea first :)
>
> Thanks!
> Rusty.
I'll work more on getting parts converted to htable and performance
measurements soonish. However, I know some numbers off the top of my
head. (All my numbers here are based on 64-bit systems).
Here's a table of per-entry overheads in Ruby with the various
hash table implementations:
table \ impl.| st.unpacked | st.packed | ihash | htable
-------------+--------------+------------+-------------+-----------
fstring | malloc(48) | never | malloc(24) | 0
casefold | malloc(48) | never | 8 | 0
^=^=^=^=^=^=^=^=^= the above are order agnostic =^=^=^=^=^=^=^=^=^=
=v=v=v=v=v=v=v=v= the below may require ordering =v=v=v=v=v=v=v=v=v
symbols | malloc(48)*2 | never | malloc(40) | malloc(16)
methods | malloc(48) | irrelevant | 8 | 0
constants | malloc(48) | irrelevant | 8 | 0
Notes:
* malloc(X) denotes additional malloc call + allocation size required,
this does not account for any internal overheads of malloc.
* bare numbers mean the overhead is added to an existing allocated
struct with no additional calls to malloc
* I've only implemented fstring for htable so far
* The fstring table I converted is around 1000 entries at minimum,
and possibly >10K on some larger code bases.
* st.packed only applies to st tables with <=5 entries, where each entry
is packed with 24-bytes/entry overhead into the core hash table
itself. This only affects method/constant tables, as the bigger
tables do not get packed.
* symbols have bidirectional mapping, so there are two tables for
symbols: id=>sym and sym=>id
ihash and htable may share the same allocated space for both tables.
* fstring and symbols are typically >=3K entries
* casefold tables are static, the big ones add up to ~2.5K entries
* I am unsure of the order-preservation requirements for symbols,
methods and constants.
If order-preservation is required, ihash and htable require an
additional 16 bytes per-entry for a struct list_node.
(ref: http://bugs.ruby-lang.org/issues/9614)
* method and constant tables always has per-entry allocation overhead
for each entry, thus ihash may embed its chaining node into that
allocation without additional malloc calls. Thus st.packed is
not relevant to method or constant tables.
* There are several method and constant tables (typically tens of them,
maybe hundreds). Some of these tables may have zero entries, some may
have <=5 (where they may be packed if st), but some may have tens of
entries (but typically not hundreds).
* st.packed probably benefits instance/class variable tables right now,
but I haven't checked those very closely.
More information about the ccan
mailing list