Skip to content

Commit

Permalink
PoC: precompute embeded string literals hash code
Browse files Browse the repository at this point in the history
Unclear how much worth it, it would be, but with embeded
strings we often have some space left in the slot, which
we can use to store the string Hash code.

It's probably only worth it for string literals, as they
are the ones likely to be used as hash keys.

We could even ony do this for string literal actually
used as hash keys, as seen by the compiler.

It is also unclear what the best layout would be. We
currently store it right after the string terminator
as to make it easy/fast to compute, but ideally it would
be at a fixed position, however this means creating another
`RString` union, which would complexify the code base significantly.

It's also unclear to us whether trying to respect alignment would
lead to a performance gain or not.

```ruby
hash = 10.times.to_h do |i|
  [i, i]
end

dyn_sym = "dynamic_symbol".to_sym
hash[:some_symbol] = 1
hash[dyn_sym] = 1
hash["small"] = 2
hash["frozen_string_literal"] = 2

Benchmark.ips do |x|
  x.report("symbol") { hash[:some_symbol] }
  x.report("dyn_symbol") { hash[:some_symbol] }
  x.report("small_lit") { hash["small"] }
  x.report("frozen_lit") { hash["frozen_string_literal"] }
  x.compare!(order: :baseline)
end
```

Before:

```
ruby 3.3.0 (2023-12-25 revision 5124f9a) [arm64-darwin23]
Warming up --------------------------------------
              symbol     2.392M i/100ms
          dyn_symbol     2.440M i/100ms
           small_lit     2.155M i/100ms
          frozen_lit     2.010M i/100ms
Calculating -------------------------------------
              symbol     24.175M (± 1.7%) i/s -    122.002M in   5.048306s
          dyn_symbol     24.345M (± 1.6%) i/s -    122.019M in   5.013400s
           small_lit     21.252M (± 2.1%) i/s -    107.744M in   5.072042s
          frozen_lit     20.095M (± 1.3%) i/s -    100.489M in   5.001681s

Comparison:
              symbol: 24174848.1 i/s
          dyn_symbol: 24345476.9 i/s - same-ish: difference falls within error
           small_lit: 21252403.2 i/s - 1.14x  slower
          frozen_lit: 20094766.0 i/s - 1.20x  slower
```

After:

```
ruby 3.4.0dev (2024-04-08T07:20:15Z interned-string-ha.. 76efed6) [arm64-darwin23]
Warming up --------------------------------------
              symbol     2.400M i/100ms
          dyn_symbol     2.405M i/100ms
           small_lit     2.308M i/100ms
          frozen_lit     2.268M i/100ms
Calculating -------------------------------------
              symbol     23.528M (± 6.9%) i/s -    117.584M in   5.033231s
          dyn_symbol     23.777M (± 4.7%) i/s -    120.231M in   5.071734s
           small_lit     23.066M (± 2.9%) i/s -    115.376M in   5.006947s
          frozen_lit     22.729M (± 1.1%) i/s -    115.693M in   5.090700s

Comparison:
              symbol: 23527823.6 i/s
          dyn_symbol: 23776757.8 i/s - same-ish: difference falls within error
           small_lit: 23065535.3 i/s - same-ish: difference falls within error
          frozen_lit: 22729351.6 i/s - same-ish: difference falls within error
```

Co-Authored-By: Étienne Barrié <etienne.barrie@gmail.com>
  • Loading branch information
byroot and etiennebarrie committed Apr 8, 2024
1 parent 00cbdb5 commit f22b383
Show file tree
Hide file tree
Showing 3 changed files with 25 additions and 1 deletion.
2 changes: 1 addition & 1 deletion compile.c
Original file line number Diff line number Diff line change
Expand Up @@ -10593,7 +10593,7 @@ iseq_compile_each0(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const no
lit = rb_str_freeze(lit);
}
else {
lit = rb_fstring(lit);
lit = rb_fstring(rb_str_precompute_hash(rb_str_freeze(lit)));
}
ADD_INSN1(ret, node, putobject, lit);
RB_OBJ_WRITTEN(iseq, Qundef, lit);
Expand Down
1 change: 1 addition & 0 deletions internal/string.h
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ VALUE rb_str_concat_literals(size_t num, const VALUE *strary);
VALUE rb_str_eql(VALUE str1, VALUE str2);
VALUE rb_id_quote_unprintable(ID);
VALUE rb_sym_proc_call(ID mid, int argc, const VALUE *argv, int kw_splat, VALUE passed_proc);
VALUE rb_str_precompute_hash(VALUE);

struct rb_execution_context_struct;
VALUE rb_ec_str_resurrect(struct rb_execution_context_struct *ec, VALUE str, bool chilled);
Expand Down
23 changes: 23 additions & 0 deletions string.c
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ VALUE rb_cSymbol;
* another string (the shared root).
* 3: STR_CHILLED (will be frozen in a future version)
* The string appears frozen but can be mutated with a warning.
* 4: STR_PRECOMPUTED_HASH
* TODO: doc
* 5: STR_SHARED_ROOT
* Other strings may point to the contents of this string. When this
* flag is set, STR_SHARED must not be set.
Expand Down Expand Up @@ -116,6 +118,7 @@ VALUE rb_cSymbol;
*/

#define RUBY_MAX_CHAR_LEN 16
#define STR_PRECOMPUTED_HASH FL_USER4
#define STR_SHARED_ROOT FL_USER5
#define STR_BORROWED FL_USER6
#define STR_TMPLOCK FL_USER7
Expand Down Expand Up @@ -401,6 +404,22 @@ fstr_update_callback(st_data_t *key, st_data_t *value, st_data_t data, int exist
}
}

VALUE
rb_str_precompute_hash(VALUE str)
{
if (!STR_EMBED_P(str)) {
return str;
}

size_t used_bytes = (RSTRING_LEN(str) + TERM_LEN(str));
size_t free_bytes = str_embed_capa(str) - used_bytes;
if (free_bytes >= sizeof(st_index_t)) {
*(st_index_t *)(RSTRING_PTR(str) + used_bytes) = rb_str_hash(str);
FL_SET(str, STR_PRECOMPUTED_HASH);
}
return str;
}

VALUE
rb_fstring(VALUE str)
{
Expand Down Expand Up @@ -3655,6 +3674,10 @@ rb_str_prepend_multi(int argc, VALUE *argv, VALUE str)
st_index_t
rb_str_hash(VALUE str)
{
if (FL_TEST_RAW(str, STR_PRECOMPUTED_HASH)) {
return *(st_index_t *)(RSTRING_END(str) + TERM_LEN(str));
}

st_index_t h = rb_memhash((const void *)RSTRING_PTR(str), RSTRING_LEN(str));
int e = RSTRING_LEN(str) ? ENCODING_GET(str) : 0;
if (e && !is_ascii_string(str)) {
Expand Down

0 comments on commit f22b383

Please sign in to comment.