# Implement function over lists efficiently, instead of per-element

I have a function, which takes the numeric values d1,d2,p1,p2,s as input, and should return:

• {d1,d2} if d1 <= p1*s and d2 <= p2*s
• {d1,s-d1} if d1 <= p1*s and d2 > p2*s
• {s-d2,d2} if d1 > p1*s and d2 <= p2*s
• {p1*s,p2*s} if d1 > p1*s and d2 > p2*s

An easy way to implement this function is the following:

f[d1_,d2_,p1_,p2_,s_] := Which[d1 <= p1*s && d2 <= p2*s, {d1,d2}, d1 <= p1*s && d2 > p2*s, {d1,s-d1}, d1 > p1*s && d2 <= p2*s, {s-d2,d2}, True, {p1*s,p2*s}]


However, instead of only calling this function for one set of values, I want to call this function for a list of values. Specifically, I have given a list Px that consists of pairs {p1,p2}, a list Sx that consists of values s, and a list Dx that consists of values {d1,d2}.

In an effort to make the implementation more efficient, I did the following:

checkCase = Thread[#[[1]] <= #[[2]]*#[[3]]] & /@ Transpose@{Dx, Sx, Px};
functionValue = Flatten[Which[
#[[1]] == {True, True}, #[[2]],
#[[1]] == {True, False}, {#[[2]][[1]], #[[3]] - #[[2]][[1]]},
#[[1]] == {False, True}, {#[[3]] - #[[2]][[2]], #[[2]][[2]]},
True, #[[4]]*#[[3]]
] & /@ Transpose@{checkCase, Dx, Sx, Px}];


However, this is of course a very basic speed-up, just using the map-operator. I feel like there are is much more potential in speeding-up the above computation over the three lists, using vertorized operators, and would be very grateful if someone would be able to point out what functions and/or operators would be useful here.

• You can use your function f on your lists by combing them and then MapApplying. Eg: Dx = RandomInteger[{0, 9}, {3, 2}]; Px = RandomInteger[{0, 9}, {3, 2}]; Sx = RandomInteger[{0, 9}, 3]; input = Flatten /@ Thread[{Dx, Px, Sx}]; f @@@ input
– ydd
Aug 23, 2023 at 15:37
• Perhaps BoolEval, written by our very own @Szabolcs, could be helpful here. Aug 24, 2023 at 1:40

With functions such as UnitStep, you can apply your condition in a Listable fashion to enjoy its performance, like below:

Here I chose 1 million random numbers for each column:

Block[{d1, d2, p1, p2, s, temp1, temp2, temp1not, temp2not, result1,
result2, result3, result4},

{d1, d2, p1, p2, s} = RandomInteger[10, {5, 1000000}];

temp1 = UnitStep[p1*s - d1];
temp2 = UnitStep[p2*s - d2];
temp1not = 1 - temp1;
temp2not = 1 - temp2;

result1 = Pick[Transpose@{d1, d2}, temp1*temp2, 1];
result2 = Pick[Transpose@{d1, s - d2}, temp1*temp2not, 1];
result3 = Pick[Transpose@{s - d2, d2}, temp1not*temp2, 1];
result4 = Pick[Transpose@{p1*s, p2*s}, temp1not*temp2not, 1];

]

(* Out: {0.131124, Null} *)


Your function on similar data was around 4 times slower on my machine with Mathematica 13.3.1:

Block[{d = RandomInteger[10, {1000000, 5}]},
f @@@ d;
] // AbsoluteTiming

(* Out: {3.84095, Null} *)


We can squeeze our code further if we Pick elements first before calculating the final expression like s-d2, d2 or by using Compile* functions in Mathematica.

Since version 12.1, we can execute Julia directly from the notebook. In computation-intensive workflows, it's a good reference to compare. In this case, a similar approach was 5 times faster than the provided solution.

Here is the code in Julia:

using Random
n= 1000000
d1 = rand(Int, n)
d2 = rand(Int, n)
p1 = rand(Int, n)
p2 = rand(Int, n)
s = rand(Int, n)

temp1 = d1 .<= p1.*s
temp2 = d2 .<= p2.*s

i = temp1 .* temp2
result1 = [d1[i] d2[i]]

i = temp1 .* .!temp2
result2 = [d1[i] s[i]-d2[i]]

i = .!temp1 .* temp2
result3 = [s[i]-d2[i] d2[i]]

i = .!temp1 .* .!temp2
result4 = [p1[i].*s[i] p2[i].*s[i]]


The main part was executed in 0.021768 seconds (second run).