Rewrite Array#each in Ruby using Primitive (#9533)

This commit is contained in:
Takashi Kokubun 2024-01-23 12:09:57 -08:00 committed by GitHub
parent 27c1dd8634
commit c84237f953
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 82 additions and 50 deletions

59
array.c
View File

@ -28,6 +28,7 @@
#include "ruby/encoding.h"
#include "ruby/st.h"
#include "ruby/util.h"
#include "vm_core.h"
#include "builtin.h"
#if !ARRAY_DEBUG
@ -2484,50 +2485,19 @@ ary_enum_length(VALUE ary, VALUE args, VALUE eobj)
return rb_ary_length(ary);
}
/*
* call-seq:
* array.each {|element| ... } -> self
* array.each -> Enumerator
*
* Iterates over array elements.
*
* When a block given, passes each successive array element to the block;
* returns +self+:
*
* a = [:foo, 'bar', 2]
* a.each {|element| puts "#{element.class} #{element}" }
*
* Output:
*
* Symbol foo
* String bar
* Integer 2
*
* Allows the array to be modified during iteration:
*
* a = [:foo, 'bar', 2]
* a.each {|element| puts element; a.clear if element.to_s.start_with?('b') }
*
* Output:
*
* foo
* bar
*
* When no block given, returns a new Enumerator:
* a = [:foo, 'bar', 2]
*
* e = a.each
* e # => #<Enumerator: [:foo, "bar", 2]:each>
* a1 = e.each {|element| puts "#{element.class} #{element}" }
*
* Output:
*
* Symbol foo
* String bar
* Integer 2
*
* Related: #each_index, #reverse_each.
*/
// Primitive to avoid a race condition in Array#each.
// Return `true` and write `value` and `index` if the element exists.
static VALUE
ary_fetch_next(VALUE self, VALUE *index, VALUE *value)
{
long i = NUM2LONG(*index);
if (i >= RARRAY_LEN(self)) {
return Qfalse;
}
*value = RARRAY_AREF(self, i);
*index = LONG2NUM(i + 1);
return Qtrue;
}
VALUE
rb_ary_each(VALUE ary)
@ -8644,7 +8614,6 @@ Init_Array(void)
rb_define_method(rb_cArray, "unshift", rb_ary_unshift_m, -1);
rb_define_alias(rb_cArray, "prepend", "unshift");
rb_define_method(rb_cArray, "insert", rb_ary_insert, -1);
rb_define_method(rb_cArray, "each", rb_ary_each, 0);
rb_define_method(rb_cArray, "each_index", rb_ary_each_index, 0);
rb_define_method(rb_cArray, "reverse_each", rb_ary_reverse_each, 0);
rb_define_method(rb_cArray, "length", rb_ary_length, 0);

View File

@ -1,4 +1,59 @@
class Array
# call-seq:
# array.each {|element| ... } -> self
# array.each -> Enumerator
#
# Iterates over array elements.
#
# When a block given, passes each successive array element to the block;
# returns +self+:
#
# a = [:foo, 'bar', 2]
# a.each {|element| puts "#{element.class} #{element}" }
#
# Output:
#
# Symbol foo
# String bar
# Integer 2
#
# Allows the array to be modified during iteration:
#
# a = [:foo, 'bar', 2]
# a.each {|element| puts element; a.clear if element.to_s.start_with?('b') }
#
# Output:
#
# foo
# bar
#
# When no block given, returns a new Enumerator:
# a = [:foo, 'bar', 2]
#
# e = a.each
# e # => #<Enumerator: [:foo, "bar", 2]:each>
# a1 = e.each {|element| puts "#{element.class} #{element}" }
#
# Output:
#
# Symbol foo
# String bar
# Integer 2
#
# Related: #each_index, #reverse_each.
def each
Primitive.attr! :inline_block
unless defined?(yield)
return to_enum(:each) { self.length }
end
_i = 0
value = nil
while Primitive.cexpr!(%q{ ary_fetch_next(self, LOCAL_PTR(_i), LOCAL_PTR(value)) })
yield value
end
self
end
# call-seq:
# array.shuffle!(random: Random) -> array
#

