为什么这种带有记忆功能的LCS(最长公共子序列)Python实现的性能很差?

为什么这种带有记忆功能的LCS(最长公共子序列)Python实现的性能很差?,python,recursion,dynamic-programming,memoization,lcs,Python,Recursion,Dynamic Programming,Memoization,Lcs,我正在学习动态规划,偶然发现了LCS(最长公共子序列)算法 我已经用Python实现了它的几个版本,以了解实现之间的差异以及它们的性能 代码如下: import time import sys sys.setrecursionlimit(50000) def current_milli_time(): return time.time() * 1000 def memoize_decorator(fn): cache = {} def inner_fn(*args):

我正在学习动态规划,偶然发现了LCS(最长公共子序列)算法

我已经用Python实现了它的几个版本,以了解实现之间的差异以及它们的性能

代码如下:

import time
import sys

sys.setrecursionlimit(50000)


def current_milli_time(): return time.time() * 1000


def memoize_decorator(fn):
    cache = {}

    def inner_fn(*args):
        if args in cache:
            return cache[args]
        ret = fn(*args)
        cache[args] = ret
        return ret

    inner_fn.__name__ = fn.__name__
    inner_fn.__doc__ = fn.__doc__
    return inner_fn


class bcolors:
    HEADER = '\033[95m'
    OKBLUE = '\033[94m'
    OKGREEN = '\033[92m'
    WARNING = '\033[93m'
    FAIL = '\033[91m'
    ENDC = '\033[0m'
    BOLD = '\033[1m'
    UNDERLINE = '\033[4m'


class TestORedAssertValue:
    def __init__(self, *ored_assert_values):
        self.__values = ored_assert_values

    def assert_value(self, value):
        for self_value in self.__values:
            if self_value == value:
                return True
        return False

    def values(self):
        return list(self.__values)


def test_assert(label, value, assertValue, *args):
    ok = False
    assertToPrint = assertValue
    if isinstance(assertValue, TestORedAssertValue):
        ok = assertValue.assert_value(value)
        assertToPrint = " OR ".join(map(str, list(assertValue.values())))
    elif value == assertValue:
        ok = True
    if ok:
        print('#', label, ' - ', bcolors.OKGREEN, 'ok', bcolors.ENDC, sep='')
    else:
        print('#', label, ' - args: ', ", ".join(map(str, args)), ', expected: ', assertToPrint,
              ', got: ', value, ' - ', bcolors.FAIL, 'fail', bcolors.ENDC, sep='')


def measure_time_decorator(fn):
    def inner(*args):
        start = current_milli_time()
        ret = fn(*args)
        end = current_milli_time()
        print('time: ', end - start, ' ms', sep='', end=' - ')
        return ret
    inner.__name__ = fn.__name__
    inner.__doc__ = fn.__doc__
    return inner


def test(fn):
    print()
    print("Testing:", fn.__name__)

    fn = measure_time_decorator(fn)

    A = ['A', 'C', 'B', 'D', 'E', 'G', 'C', 'E', 'D', 'B', 'G']
    B = ['B', 'E', 'G', 'C', 'F', 'E', 'U', 'B', 'K']
    assertRes = ['B', 'E', 'G', 'C', 'E', 'B']
    res = fn(A, B)
    test_assert('testcase 1', res, assertRes, A, B)

    A = ['A', 'B']
    B = []
    assertRes = []
    res = fn(A, B)
    test_assert('testcase 2', res, assertRes, A, B)

    A = []
    B = []
    assertRes = []
    res = fn(A, B)
    test_assert('testcase 3', res, assertRes, A, B)

    A = [1, 2]
    B = [1, 2]
    assertRes = [1, 2]
    res = fn(A, B)
    test_assert('testcase 4', res, assertRes, A, B)

    A = [1, 2]
    B = [1, 2]
    assertRes = [1, 2]
    res = fn(A, B)
    test_assert('testcase 5', res, assertRes, A, B)

    A = ['A', 'B', 'C', 'E', 'F', 'G', 'H', 'I', 'L']
    B = ['A', 'B', 'C', 'E', 'F', 'G', 'H', 'I', 'L']
    assertRes = ['A', 'B', 'C', 'E', 'F', 'G', 'H', 'I', 'L']
    res = fn(A, B)
    test_assert('testcase 6', res, assertRes, A, B)

    A = [x for x in range(3000)]
    B = A
    assertRes = A
    res = fn(A, B)
    test_assert('testcase 7', res, assertRes, A, B)

    A = [x for x in range(3000)]
    B = list(reversed(A))
    assertRes = TestORedAssertValue([0], [2999])
    res = fn(A, B)
    test_assert('testcase 8', res, assertRes, A, B)


