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?
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:
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.
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:
p_1 calls because p_2 just takes too longIn this particular case, Option 1 is faster because the grouped p_1 calls are able to take advantage of the cache.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With