【问题标题】:Fastest way to get pairs of range(n)?获得成对范围(n)的最快方法?
【发布时间】:2022-08-10 01:32:43
【问题描述】:

想象一下,您想要所有从 0 到 n-1 的数字对,例如 n = 4

[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]

三种方法可以做到这一点:

list(combinations(range(n), 2))

[(i, j) for i, j in combinations(range(n), 2)]

[(i, j) for i in range(n) for j in range(i+1, n)]

n = 1000 的基准测试结果:

44.1 ms ± 0.2 ms  f_combinations_pure
57.7 ms ± 0.3 ms  f_combinations
66.6 ms ± 0.1 ms  f_ranges

第一个真的不算数。在现实中,你想要带有ij 的东西,例如将它们用于get substringscomments 启发了这个的问题)。

问题一:为什么f_rangesf_combinations慢?它的for i in 总体上只运行n 次,因此与运行n*(n-1)/2 次的for j in 相比,它微不足道。 for j in range(...) 只分配一个数字,而 for i, j in combinations(...) 构建并分配的数字,所以后者应该是慢点.为什么快点?

问题2:你能想出最快的方法是什么?为了公平比较,它应该是一个列表理解 [(i, j) for ...] 产生相同的对列表。

(因为我自己包括了一个答案(is encouraged),所以我在那里包括了基准代码。)

    标签: python performance combinations


    【解决方案1】:

    关于问题1:为什么rangecombinations慢?

    虽然for j in range(...) 确实具有只分配一个数字的优势,但它具有坏处一遍又一遍地创造它们。在 Python 中,数字是对象,它们的创建(和删除)需要一点时间。

    另一方面,combinations(...) 首先只创建和存储一次数字对象,然后成对地重复使用它们。你可能认为“等一下,它可以重复使用数字,但它产生的对为tuple 对象,所以它每次迭代创建一个对象!”.嗯......它有一个优化。它实际上一遍又一遍地重用相同的tuple 对象,用不同的数字填充它。“什么?不可能!元组是不可变的!”嗯......表面上它们是不可变的,是的。但是,如果combinations 迭代器发现没有其他对其结果元组的引用,那么它会“作弊”并无论如何都会对其进行修改。在 C 代码级别,它可以做到这一点。如果没有其他东西可以参考它,那么就没有害处。请注意,for i, j in ... 解包元组并且不保留对它的引用。如果您改为使用for pair in ...,那么pair 是对它的引用,并且不会应用优化,实际上每次都会创建一个新的result 元组。有兴趣的可以看combinations_next的源码。

    关于问题2:最快的方法是什么?

    我发现了四种更快的方法:

    44.1 ms ± 0.2 ms  f_combinations_pure
    51.7 ms ± 0.1 ms  f_list
    52.7 ms ± 0.2 ms  f_tee
    53.6 ms ± 0.1 ms  f_copy_iterator
    54.6 ms ± 0.1 ms  f_tuples
    57.7 ms ± 0.3 ms  f_combinations
    66.6 ms ± 0.1 ms  f_ranges
    

    所有四种更快的方法都避免了使 range 解决方案变慢的原因:它们不是创建(和删除)Θ(n²) int 对象,而是一遍又一遍地重用相同的对象。

    f_tuples 将它们放入一个元组并遍历切片:

    def f_tuples(n):
        nums = tuple(range(n))
        return [(i, j)
                for i in nums
                for j in nums[i+1:]]
    

    f_list 将它们放入一个列表中,然后在每个 j-loop 之前,它会删除第一个数字:

    def f_list(n):
        js = list(range(n))
        return [(i, j)
                for i in range(n)
                if [js.pop(0)]
                for j in js]
    

    f_copy_iterator 将它们放入一个元组中,然后为i 使用一个迭代器,为j 使用该迭代器的copy(这是一个在i 之后开始一个位置的迭代器):

    def f_copy_iterator(n):
        nums = iter(tuple(range(n)))
        return [(i, j)
                for i in nums
                for j in copy(nums)]
    

    f_tee 使用 itertools.tee 获得与 copy 类似的效果。它的JSj 值的主要迭代器,在每个j-loop 之前,它会丢弃第一个值,然后对JS 进行开球以获取剩余值的第二个迭代器:

    def f_tee(n):
        return [(i, j)
                for JS in [iter(range(n))]
                for i in range(n)
                for _, (JS, js) in [(next(JS), tee(JS))]
                for j in js]
    

    额外的问题:像那些更快的方法那样优化是否值得?

    嗯,应该不会吧。可能你最好只使用for i, j in combinations(...)。更快的方法并没有快多少,而且它们更复杂一些。另外,实际上,您实际上会使用ij 做一些事情(例如获取子字符串),因此相对较小的速度优势变得相对较小。

    但我希望你至少觉得这很有趣,也许学到了一些新的东西有朝一日有用。

    完整的基准代码

    Try it online!

    def f_combinations_pure(n):
        return list(combinations(range(n), 2))
    
    
    def f_combinations(n):
        return [(i, j) for i, j in combinations(range(n), 2)]
    
    
    def f_ranges(n):
        return [(i, j) for i in range(n) for j in range(i+1, n)]
    
    
    def f_tuples(n):
        nums = tuple(range(n))
        return [(i, j) for i in nums for j in nums[i+1:]]
    
    
    def f_list(n):
        js = list(range(n))
        return [(i, j) for i in range(n) if [js.pop(0)] for j in js]
    
    
    def f_copy_iterator(n):
        nums = iter(tuple(range(n)))
        return [(i, j) for i in nums for j in copy(nums)]
    
    
    def f_tee(n):
        return [(i, j)
                for JS in [iter(range(n))]
                for i in range(n)
                for _, (JS, js) in [(next(JS), tee(JS))]
                for j in js]
    
    
    fs = [
        f_combinations_pure,
        f_combinations,
        f_ranges,
        f_tuples,
        f_list,
        f_copy_iterator,
        f_tee
    ]
    
    from timeit import default_timer as time
    from itertools import combinations, tee
    from statistics import mean, stdev
    from random import shuffle
    from copy import copy
    
    # Correctness
    expect = fs[0](1000)
    for f in fs:
        result = f(1000)
        assert result == expect
    
    # Prepare for timing
    times = {f: [] for f in fs}
    def stats(f):
        ts = [t * 1e3 for t in sorted(times[f])[:5]]
        return f'{mean(ts):4.1f} ms ± {stdev(ts):3.1f} ms '
    
    # Timing
    for i in range(25):
        shuffle(fs)
        for f in fs:
            start = time()
            result = f(1000)
            end = time()
            times[f].append(end - start)
            del result
    
    # Results
    for f in sorted(fs, key=stats):
        print(stats(f), f.__name__)
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-03-26
      • 2015-01-06
      • 1970-01-01
      • 2019-10-11
      • 1970-01-01
      • 2023-03-05
      相关资源
      最近更新 更多