def longest_common_subsequence(A, B):
    N = len(A)
    M = len(B)
    res_matrix = [[[]] * (M + 1) for i in range(N + 1)]
    for i in range(1, N + 1):
        for j in range(1, M + 1):
            if A[i - 1] == B[j - 1]:
                res_matrix[i][j] = res_matrix[i - 1][j - 1] + [A[i - 1]]
            else:
                res_matrix[i][j] = res_matrix[i][j - 1] if (
                    len(res_matrix[i][j - 1])
                    >
                    len(res_matrix[i - 1][j])
                ) else res_matrix[i - 1][j]
    return res_matrix[-1][-1]


def longest_common_subsequence_recursive_memoized(A, B):
    N = len(A)
    M = len(B)
    if N <= 0 or M <= 0:
        return []
    res_matrix = [[[]] * M for i in range(N)]

    @memoize_decorator
    def recursion(i, j):
        if i <= -1 or j <= -1:
            return []
        elif A[i] == B[j]:
            res_matrix[i][j] = recursion(i - 1, j - 1) + [A[i]]
        else:
            prev1 = recursion(i - 1, j)
            prev2 = recursion(i, j - 1)
            res_matrix[i][j] = prev1 if (
                len(prev1)
                >
                len(prev2)
            ) else prev2
        return res_matrix[i][j]

    recursion(N - 1, M - 1)
    return res_matrix[-1][-1]


def longest_common_subsequence_recursive_memoized_mit(A, B):
    N = len(A)
    M = len(B)
    if N <= 0 or M <= 0:
        return []
    res_matrix = [[None] * M for i in range(N)]

    def lcs(i, j):
        if i <= -1 or j <= -1:
            return []
        if res_matrix[i][j] == None:
            if A[i] == B[j]:
                res_matrix[i][j] = lcs(i - 1, j - 1) + [A[i]]
            else:
                prev1 = lcs(i - 1, j)
                prev2 = lcs(i, j - 1)
                res_matrix[i][j] = prev1 if (
                    len(prev1)
                    >
                    len(prev2)
                ) else prev2
        return res_matrix[i][j]

    return lcs(N - 1, M - 1)


if __name__ == "__main__":
    test(longest_common_subsequence_recursive_memoized)
    test(longest_common_subsequence_recursive_memoized_mit)
    test(longest_common_subsequence)
    print()

有趣的部分是
testcase8
。 您可以看到,对于这个测试用例,
longest\u common\u subsequence\u recursive\u memorized
执行得很差(
105788.96801757812 ms~=105.8秒
),而对于其他两个函数,它只需要不超过30秒(
longest\u common\u subsequence
是最好的一个,大约需要10秒完成)

我的问题是:
longest\u common\u subsequence\u recursive\u memorized
为什么对testcase 8执行得如此糟糕,而实现与
longest\u common\u subsequence\u recursive\u memorized\u mit
非常相似

尽管如此,它仍然使用记忆,而不是直接访问
res_matrix
并从中返回值,它使用一个装饰器包装递归函数来缓存计算结果,并在需要之前已经计算过的计算时立即返回

谢谢你的关注

编辑:经过几次尝试,我发现性能问题似乎与
@memoize\u decorator
功能有关

如果我重写
longest\u common\u subsequence\u recursive\u memorized
函数,添加
longest\u common\u subsequence\u recursive\u memorized\u mit
中使用的等效测试(mit版本):

如果我评论
@memoize\u decorator
行:

def longest_common_subsequence_recursive_memoized(A, B):
    N = len(A)
    M = len(B)
    if N <= 0 or M <= 0:
        return []
    res_matrix = [[None] * M for i in range(N)]

    # @memoize_decorator <--- Comment
    def lcs(i, j):
        if i <= -1 or j <= -1:
            return []
        if res_matrix[i][j] == None:
            if A[i] == B[j]:
                res_matrix[i][j] = lcs(i - 1, j - 1) + [A[i]]
            else:
                prev1 = lcs(i - 1, j)
                prev2 = lcs(i, j - 1)
                res_matrix[i][j] = prev1 if (
                    len(prev1)
                    >
                    len(prev2)
                ) else prev2
        return res_matrix[i][j]

    lcs(N - 1, M - 1)
    return res_matrix[-1][-1]
