Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Ruby Performance: Chaining selects vs AND-ing predicates?

If I want to select all elements of an array arr satisfying both predicates p_1 and p_2, then I have two options for implementation:

Option 1:

arr.select{|x| x.p_1}.select{|x| x.p_2}

Option 2:

arr.select{|x| x.p_1 && x.p_2}

Is there a significant difference between the two? In my use case the predicate p_1 reduces the list much more than p_2, and p_2 is more expensive than p_1. So I suspect putting p_1 before p_2 makes it quicker. But do either of the options above make a difference?

like image 321
preferred_anon Avatar asked Aug 15 '26 10:08

preferred_anon


1 Answers

It looks like you're already aware of both the performance characteristics your predicates and the shape of your data, which is great!

Is there a difference? Simply put, yes -- the evaluation order is different:

# Option 1
arr[0].p_1
arr[1].p_1
arr[2].p_1
...
arr[n].p_1
arr[0].p_2
arr[1].p_2
arr[2].p_2
...
arr[n].p_2

versus

# Option 2
arr[0].p_1
arr[0].p_2
arr[1].p_1
arr[1].p_2
arr[2].p_1
arr[2].p_2
...
arr[n].p_1
arr[n].p_2

Now, does it matter? That depends on very situational and contextual side-effects. Just as examples, let's explore a few scenarios:

Blocking, Buffered I/O

Suppose the reason p_2 was much more expensive is because it does some I/O like write to disk. It may be the case that this output operation is buffered, and while the Ruby runtime may return from the p_2 call, the output is still flushing by the time p_2 is called again, blocking it.

In this particular case, Option 2 is faster because p_1 computation can continue in the interim between the p_2 calls that block each other.

Cache Misses

Suppose the reason p_1 is fast is because its computation can be cached. Let's also suppose that calling p_2 disrupts that cache somehow in a way that make subsequent p_1 calls get cache misses:

  • maybe it also adds to the cache, and the cache fills up, evicting values
  • maybe the cache is time-evicted, and the cached data is evicted between p_1 calls because p_2 just takes too long

In this particular case, Option 1 is faster because the grouped p_1 calls are able to take advantage of the cache.

Sharing Limited Memory

Suppose both p_1 and p_2 calls require a lot of memory. Perhaps by interleaving them, resources needed by both have to be made readily available the entire time, hitting the memory limits of the system, hurting performance.

In this case, Option 1 is faster because once all p_1 calls are done, the memory used to hold its resources can be freed for use by the later p_2 calls.

like image 158
Kache Avatar answered Aug 18 '26 02:08

Kache