In the past few days I've been working on improving the runtime of a python function which requires many uses of the remainder function (%) among other things. My main test case is over an 80,000 element numpy array (monotonically increasing), with 10000 iterations, although I've tried on various other sizes as well.
Eventually I reached a point where the remainder function is a major bottleneck, and tried various solutions. This is the behaviour I found when running the following code:
import numpy as np
import time
a = np.random.rand(80000)
a = np.cumsum(a)
d = 3
start_time1 = time.time()
for i in range(10000):
b = a % d
d += 0.001
end_time1 = time.time()
d = 3
start_time2 = time.time()
for i in range(10000):
b = a - (d * np.floor(a / d))
d += 0.001
end_time2 = time.time()
print((end_time1 - start_time1) / 10000)
print((end_time2 - start_time2) / 10000)
The output is:
0.0031344462633132934
0.00022937238216400147
when increasing the array size to 800,000:
0.014903099656105041
0.010498356819152833
(For this post I ran the code only once for actual output, while trying to understand the issue I've gotten these results consistently.)
While this solves my runtime problem - I have a hard time understand why. Am I missing something? The only difference I can think of is the overhead of an additional function call, but the first case is pretty extreme (and 1.5x the runtime isn't good enough either), and if that were the case I would think that the existance of the np.remainder function is pointless.
Edit: I tried testing the same code with non-numpy loops:
import numpy as np
import time
def pythonic_remainder(array, d):
b = np.zeros(len(array))
for i in range(len(array)):
b[i] = array[i] % d
def split_pythonic_remainder(array, d):
b = np.zeros(len(array))
for i in range(len(array)):
b[i] = array[i] - (d * np.floor(array[i] / d))
def split_remainder(a, d):
return a - (d * np.floor(a / d))
def divide(array, iterations, action):
d = 3
for i in range(iterations):
b = action(array, d)
d += 0.001
a = np.random.rand(80000)
a = np.cumsum(a)
start_time = time.time()
divide(a, 10000, split_remainder)
print((time.time() - start_time) / 10000)
start_time = time.time()
divide(a, 10000, np.remainder)
print((time.time() - start_time) / 10000)
start_time = time.time()
divide(a, 10000, pythonic_remainder)
print((time.time() - start_time) / 10000)
start_time = time.time()
divide(a, 10000, split_pythonic_remainder)
print((time.time() - start_time) / 10000)
The result I get is:
0.0003770533800125122
0.003932329940795899
0.018835473942756652
0.10940513386726379
I find it interesting that the opposite is true in the non-numpy case.