From f22b383129939c1f7ee0fcc5522929a130c9bdc7 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Mon, 8 Apr 2024 12:04:01 +0200 Subject: [PATCH] PoC: precompute embeded string literals hash code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 5124f9ac75) [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.. 76efed65bd) [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é --- compile.c | 2 +- internal/string.h | 1 + string.c | 23 +++++++++++++++++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/compile.c b/compile.c index d509957dac2432..c9dcd4859666b6 100644 --- a/compile.c +++ b/compile.c @@ -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); diff --git a/internal/string.h b/internal/string.h index fb37f731147096..00b565293c5624 100644 --- a/internal/string.h +++ b/internal/string.h @@ -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); diff --git a/string.c b/string.c index 040228844da0cb..05bb1ab4109276 100644 --- a/string.c +++ b/string.c @@ -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. @@ -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 @@ -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) { @@ -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)) {