因此,我猜性能缺陷来自
memoize\u decorator
,好像我删除了它,就获得了性能增益。奇怪的是,记忆会加速重复计算

但是在这个修改后的
最长的\u公共的\u子序列\u递归的\u记忆化的
函数中,如果它的内部函数是用
记忆化的
修饰符来记忆的,则实际上并不重要,此时,因为通过
res\u矩阵
如果res\u矩阵[i][j]==None:
测试,
最长的\u公共\u子序列\u递归\u记忆
已经单独使用了记忆

因此,我的最终诊断是,
~95秒
内部fn中的代码相关:

    ...
        if args in cache:
            return cache[args]
        ret = fn(*args)
        cache[args] = ret
        return ret
    ...

另一方面,如果我将
最长的\u公共的\u子序列\u递归的
保留为以前的但是没有
@memoize\u decorator
(将其重命名为
最长的\u公共的\u子序列\u递归的
):

def longest\u common\u subsequence\u recursive(A,B):
N=len(A)
M=len(B)
如果N
我的问题是:为什么最长的\u公共的\u子序列\u递归的\u记忆
当实现非常复杂时,testcase 8的性能非常差
与最长\u公共\u子序列\u递归\u记忆\u mit类似

我发现两者之间的主要性能差异在于MIT版本具有:

res_matrix = [[None] * M for i in range(N)]
...
if res_matrix[i][j] == None:
而您的使用:

res_matrix = [[[]] * M for i in range(N)]
没有等效的测试。如果我们修改您的应用程序以合并与MIT相同的初始化和测试,并使用Python自己的
rlu_cache()
functools进行修饰,以便我们可以查询缓存,我们将得到:

CacheInfo(hits=380620, misses=17610383, maxsize=128, currsize=128)
time: 19195.7060546875 ms - #testcase 8 - ok
将缓存大小从默认值128增加可提高性能:

CacheInfo(hits=8988004, misses=9002999, maxsize=4096, currsize=4096)
time: 16645.57080078125 ms - #testcase 8 - ok

但只到了某一点,就没有什么区别了。

谢谢你的回复<代码>当您使用时:。。。没有等效测试。
谢谢您的回复。然后我想到的问题是,为什么缓存中的
if args:
@memoize\u decorator(recursion)
中的
最长公共子序列\u recursive\u recursive
if res\u矩阵[i][j]的工作原理几乎相同==无:
最长\u公共\u子序列\u递归\u记忆\u mit中的条件
?即使memoized
recusion
函数返回将返回值保存在缓存中,缓存似乎也不会提高执行速度。。。这是我仍然不明白的主要区别。你能提供一个例子说明这两种方法之间的区别吗?我再次尝试使用代码,似乎开销来自
@memoize\u decorator
decorator函数。即使我将
最长的\u公共的\u子序列\u递归的\u记忆化
与MIT版本相同,如果我用
@memoize\u decorator
包装内部的
递归()
函数,完成testcase 8需要90秒……如果我注释
@memoize\u decorator
行,然后在25秒内运行
最长的\u公共\u子序列\u递归\u记忆
(修改为与MIT函数相同,如果res\u矩阵[i][j]==None:
测试,则使用等效的
)。。。这可能是因为
memoize\u decorator
中使用了
缓存
数据结构吗?
def longest_common_subsequence_recursive(A, B):
    N = len(A)
    M = len(B)
    if N <= 0 or M <= 0:
        return []
    res_matrix = [[[]] * M for i in range(N)]

    # @memoize_decorator <--- Without memoization
    def recursion(i, j):
        if i <= -1 or j <= -1:
            return []
        elif A[i] == B[j]:
            res_matrix[i][j] = recursion(i - 1, j - 1) + [A[i]]
        else:
            prev1 = recursion(i - 1, j)
            prev2 = recursion(i, j - 1)
            res_matrix[i][j] = prev1 if (
                len(prev1)
                >
                len(prev2)
            ) else prev2
        return res_matrix[i][j]

    recursion(N - 1, M - 1)
    return res_matrix[-1][-1]
res_matrix = [[None] * M for i in range(N)]
...
if res_matrix[i][j] == None:
res_matrix = [[[]] * M for i in range(N)]
CacheInfo(hits=380620, misses=17610383, maxsize=128, currsize=128)
time: 19195.7060546875 ms - #testcase 8 - ok
CacheInfo(hits=8988004, misses=9002999, maxsize=4096, currsize=4096)
time: 16645.57080078125 ms - #testcase 8 - ok