【问题标题】:Minimizing the number of costly function calls when the parameters remain the same (python)当参数保持不变时,最小化代价高昂的函数调用次数(python)
【发布时间】:2013-12-24 11:22:37
【问题描述】:

假设有一个函数costly_function_a(x) 使得:

  1. 在执行时间方面非常昂贵;
  2. 只要输入相同的x,它就会返回相同的输出;和
  3. 除了返回输出之外,它不执行“其他任务”。

在这些情况下,我们可以将结果存储在一个临时变量中,然后使用该变量来执行这些计算,而不是使用相同的x 连续两次调用该函数。

现在假设有一些函数(下例中的f(x)g(x)h(x))调用costly_function_a(x),并且其中一些函数可能会相互调用(在下例中,@ 987654328@和h(x)都打电话给f(x))。在这种情况下,使用上面提到的简单方法仍然会导致使用相同的x 重复调用costly_function_a(x)(参见下面的OkayVersion)。我确实找到了一种方法来最小化调用次数,但它是“丑陋的”(见下面的FastVersion)。有更好的方法来做到这一点吗?

#Dummy functions representing extremely slow code.
#The goal is to call these costly functions as rarely as possible.
def costly_function_a(x):
    print("costly_function_a has been called.")
    return x #Dummy operation.
def costly_function_b(x):
    print("costly_function_b has been called.")
    return 5.*x #Dummy operation.

#Simplest (but slowest) implementation.
class SlowVersion:
    def __init__(self,a,b):
        self.a = a
        self.b = b
    def f(self,x): #Dummy operation.
        return self.a(x) + 2.*self.a(x)**2
    def g(self,x): #Dummy operation.
        return self.f(x) + 0.7*self.a(x) + .1*x
    def h(self,x): #Dummy operation.
        return self.f(x) + 0.5*self.a(x) + self.b(x) + 3.*self.b(x)**2

#Equivalent to SlowVersion, but call the costly functions less often.
class OkayVersion:
    def __init__(self,a,b):
        self.a = a
        self.b = b
    def f(self,x): #Same result as SlowVersion.f(x)
        a_at_x = self.a(x)
        return a_at_x + 2.*a_at_x**2
    def g(self,x): #Same result as SlowVersion.g(x)
        return self.f(x) + 0.7*self.a(x) + .1*x
    def h(self,x): #Same result as SlowVersion.h(x)
        a_at_x = self.a(x)
        b_at_x = self.b(x)
        return self.f(x) + 0.5*a_at_x + b_at_x + 3.*b_at_x**2

#Equivalent to SlowVersion, but calls the costly functions even less often.
#Is this the simplest way to do it? I am aware that this code is highly
#redundant. One could simplify it by defining some factory functions...
class FastVersion:
    def __init__(self,a,b):
        self.a = a
        self.b = b
    def f(self, x, _at_x=None): #Same result as SlowVersion.f(x)
        if _at_x is None:
            _at_x = dict()
        if 'a' not in _at_x:
            _at_x['a'] = self.a(x)
        return _at_x['a'] + 2.*_at_x['a']**2
    def g(self, x, _at_x=None): #Same result as SlowVersion.g(x)
        if _at_x is None:
            _at_x = dict()
        if 'a' not in _at_x:
            _at_x['a'] = self.a(x)
        return self.f(x,_at_x) + 0.7*_at_x['a'] + .1*x
    def h(self,x,_at_x=None): #Same result as SlowVersion.h(x)
        if _at_x is None:
            _at_x = dict()
        if 'a' not in _at_x:
            _at_x['a'] = self.a(x)
        if 'b' not in _at_x:
            _at_x['b'] = self.b(x)
        return self.f(x,_at_x) + 0.5*_at_x['a'] + _at_x['b'] + 3.*_at_x['b']**2

if __name__ == '__main__':

    slow = SlowVersion(costly_function_a,costly_function_b)
    print("Using slow version.")
    print("f(2.) = " + str(slow.f(2.)))
    print("g(2.) = " + str(slow.g(2.)))
    print("h(2.) = " + str(slow.h(2.)) + "\n")

    okay = OkayVersion(costly_function_a,costly_function_b)
    print("Using okay version.")
    print("f(2.) = " + str(okay.f(2.)))
    print("g(2.) = " + str(okay.g(2.)))
    print("h(2.) = " + str(okay.h(2.)) + "\n")

    fast = FastVersion(costly_function_a,costly_function_b)
    print("Using fast version 'casually'.")
    print("f(2.) = " + str(fast.f(2.)))
    print("g(2.) = " + str(fast.g(2.)))
    print("h(2.) = " + str(fast.h(2.)) + "\n")

    print("Using fast version 'optimally'.")
    _at_x = dict()
    print("f(2.) = " + str(fast.f(2.,_at_x)))
    print("g(2.) = " + str(fast.g(2.,_at_x)))
    print("h(2.) = " + str(fast.h(2.,_at_x)))
    #Of course, one must "clean up" _at_x before using a different x...

这段代码的输出是:

Using slow version.
costly_function_a has been called.
costly_function_a has been called.
f(2.) = 10.0
costly_function_a has been called.
costly_function_a has been called.
costly_function_a has been called.
g(2.) = 11.6
costly_function_a has been called.
costly_function_a has been called.
costly_function_a has been called.
costly_function_b has been called.
costly_function_b has been called.
h(2.) = 321.0