4
benchmark/loop_each.yml Normal file
View File

@ -0,0 +1,4 @@
prelude: |
arr = [nil] * 30_000_000
benchmark:
loop_each: arr.each{|e|}

View File

@ -106,6 +106,8 @@ rb_vm_lvar(rb_execution_context_t *ec, int index)
#endif
}
#define LOCAL_PTR(local) local ## __ptr
// dump/load
struct builtin_binary {

View File

@ -19,7 +19,7 @@ matrix 0.4.2 https://github.com/ruby/matrix
prime 0.1.2 https://github.com/ruby/prime
rbs 3.4.2 https://github.com/ruby/rbs
typeprof 0.21.9 https://github.com/ruby/typeprof
debug 1.9.1 https://github.com/ruby/debug
debug 1.9.1 https://github.com/ruby/debug 19b91b14ce814a0eb615abb8d2bef0594c61c5c8
racc 1.7.3 https://github.com/ruby/racc
mutex_m 0.2.0 https://github.com/ruby/mutex_m
getoptlong 0.2.1 https://github.com/ruby/getoptlong

View File

@ -2841,6 +2841,7 @@ EOS
def test_low_memory_startup
omit "JIT enabled" if %w[YJIT RJIT].any? {|n| RubyVM.const_defined?(n) and RubyVM.const_get(n).enabled?}
omit "flaky on Travis arm32" if /armv8l-linux-eabihf/ =~ RUBY_PLATFORM
as = 1<<25
_, _, status = EnvUtil.invoke_ruby(%W'-W0', "", true, :merge_to_stdout, rlimit_as: as)
omit sprintf("Crashed with AS: %#x: %s", as, status) if status.signaled?

View File

@ -504,7 +504,7 @@ class TestSetTraceFunc < Test::Unit::TestCase
1: trace = TracePoint.trace(*trace_events){|tp| next if !target_thread?
2: events << [tp.event, tp.lineno, tp.path, _defined_class.(tp), tp.method_id, tp.self, tp.binding&.eval("_local_var"), _get_data.(tp)] if tp.path == 'xyzzy'
3: }
4: [1].each{|;_local_var| _local_var = :inner
4: [1].reverse_each{|;_local_var| _local_var = :inner
5: tap{}
6: }
7: class XYZZY
@ -531,10 +531,10 @@ class TestSetTraceFunc < Test::Unit::TestCase
answer_events = [
#
[:line, 4, 'xyzzy', self.class, method, self, :outer, :nothing],
[:c_call, 4, 'xyzzy', Array, :each, [1], nil, :nothing],
[:c_call, 4, 'xyzzy', Array, :reverse_each, [1], nil, :nothing],
[:line, 4, 'xyzzy', self.class, method, self, nil, :nothing],
[:line, 5, 'xyzzy', self.class, method, self, :inner, :nothing],
[:c_return, 4, "xyzzy", Array, :each, [1], nil, [1]],
[:c_return, 4, "xyzzy", Array, :reverse_each, [1], nil, [1]],
[:line, 7, 'xyzzy', self.class, method, self, :outer, :nothing],
[:c_call, 7, "xyzzy", Module, :const_added, TestSetTraceFunc, nil, :nothing],
[:c_return, 7, "xyzzy", Module, :const_added, TestSetTraceFunc, nil, nil],

View File

@ -274,7 +274,8 @@ def generate_cexpr(ofile, lineno, line_file, body_lineno, text, locals, func_nam
locals&.reverse_each&.with_index{|param, i|
next unless Symbol === param
next unless local_candidates.include?(param.to_s)
f.puts "MAYBE_UNUSED(const VALUE) #{param} = rb_vm_lvar(ec, #{-3 - i});"
f.puts "VALUE *const #{param}__ptr = (VALUE *)&ec->cfp->ep[#{-3 - i}];"
f.puts "MAYBE_UNUSED(const VALUE) #{param} = *#{param}__ptr;"
lineno += 1
}
f.puts "#line #{body_lineno} \"#{line_file}\""