1

I have a function that takes in two arrays, does some calculation, and then returns a new array. The function is called maybe 60,000 times (or 500k, depending), where up to 25% of the times it is called, it is redoing a previous calculation.

prev_calc = {}
def arrayMagic(ar1,ar2):
    ar1_t = tuple(ar1)
    ar2_t = tuple(ar2)

    if (ar1_t,ar2_t) in prev_calc: 
        return prev_calc[(ar1_t,ar2_t)]
    #some somewhat, but not too expensive function
    res = magicCalc(ar1,ar2)

    prev_calc[(ar1_t,ar2_t)] = res

    return res

ar1 and ar2 are arrays of 3 floats each; for example: [1.1,1.2,2.2] res is also an array of 3 floats.

The problem is that the tuple conversion and dictionary lookup happen to be very close to the time the magicCalc() function takes. Since this is the major bottleneck in my code, optimizing this would solve alot. The function is passed the two arrays, so I can't have them as an array already.

Is there a fast way to store past results of a function and return them?

user-2147482637
  • 2,115
  • 8
  • 35
  • 56
  • `The problem is that the tuple conversion and dictionary lookup happen to be very close to the time the magicCalc() function takes.` What do you mean by this? – information_interchange Jan 14 '20 at 01:20
  • @information_interchange the amount of time magicCalc takes is not extremely long, but its not negligible. Basically skipping the dictionary storage takes the same amount of time as after I implemented it. So if this is the fastest way to store results of the function, then I am done as both methods are close to the same speed. If there is any faster way, then I benefit. – user-2147482637 Jan 14 '20 at 01:25
  • 1
    Could you tell more about what `ar1` and `ar2` are? `numpy` arrays? How large? – Michael Butscher Jan 14 '20 at 01:34
  • How big are `ar1` and `ar2`? – C.Nivs Jan 14 '20 at 01:34
  • @C.Nivs sorry I updated, both are 3 floats. not numpy. – user-2147482637 Jan 14 '20 at 01:38
  • @MichaelButscher not numpy, part of it relies on ironpython – user-2147482637 Jan 14 '20 at 01:38
  • 1
    `retval = prev_calc.get((ar1, ar2)); if retval != None: return retval` would mean you aren't doing your lookups twice (once for the `in` condition, and a second time for the `return`). That said, I generally avoid choosing Python for performance-critical code. – Charles Duffy Jan 14 '20 at 01:47
  • 2
    Also -- if you haven't already run this code through a profiler, I'd strongly recommend it. Ideally, one that can keep an eye on what the garbage collector is doing. – Charles Duffy Jan 14 '20 at 01:50
  • @CharlesDuffy i will try the get method in the morning. i can't profile in the ironpython side, which i need as this is interfacing with an API. otherwise i agree about speed – user-2147482637 Jan 14 '20 at 02:05
  • There are some pretty good .net profilers out there. Sure, it's more work to read a profile of time spent in IronPython itself than it is to read one that describes your Python code's performance directly, but it may be at least as informative. – Charles Duffy Jan 14 '20 at 02:18
  • @CharlesDuffy the get method is slower than checking if the key is in the dictionary since it is only there 25% of the time. – user-2147482637 Jan 15 '20 at 04:13
  • I'm honestly surprised -- it should require the same tree traversal either way, and `None` is a singleton, so there's no garbage collection load created by code that returns it. – Charles Duffy Jan 15 '20 at 13:54
  • @CharlesDuffy https://stackoverflow.com/questions/36566331/why-does-dict-getkey-run-slower-than-dictkey – user-2147482637 Jan 15 '20 at 21:38
  • That's the case for CPython, but IronPython is running on a JITted engine; it should be able to optimize the lookup into the fast path; for that matter, I'd hope good JIT would be inlining the call itself. – Charles Duffy Jan 15 '20 at 21:56

1 Answers1

0

This seems pretty quick...

d = dict()
l1 = [1.1,21.1,31.1,41.1,51.1]
l2 = [12.1,21.1,31.1,41.1,51.1]


def do(a1):
    cacheKey = str(a1) 
    if cacheKey in d:
        print("Read from cache")
        return d[cacheKey]
    else:
        print("calc and cache result")
        d[cacheKey] = sum(a1)
        return d[cacheKey]

print(do(l1))
print(do(l2))
print(do(l1))
TomG12
  • 21
  • 3
  • Measured how? The `print()`s are going to be slower than anything else here, slow enough to dominate any timings measured. – Charles Duffy Jan 14 '20 at 02:02
  • The print statements are only for example. The real issue here is how best to hash an array in order to cache it. Given that there are only 3 floats in the array, converting it to a string shouldn't be that expensive. No actual measurement was taken. – TomG12 Jan 14 '20 at 02:11
  • 1
    If you want to assert that `str(somelist)` is faster than `tuple(somelist)`, benchmarks are in order to back that up. I would be *extremely* surprised if it were true. – Charles Duffy Jan 14 '20 at 02:11
  • 1
    See https://ideone.com/gCZahY -- `tuple(l)` is more than 20x faster than `str(l)`. – Charles Duffy Jan 14 '20 at 02:17