Skip to content

Optimize array pattern matching by caching #deconstruct value - #3104

Merged
k-tsj merged 3 commits into
ruby:masterfrom
palkan:experiment/pattern-matching-performance
Jun 27, 2020
Merged

Optimize array pattern matching by caching #deconstruct value#3104
k-tsj merged 3 commits into
ruby:masterfrom
palkan:experiment/pattern-matching-performance

Conversation

@palkan

@palkan palkan commented May 13, 2020

Copy link
Copy Markdown
Contributor

This is follow-up for the discussion started in #2948.

Overview

We can improve array patterns matching performance by keeping the result of the #deconstruct call (or the fact, that the value doesn't respond to it).

The solution proposed in this PR is the following:

  • In compile_case3:
    • Reserve a stack slot for #deconstruct value (with the default value nil — haven't tried to deconstruct)
  • In iseq_compile_pattern_each:
    • Added deconstructed_pos argument which is used to track the distance to the reserved slot (since it could be different depending on the node type).

Benchmark

Compared with the current master (benchmark):

                    first_match
              ./ruby:   1137096.7 i/s 
  /etc/ruby/bin/ruby:   1112537.0 i/s - 1.02x  slower

                     second_match
              ./ruby:    995800.6 i/s 
  /etc/ruby/bin/ruby:    961526.1 i/s - 1.04x  slower

                      third_match
              ./ruby:    895467.2 i/s 
  /etc/ruby/bin/ruby:    822866.8 i/s - 1.09x  slower

                     fourth_match
              ./ruby:    780673.0 i/s 
  /etc/ruby/bin/ruby:    693164.3 i/s - 1.13x  slower

Limitations & future ideas

Currently, only the top-level array patterns are affected. Adding a similar optimization to nested patterns would require additional (and, I think, in most cases useless) stack allocations. Thus, this approach is not optimal, still an ad-hoc optimization.

The proper solution would be to analyze the pattern tree first and generate an optimal execution path, which wouldn't have duplicate structural checks. I plan to work on this too, but that would require much more time than the proposed minor improvement. So, I think this patch is still worth to be considered.

@palkan

palkan commented May 13, 2020

Copy link
Copy Markdown
Contributor Author

@k-tsj Could you please take a look?

@k-tsj

k-tsj commented May 16, 2020

Copy link
Copy Markdown
Member

Great work.

I am positive to merge this pull request.
However, I have been very busy lately, so please give me some time to review.

Could you add some tests?
For example, it seems we need following case.

case [0]
in []
in [1] | [0]
  true 
end

@palkan
palkan force-pushed the experiment/pattern-matching-performance branch from 1c46f5a to 6641f05 Compare May 18, 2020 09:59
@palkan

palkan commented May 18, 2020

Copy link
Copy Markdown
Contributor Author

@k-tsj Added some tests, found some bugs and fixed them 🙂

Unfortunately, the initial idea of including deconstruct instructions only in the first pattern doesn't work well with constant matching, since it's checked first.
So, I rewrote this logic by relying only on the current reserved value on the stack and by adding a couple of new labels to the compiled instructions for branching. The current logic could described as follows:

  • Check reserved value:

    • if it's nil — we haven't tried to deconstruct yet, jump to deconstruct
    • if it's false — the matchee doesn't respond to #deconsturct, jump to match_failed
    • if it's non-falsey — jump to deconstructed.
  • In the deconstruct state:

    • if doesn't respond to #deconstruct — put false on the stack (instead of nil)
    • if does — try to deconstruct and put the result on the stack.

@palkan
palkan force-pushed the experiment/pattern-matching-performance branch 2 times, most recently from 998b0fe to ea253ed Compare May 18, 2020 14:05
@k-tsj

k-tsj commented May 23, 2020

Copy link
Copy Markdown
Member

I found a bug.
Could you check it?

$ ruby -e 'case [[0]]; in [[*a]]; p a;end'
(Expected)
[0]

(Actual)
-e:1:in `<main>': undefined method `length' for 0:Integer (NoMethodError)

@palkan
palkan force-pushed the experiment/pattern-matching-performance branch from ea253ed to a47b1ab Compare May 23, 2020 16:26
@palkan

palkan commented May 23, 2020

Copy link
Copy Markdown
Contributor Author

@k-tsj Fixed (and added a few more tests).

@k-tsj

k-tsj commented Jun 14, 2020

Copy link
Copy Markdown
Member

Could you rebase to master?

@palkan
palkan force-pushed the experiment/pattern-matching-performance branch 2 times, most recently from 9e9373b to bfa5f0b Compare June 14, 2020 19:56
@palkan

palkan commented Jun 14, 2020

Copy link
Copy Markdown
Contributor Author

@k-tsj Rebased and extracted this functionality into iseq_compile_array_deconstruct to re-use in array and find patterns.

@k-tsj

k-tsj commented Jun 20, 2020

Copy link
Copy Markdown
Member

Oops, it seems that changes in 37cd877 cause conflict.
After it's fixed, I'll merge this PR.

@palkan
palkan force-pushed the experiment/pattern-matching-performance branch from bfa5f0b to c9d05f4 Compare June 22, 2020 16:39
@palkan

palkan commented Jun 22, 2020

Copy link
Copy Markdown
Contributor Author

@k-tsj Fixed!

@k-tsj
k-tsj merged commit c9ee34a into ruby:master Jun 27, 2020
@k-tsj

k-tsj commented Jun 27, 2020

Copy link
Copy Markdown
Member

Thanks!
I've merged.

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.

2 participants