Skip to content

Use a BOP for Hash#default #6945

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Dec 17, 2022
Merged

Conversation

jhawthorn
Copy link
Member

@jhawthorn jhawthorn commented Dec 15, 2022

On a hash miss we need to call default if it is redefined in order to return the default value to be used. Previously we checked this with rb_method_basic_definition_p, which avoids the method call but requires a method lookup.

This commit replaces the previous check with BASIC_OP_UNREDEFINED_P and a new BOP_DEFAULT. We still need to fall back to rb_method_basic_definition_p when called on a subclass of hash.

|                |compare-ruby|built-ruby|
|:---------------|-----------:|---------:|
|hash_aref_miss  |       2.692|     3.531|
|                |           -|     1.31x|

On railsbench, before this change I measured how many times we call rb_method_basic_definition_p and with what IDs. For 5 iterations:

before: 5086076
after: 716854

So this is optimizing ~95% of the checks. Rails does include some subclasses of Hash (ex. HashWithIndifferentAccess), which is why it isn't 100%. default was by far the ID most passed to rb_method_basic_definition_p, second was to_proc with 273898 calls.

@matzbot matzbot requested a review from a team December 15, 2022 23:37
@jhawthorn
Copy link
Member Author

This does seem to improve Railsbench a little bit. There are no real changes to YJIT here (just bindgen updates) but it should improve its performance by the same as the interpreter because it also just calls rb_hash_aref

Before

Average of last 10, non-warmup iters: 1212ms
Total time spent benchmarking: 80s

interp: ruby 3.2.0dev (2022-12-15T17:00:30Z master f50aa19da6) [x86_64-linux]
yjit: ruby 3.2.0dev (2022-12-15T17:00:30Z master f50aa19da6) +YJIT dev_nodebug [x86_64-linux]

----------  -----------  ----------  ---------  ----------  -----------  ------------
bench       interp (ms)  stddev (%)  yjit (ms)  stddev (%)  interp/yjit  yjit 1st itr
railsbench  1798.1       0.7         1212.2     1.6         1.48         1.30
----------  -----------  ----------  ---------  ----------  -----------  ------------
Legend:
- interp/yjit: ratio of interp/yjit time. Higher is better for yjit. Above 1 represents a speedup.
- yjit 1st itr: ratio of interp/yjit time for the first benchmarking iteration.

After

Average of last 10, non-warmup iters: 1208ms
Total time spent benchmarking: 79s

interp: ruby 3.2.0dev (2022-12-15T23:26:12Z hash_bop_default cbb6f121f0) [x86_64-linux]
yjit: ruby 3.2.0dev (2022-12-15T23:26:12Z hash_bop_default cbb6f121f0) +YJIT dev_nodebug [x86_64-linux]

----------  -----------  ----------  ---------  ----------  -----------  ------------
bench       interp (ms)  stddev (%)  yjit (ms)  stddev (%)  interp/yjit  yjit 1st itr
railsbench  1775.4       0.7         1208.9     1.3         1.47         1.30
----------  -----------  ----------  ---------  ----------  -----------  ------------
Legend:
- interp/yjit: ratio of interp/yjit time. Higher is better for yjit. Above 1 represents a speedup.
- yjit 1st itr: ratio of interp/yjit time for the first benchmarking iteration.

@k0kubun
Copy link
Member

k0kubun commented Dec 16, 2022

This does seem to improve Railsbench a little bit

nitpick: It seems more easier to understand the difference this PR is making if you compare before/after between interpreters and YJITs, not interp/yjit. You could do it like:

./run_benchmarks.rb railsbench -e "before::/path/to/old-ruby" -e "after::/path/to/new-ruby"
./run_benchmarks.rb railsbench -e "before::/path/to/old-ruby --yjit" -e "after::/path/to/new-ruby --yjit"

If you intentionally do it that way to avoid having multiple build directories, it's okay too :) But I just wanted to make sure you're aware.

@jhawthorn
Copy link
Member Author

I should do that 😅. When I'm testing just for myself I tend to run the comparison Ruby directly from the source tree, but that has also caused me to make mistakes.

Copy link
Member

@XrXr XrXr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice

jhawthorn and others added 2 commits December 17, 2022 14:26
On a hash miss we need to call default if it is redefined in order to
return the default value to be used. Previously we checked this with
rb_method_basic_definition_p, which avoids the method call but requires
a method lookup.

This commit replaces the previous check with BASIC_OP_UNREDEFINED_P and
a new BOP_DEFAULT. We still need to fall back to
rb_method_basic_definition_p when called on a subclasss of hash.

    |                |compare-ruby|built-ruby|
    |:---------------|-----------:|---------:|
    |hash_aref_miss  |       2.692|     3.531|
    |                |           -|     1.31x|

Co-authored-by: Daniel Colson <danieljamescolson@gmail.com>
Co-authored-by: "Ian C. Anderson" <ian@iancanderson.com>
Co-authored-by: Jack McCracken <me@jackmc.xyz>
We should always have a T_HASH here, so we can use FL_TEST_RAW to avoid
checking whether we may have an immediate value.

I expect this to be a very small performance improvement (perf stat
./miniruby benchmark/hash_aref_miss.rb shows a ~1% improvement). It also
removes 9 instructions from rb_hash_default_value on x86_64.
@jhawthorn jhawthorn merged commit ea3d3c4 into ruby:master Dec 17, 2022
@jhawthorn jhawthorn deleted the hash_bop_default branch December 17, 2022 22:51
matzbot pushed a commit that referenced this pull request Nov 8, 2023
Since #6945 the extension dir changed to Gem::BasicSpecification's implementation, we didn't hook that in rubygems_ext.rb. So for universal rubies, we ended up using the universal platform name when installing, but arch replaced platform name when checking. This lead to native extensions can never be correctly installed on universal rubies.

Hook Gem::BasicSpecifications so the behavior is consistent on installing and checking.

rubygems/rubygems@8d699ed096
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants