[ruby/prism] Optimize ruby visitor

`compact_child_nodes` allocates an array. We can skip that step by simply yielding the nodes.

Benchmark for visiting the rails codebase:

```rb
require "prism"
require "benchmark/ips"

files = Dir.glob("../rails/**/*.rb")
results = files.map { Prism.parse_file(it) }
visitor = Prism::Visitor.new

Benchmark.ips do |x|
  x.config(warmup: 3, time: 10)

  x.report do
    results.each do
      visitor.visit(it.value)
    end
  end
end

RubyVM::YJIT.enable

Benchmark.ips do |x|
  x.config(warmup: 3, time: 10)

  x.report do
    results.each do
      visitor.visit(it.value)
    end
  end
end
```

Before:
```
ruby 3.4.8 (2025-12-17 revision https://github.com/ruby/prism/commit/995b59f666) +PRISM [x86_64-linux]
Warming up --------------------------------------
                         1.000 i/100ms
Calculating -------------------------------------
                          2.691 (± 0.0%) i/s  (371.55 ms/i) -     27.000 in  10.089422s
ruby 3.4.8 (2025-12-17 revision https://github.com/ruby/prism/commit/995b59f666) +YJIT +PRISM [x86_64-linux]
Warming up --------------------------------------
                         1.000 i/100ms
Calculating -------------------------------------
                          7.278 (±13.7%) i/s  (137.39 ms/i) -     70.000 in  10.071568s
```
After:
```
ruby 3.4.8 (2025-12-17 revision https://github.com/ruby/prism/commit/995b59f666) +PRISM [x86_64-linux]
Warming up --------------------------------------
                         1.000 i/100ms
Calculating -------------------------------------
                          3.429 (± 0.0%) i/s  (291.65 ms/i) -     35.000 in  10.208580s
ruby 3.4.8 (2025-12-17 revision https://github.com/ruby/prism/commit/995b59f666) +YJIT +PRISM [x86_64-linux]
Warming up --------------------------------------
                         1.000 i/100ms
Calculating -------------------------------------
                         16.815 (± 0.0%) i/s   (59.47 ms/i) -    169.000 in  10.054668s
```

~21% faster on the interpreter, ~56% with YJIT

https://github.com/ruby/prism/commit/bf631750cf
This commit is contained in:
Earlopain 2025-12-23 14:41:09 +01:00 committed by git
parent 14fbcf0e6e
commit 65634d8df5
4 changed files with 29 additions and 6 deletions

View File

@ -1422,7 +1422,7 @@ module Prism
# ```
def visit_parameters_node(node)
children =
node.compact_child_nodes.map do |element|
node.each_child_node.map do |element|
if element.is_a?(MultiTargetNode)
visit_destructured_parameter(element)
else

View File

@ -29,14 +29,14 @@ module Prism
# Visit the child nodes of the given node.
def visit_child_nodes(node)
node.compact_child_nodes.map { |node| node.accept(self) }
node.each_child_node.map { |node| node.accept(self) }
end
<%- nodes.each_with_index do |node, index| -%>
<%= "\n" if index != 0 -%>
# Compile a <%= node.name %> node
def visit_<%= node.human %>(node)
node.compact_child_nodes.map { |node| node.accept(self) }
node.each_child_node.map { |node| node.accept(self) }
end
<%- end -%>
end

View File

@ -187,7 +187,7 @@ module Prism
while (node = queue.shift)
result << node
node.compact_child_nodes.each do |child_node|
node.each_child_node do |child_node|
child_location = child_node.location
start_line = child_location.start_line
@ -259,6 +259,13 @@ module Prism
alias deconstruct child_nodes
# With a block given, yields each child node. Without a block, returns
# an enumerator that contains each child node. Excludes any `nil`s in
# the place of optional nodes that were not present.
def each_child_node
raise NoMethodError, "undefined method `each_child_node' for #{inspect}"
end
# Returns an array of child nodes, excluding any `nil`s in the place of
# optional nodes that were not present.
def compact_child_nodes
@ -335,6 +342,22 @@ module Prism
}.compact.join(", ") %>]
end
# def each_child_node: () { (Prism::node) -> void } -> void | () -> Enumerator[Prism::node]
def each_child_node
return to_enum(:each_child_node) unless block_given?
<%- node.fields.each do |field| -%>
<%- case field -%>
<%- when Prism::Template::NodeField -%>
yield <%= field.name %>
<%- when Prism::Template::OptionalNodeField -%>
yield <%= field.name %> if <%= field.name %>
<%- when Prism::Template::NodeListField -%>
<%= field.name %>.each {|node| yield node }
<%- end -%>
<%- end -%>
end
# def compact_child_nodes: () -> Array[Node]
def compact_child_nodes
<%- if node.fields.any? { |field| field.is_a?(Prism::Template::OptionalNodeField) } -%>

View File

@ -20,7 +20,7 @@ module Prism
# Visits the child nodes of `node` by calling `accept` on each one.
def visit_child_nodes(node)
# @type self: _Visitor
node.compact_child_nodes.each { |node| node.accept(self) }
node.each_child_node { |node| node.accept(self) }
end
end
@ -48,7 +48,7 @@ module Prism
<%= "\n" if index != 0 -%>
# Visit a <%= node.name %> node
def visit_<%= node.human %>(node)
node.compact_child_nodes.each { |node| node.accept(self) }
node.each_child_node { |node| node.accept(self) }
end
<%- end -%>
end