Using okay version.
costly_function_a has been called.
f(2.) = 10.0
costly_function_a has been called.
costly_function_a has been called.
g(2.) = 11.6
costly_function_a has been called.
costly_function_b has been called.
costly_function_a has been called.
h(2.) = 321.0

Using fast version 'casually'.
costly_function_a has been called.
f(2.) = 10.0
costly_function_a has been called.
g(2.) = 11.6
costly_function_a has been called.
costly_function_b has been called.
h(2.) = 321.0

Using fast version 'optimally'.
costly_function_a has been called.
f(2.) = 10.0
g(2.) = 11.6
costly_function_b has been called.
h(2.) = 321.0

请注意,我不想“存储”过去使用的所有 x 值的结果(因为这需要太多内存)。此外,我不想让函数返回 (f,g,h) 形式的元组,因为在某些情况下我只想要 f(因此无需评估 costly_function_b)。

【问题讨论】:

  • 鉴于您不想要记忆,您期望什么样的答案或改进?
  • 我不想记住 x 的所有过去值的结果。但是,本地跟踪“当前x”很好(如本示例中所做的那样)。

标签: python optimization


【解决方案1】:

我接受@MartijnPieters 的解决方案,因为这对于 99% 会遇到与我类似的问题的人来说可能是正确的方法。然而,在我非常特殊的情况下,我只需要一个“1 的缓存”,所以花哨的@lru_cache(1) 装饰器有点矫枉过正。我最终编写了自己的装饰器(感谢this awesome stackoverflow 答案),我在下面提供。请注意,我是 Python 新手,所以这段代码可能并不完美。

from functools import wraps

def last_cache(func):
    """A decorator caching the last value returned by a function.

    If the decorated function is called twice (or more) in a row with exactly
    the same parameters, then this decorator will return a cached value of the
    decorated function's last output instead of calling it again. This may
    speed up execution if the decorated function is costly to call.

    The decorated function must respect the following conditions:
    1.  Repeated calls return the same value if the same parameters are used.
    2.  The function's only "task" is to return a value.

    """
    _first_call = [True]
    _last_args = [None]
    _last_kwargs = [None]
    _last_value = [None]
    @wraps(func)
    def _last_cache_wrapper(*args, **kwargs):
        if _first_call[0] or (args!=_last_args[0]) or (kwargs!=_last_kwargs[0]):
            _first_call[0] = False
            _last_args[0] = args
            _last_kwargs[0] = kwargs
            _last_value[0] = func(*args, **kwargs)
        return _last_value[0]
    return _last_cache_wrapper

【讨论】:

    【解决方案2】:

    您正在寻找的是 LRU 缓存;仅缓存最近使用的项目,限制内存使用以平衡调用成本和内存需求。

    由于使用不同的 x 值调用代价高昂的函数,最多会缓存多个返回值(每个唯一的 x 值),当缓存已满时,会丢弃最近最少使用的缓存结果。

    从 Python 3.2 开始,标准库附带了一个装饰器实现:@functools.lru_cache():

    from functools import lru_cache
    
    @lru_cache(16)  # cache 16 different `x` return values
    def costly_function_a(x):
        print("costly_function_a has been called.")
        return x #Dummy operation.
    
    @lru_cache(32)  # cache 32 different `x` return values
    def costly_function_b(x):
        print("costly_function_b has been called.")
        return 5.*x #Dummy operation.
    

    backport is available 用于早期版本,或者选择其他可以处理 PyPI 上可用的 LRU 缓存的可用库之一。

    如果您只需要缓存 一个 最近的项目,请创建自己的装饰器:

    from functools import wraps
    
    def cache_most_recent(func):
        cache = [None, None]
        @wraps(func)
        def wrapper(*args, **kw):
            if (args, kw) == cache[0]:
                return cache[1]
            cache[0] = args, kw
            cache[1] = func(*args, **kw)
            return cache[1]
        return wrapper
    
    @cache_most_recent
    def costly_function_a(x):
        print("costly_function_a has been called.")
        return x #Dummy operation.
    
    @cache_most_recent
    def costly_function_b(x):
        print("costly_function_b has been called.")
        return 5.*x #Dummy operation.
    

    这个更简单的装饰器比功能更强大的functools.lru_cache() 的开销更少。

    【讨论】:

    • 优于我的,虽然我没有考虑有限缓存。我想如果知道它只会有几个,那将是最佳的。 +1
    • 哇,我从来不知道标准库中有什么东西可以做到这一点,我总是自己实现它。我猜是时候切换到 3.2 了
    • @qwwqwwq:当然,缓存也有性能损失;当它应用于re 模块perfomance actually suffered 时,因为之前使用的朴素模式缓存要快得多。该更改已恢复。
    • 非常有趣。这对于@lru_cache(1) 仍然是最优的吗?
    • @user1661473:如果函数本身的成本高于缓存管理所产生的(轻微的)性能损失,那么我肯定会这么说。请注意,以 2 为单位的高速缓存大小是最佳的;至少使用@lru_cache(2)
    猜你喜欢
    • 1970-01-01
    • 2019-09-07
    • 1970-01-01
    • 1970-01-01
    • 2012-03-21
    • 1970-01-01
    • 1970-01-01
    • 2017-08-18
    • 1970-01-01
    相关资源
    最近更新 更多