mruby-array-ext: implement Array#intersect? in C for better performance

Moved Array#intersect? implementation from Ruby to C to improve memory
usage and performance with early termination optimization. The C
implementation uses hash-based lookup for large arrays (>16 elements)
and linear search for smaller arrays.

Added comprehensive test coverage including early termination scenarios,
empty arrays, size optimization verification, and edge cases with
duplicates and large arrays.

Co-authored-by: Atlassian Rovo Dev
This commit is contained in:
Yukihiro "Matz" Matsumoto
2025-06-28 01:40:25 +09:00
parent ea5de2b8da
commit da813e0486
3 changed files with 144 additions and 40 deletions
-40
View File
@@ -111,46 +111,6 @@ class Array
ary
end
##
# call-seq:
# ary.intersect?(other_ary) -> true or false
#
# Returns +true+ if the array and +other_ary+ have at least one element in
# common, otherwise returns +false+.
#
# a = [ 1, 2, 3 ]
# b = [ 3, 4, 5 ]
# c = [ 5, 6, 7 ]
# a.intersect?(b) #=> true
# a.intersect?(c) #=> false
def intersect?(ary)
raise TypeError, "cannot convert #{ary.class} into Array" unless ary.class == Array
hash = {}
if self.length > ary.length
shorter = ary
longer = self
else
shorter = self
longer = ary
end
idx = 0
len = shorter.size
while idx < len
hash[shorter[idx]] = true
idx += 1
end
idx = 0
len = size
while idx < len
v = longer[idx]
if hash[v]
return true
end
idx += 1
end
false
end
##
# call-seq:
+87
View File
@@ -606,6 +606,92 @@ ary_intersection(mrb_state *mrb, mrb_value self)
return result_ary;
}
/*
* call-seq:
* ary.intersect?(other_ary) -> true or false
*
* Returns +true+ if the array and +other_ary+ have at least one element in
* common, otherwise returns +false+.
*
* a = [ 1, 2, 3 ]
* b = [ 3, 4, 5 ]
* c = [ 5, 6, 7 ]
* a.intersect?(b) #=> true
* a.intersect?(c) #=> false
*/
static mrb_value
ary_intersect_p(mrb_state *mrb, mrb_value self)
{
mrb_value other;
struct RArray *self_ary, *other_ary, *shorter_ary, *longer_ary;
mrb_value *shorter_p, *shorter_p_end, *longer_p, *longer_p_end;
mrb_get_args(mrb, "A", &other);
self_ary = mrb_ary_ptr(self);
other_ary = mrb_ary_ptr(other);
/* Choose shorter array for hash, longer for iteration (optimization) */
if (ARY_LEN(self_ary) > ARY_LEN(other_ary)) {
shorter_ary = other_ary;
longer_ary = self_ary;
}
else {
shorter_ary = self_ary;
longer_ary = other_ary;
}
/* Early termination for empty arrays */
if (ARY_LEN(shorter_ary) == 0 || ARY_LEN(longer_ary) == 0) {
return mrb_false_value();
}
if (ARY_LEN(shorter_ary) > SET_OP_HASH_THRESHOLD) {
/* Use hash for large arrays to achieve O(n) performance */
mrb_value hash = mrb_hash_new_capa(mrb, ARY_LEN(shorter_ary));
/* Populate hash with elements from shorter array */
shorter_p = ARY_PTR(shorter_ary);
shorter_p_end = shorter_p + ARY_LEN(shorter_ary);
while (shorter_p < shorter_p_end) {
mrb_hash_set(mrb, hash, *shorter_p, mrb_true_value());
shorter_p++;
}
/* Check elements from longer array against hash with early termination */
longer_p = ARY_PTR(longer_ary);
longer_p_end = longer_p + ARY_LEN(longer_ary);
while (longer_p < longer_p_end) {
mrb_value val = mrb_hash_get(mrb, hash, *longer_p);
if (!mrb_nil_p(val)) { /* key exists in shorter array */
return mrb_true_value(); /* Early termination */
}
longer_p++;
}
}
else {
/* Use linear search for small arrays */
longer_p = ARY_PTR(longer_ary);
longer_p_end = longer_p + ARY_LEN(longer_ary);
while (longer_p < longer_p_end) {
/* Check if element exists in shorter array */
shorter_p = ARY_PTR(shorter_ary);
shorter_p_end = shorter_p + ARY_LEN(shorter_ary);
while (shorter_p < shorter_p_end) {
if (mrb_equal(mrb, *longer_p, *shorter_p)) {
return mrb_true_value(); /* Early termination */
}
shorter_p++;
}
longer_p++;
}
}
return mrb_false_value();
}
void
mrb_mruby_array_ext_gem_init(mrb_state* mrb)
{
@@ -623,6 +709,7 @@ mrb_mruby_array_ext_gem_init(mrb_state* mrb)
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));
mrb_define_method_id(mrb, a, MRB_OPSYM(and), ary_intersection, MRB_ARGS_REQ(1));
mrb_define_method_id(mrb, a, MRB_SYM_Q(intersect), ary_intersect_p, MRB_ARGS_REQ(1));
}
void
+57
View File
@@ -264,6 +264,63 @@ assert("Array#intersect?") do
assert_false(a.intersect?(c))
end
assert("Array#intersect? with large arrays") do
# Test hash-based implementation (shorter array > 16)
a = (1..30).to_a
b = (25..50).to_a # 26 elements > 16, but a is longer so b is shorter
result = a.intersect?(b)
assert_true(result) # should find intersection at 25-30
# Test with larger arrays, no intersection
a = (1..20).to_a
b = (30..50).to_a # 21 elements > 16, triggers hash approach
result = a.intersect?(b)
assert_false(result) # no intersection
# Test with first element matching (early termination)
a = (1..30).to_a
b = [1] + (50..70).to_a # 22 elements > 16, first element matches
result = a.intersect?(b)
assert_true(result) # should terminate early on first element
# Test with last element matching
a = (1..30).to_a
b = (50..70).to_a + [30] # 22 elements > 16, last element matches
result = a.intersect?(b)
assert_true(result) # should find match at the end
# Test empty arrays
a = []
b = (1..20).to_a
result = a.intersect?(b)
assert_false(result) # empty array intersects with nothing
a = (1..20).to_a
b = []
result = a.intersect?(b)
assert_false(result) # intersecting with empty array
# Test array size optimization (shorter array used for hash)
a = (1..5).to_a # shorter
b = (3..30).to_a # longer, 28 elements > 16
result = a.intersect?(b)
assert_true(result) # should use a (shorter) for hash, find 3,4,5
# Test with duplicates
a = [1, 1, 2, 2, 3, 3] * 5 # 30 elements with duplicates
b = (25..50).to_a # 26 elements > 16, no intersection
result = a.intersect?(b)
assert_false(result)
# Ensure original arrays are unchanged
original_a = (1..30).to_a
original_b = (25..50).to_a
result = original_a.intersect?(original_b)
assert_true(result)
assert_equal (1..30).to_a, original_a
assert_equal (25..50).to_a, original_b
end
assert("Array#flatten") do
assert_equal [1, 2, "3", {4=>5}, :'6'], [1, 2, "3", {4=>5}, :'6'].flatten
assert_equal [1, 2, 3, 4, 5, 6], [1, 2, [3, 4, 5], 6].flatten