From df35982297ddcc360b2995815f8f8e4ec5049d5f Mon Sep 17 00:00:00 2001 From: "Yukihiro \"Matz\" Matsumoto" Date: Sat, 28 Jun 2025 00:03:55 +0900 Subject: [PATCH] mruby-parray-ext: implement Array#| in C for better performance The C implementation uses hash-based deduplication for large arrays (>16 elements) and linear search for smaller arrays, following the same pattern as other set operations. Co-authored-by: Atlassian Rovo Dev --- mrbgems/mruby-array-ext/mrblib/array.rb | 16 ---- mrbgems/mruby-array-ext/src/array.c | 106 ++++++++++++++++++++++++ mrbgems/mruby-array-ext/test/array.rb | 28 +++++++ 3 files changed, 134 insertions(+), 16 deletions(-) diff --git a/mrbgems/mruby-array-ext/mrblib/array.rb b/mrbgems/mruby-array-ext/mrblib/array.rb index 588ac17a8..fe881367e 100644 --- a/mrbgems/mruby-array-ext/mrblib/array.rb +++ b/mrbgems/mruby-array-ext/mrblib/array.rb @@ -72,22 +72,6 @@ class Array ary end - ## - # call-seq: - # ary | other_ary -> new_ary - # - # Set Union---Returns a new array by joining this array with - # other_ary, removing duplicates. - # - # [ "a", "b", "c" ] | [ "c", "d", "a" ] - # #=> [ "a", "b", "c", "d" ] - # - def |(elem) - raise TypeError, "can't convert #{elem.class} into Array" unless elem.class == Array - - ary = self + elem - ary.uniq! or ary - end ## # call-seq: diff --git a/mrbgems/mruby-array-ext/src/array.c b/mrbgems/mruby-array-ext/src/array.c index 808e4d6be..e03dd7ab4 100644 --- a/mrbgems/mruby-array-ext/src/array.c +++ b/mrbgems/mruby-array-ext/src/array.c @@ -411,6 +411,111 @@ ary_sub(mrb_state *mrb, mrb_value self) return result_ary; } +/* + * call-seq: + * ary | other_ary -> new_ary + * + * Set Union---Returns a new array by joining this array with + * other_ary, removing duplicates. + * + * [ "a", "b", "c" ] | [ "c", "d", "a" ] + * #=> [ "a", "b", "c", "d" ] + */ + +static mrb_value +ary_union(mrb_state *mrb, mrb_value self) +{ + mrb_value other, result_ary; + struct RArray *self_ary, *other_ary; + mrb_value *p, *p_end, *other_p, *other_p_end; + mrb_int total_len; + + mrb_get_args(mrb, "A", &other); + + self_ary = mrb_ary_ptr(self); + other_ary = mrb_ary_ptr(other); + total_len = ARY_LEN(self_ary) + ARY_LEN(other_ary); + + result_ary = mrb_ary_new(mrb); + + if (total_len > SET_OP_HASH_THRESHOLD) { + /* Use hash for large arrays to achieve O(n) performance */ + /* Follow the Ruby pattern: hash[key] = true, then check if hash[key] */ + mrb_value hash = mrb_hash_new_capa(mrb, total_len); + + /* Add elements from self */ + p = ARY_PTR(self_ary); + p_end = p + ARY_LEN(self_ary); + while (p < p_end) { + mrb_value val = mrb_hash_get(mrb, hash, *p); + if (mrb_nil_p(val)) { /* key doesn't exist */ + mrb_hash_set(mrb, hash, *p, mrb_true_value()); + mrb_ary_push(mrb, result_ary, *p); + } + p++; + } + + /* Add elements from other */ + other_p = ARY_PTR(other_ary); + other_p_end = other_p + ARY_LEN(other_ary); + while (other_p < other_p_end) { + mrb_value val = mrb_hash_get(mrb, hash, *other_p); + if (mrb_nil_p(val)) { /* key doesn't exist */ + mrb_hash_set(mrb, hash, *other_p, mrb_true_value()); + mrb_ary_push(mrb, result_ary, *other_p); + } + other_p++; + } + } + else { + /* Use linear search for small arrays */ + + /* Add elements from self */ + p = ARY_PTR(self_ary); + p_end = p + ARY_LEN(self_ary); + while (p < p_end) { + mrb_int result_len = RARRAY_LEN(result_ary); + mrb_value *result_ptr = ARY_PTR(RARRAY(result_ary)); + mrb_bool found = FALSE; + + for (mrb_int i = 0; i < result_len; i++) { + if (mrb_equal(mrb, *p, result_ptr[i])) { + found = TRUE; + break; + } + } + + if (!found) { + mrb_ary_push(mrb, result_ary, *p); + } + p++; + } + + /* Add elements from other */ + other_p = ARY_PTR(other_ary); + other_p_end = other_p + ARY_LEN(other_ary); + while (other_p < other_p_end) { + mrb_int result_len = RARRAY_LEN(result_ary); + mrb_value *result_ptr = ARY_PTR(RARRAY(result_ary)); + mrb_bool found = FALSE; + + for (mrb_int i = 0; i < result_len; i++) { + if (mrb_equal(mrb, *other_p, result_ptr[i])) { + found = TRUE; + break; + } + } + + if (!found) { + mrb_ary_push(mrb, result_ary, *other_p); + } + other_p++; + } + } + + return result_ary; +} + void mrb_mruby_array_ext_gem_init(mrb_state* mrb) { @@ -426,6 +531,7 @@ mrb_mruby_array_ext_gem_init(mrb_state* mrb) mrb_define_method_id(mrb, a, MRB_SYM(rotate), ary_rotate, MRB_ARGS_OPT(1)); mrb_define_method_id(mrb, a, MRB_SYM_B(rotate), ary_rotate_bang, MRB_ARGS_OPT(1)); mrb_define_method_id(mrb, a, MRB_OPSYM(sub), ary_sub, MRB_ARGS_REQ(1)); + mrb_define_method_id(mrb, a, MRB_OPSYM(or), ary_union, MRB_ARGS_REQ(1)); } void diff --git a/mrbgems/mruby-array-ext/test/array.rb b/mrbgems/mruby-array-ext/test/array.rb index f19b90278..706b55712 100644 --- a/mrbgems/mruby-array-ext/test/array.rb +++ b/mrbgems/mruby-array-ext/test/array.rb @@ -139,6 +139,34 @@ assert("Array#|") do assert_equal [1, 2, 3, 1], a end +assert("Array#| with large arrays") do + # Test hash-based implementation (total length > 16) + a = (1..20).to_a + b = (18..50).to_a + result = a | b + expected = (1..50).to_a + + assert_equal expected, result + assert_equal 50, result.size + + # Test with overlapping ranges + a = (1..15).to_a + b = (10..25).to_a + result = a | b + expected = (1..25).to_a + + assert_equal expected, result + assert_equal 25, result.size + + # Ensure original arrays are unchanged + original_a = (1..20).to_a + original_b = (18..50).to_a + result = original_a | original_b + assert_equal (1..50).to_a, result + assert_equal (1..20).to_a, original_a + assert_equal (18..50).to_a, original_b +end + assert("Array#union") do a = [1, 2, 3, 1] b = [1, 4]