【问题标题】:Comparing list comprehensions and explicit loops (3 array generators faster than 1 for loop)比较列表理解和显式循环(3 个数组生成器比 1 个 for 循环更快)
【发布时间】:2017-01-23 22:33:13
【问题描述】:

我做了功课,无意中发现算法的速度有一个奇怪的不一致。 这是相同函数 bur 的 2 个版本的代码,但有 1 个不同:在第一个版本中,我使用 3 次数组生成器来过滤一些数组,在第二个版本中,我使用 1 个 for 循环和 3 个 if 语句来执行相同的过滤工作。

所以,这里是第一个版本的代码:

def kth_order_statistic(array, k):
    pivot = (array[0] + array[len(array) - 1]) // 2
    l = [x for x in array if x < pivot]
    m = [x for x in array if x == pivot]
    r = [x for x in array if x > pivot]
    if k <= len(l):
            return kth_order_statistic(l, k)
    elif k > len(l) + len(m):
            return kth_order_statistic(r, k - len(l) - len(m))
    else:
            return m[0]

这里是第二版的代码:

def kth_order_statistic2(array, k):
    pivot = (array[0] + array[len(array) - 1]) // 2
    l = []
    m = []
    r = []
    for x in array:
        if x < pivot:
            l.append(x)
        elif x > pivot:
            r.append(x)
        else:
            m.append(x)

    if k <= len(l):
        return kth_order_statistic2(l, k)
    elif k > len(l) + len(m):
        return kth_order_statistic2(r, k - len(l) - len(m))
    else:
        return m[0]

第一个版本的 IPython 输出:

In [4]: %%timeit
   ...: A = range(100000)
   ...: shuffle(A)
   ...: k = randint(1, len(A)-1)
   ...: order_statisctic(A, k)
   ...:
10 loops, best of 3: 120 ms per loop

对于第二个版本:

In [5]: %%timeit
   ...: A = range(100000)
   ...: shuffle(A)
   ...: k = randint(1, len(A)-1)
   ...: kth_order_statistic2(A, k)
   ...:
10 loops, best of 3: 169 ms per loop

那么为什么第一个版本比第二个版本快?我还使用 filter() 函数而不是数组生成器制作了第三个版本,它比第二个版本慢(每个循环有 218 毫秒)

【问题讨论】:

  • 列表推导通常比等效的 for 循环更快。扩展列表(您的附加函数)也可能比填充已知大小的列表更昂贵。
  • 显着增加您的列表大小...我想您会发现差距或多或少是恒定的...您正在用空间换时间,但它们大致等价的(生成器/迭代器与列表相比有一点开销)
  • 对于A 的不同随机排序和k 的不同值,时间可能会有很大差异。确保在这两种情况下都为 same Ak 计时。我想你会发现差别很小。
  • 你在kth_order_statistic2中打电话给kth_order_statistic
  • 真的应该使用numpy

标签: python arrays python-2.7 time list-comprehension


【解决方案1】:

让我们定义回答问题所需的函数并为它们计时:

In [18]: def iter():
    l = [x for x in range(100) if x > 10]
   ....:

In [19]: %timeit iter()
100000 loops, best of 3: 7.92 µs per loop

In [20]: def loop():
    l = []
    for x in range(100):
        if x > 10:
            l.append(x)
   ....:

In [21]: %timeit loop()
10000 loops, best of 3: 20 µs per loop

In [22]: def loop_fast():
    l = []
    for x in range(100):
        if x > 10:
            pass
   ....:

In [23]: %timeit loop_fast()
100000 loops, best of 3: 4.69 µs per loop

我们可以看到,没有 append 命令的 for 循环与列表推导式一样快。事实上,如果我们看一下字节码,我们可以看到,在列表解析的情况下,python 能够使用一个名为 LIST_APPEND 的内置字节码命令,而不是:

  • 加载列表:40 LOAD_FAST
  • 加载属性:43 LOAD_ATTRIBUTE
  • 调用加载的函数:49 CALL_FUNCTION
  • 卸载列表(?):52 POP_TOP

从下面的输出中可以看出,列表解析和“loop_fast”函数缺少前一个字节码。比较三个函数的timeit,很明显这三个函数的时间不同。

In [27]: dis.dis(iter)
  2          0 BUILD_LIST             0
             3 LOAD_GLOBAL            0 (range)
             6 LOAD_CONST             1 (1)
             9 LOAD_CONST             2 (100)
            12 CALL_FUNCTION          2
            15 GET_ITER
       >>   16 FOR_ITER              24 (to 43)
            19 STORE_FAST             0 (x)
            22 LOAD_FAST              0 (x)
            25 LOAD_CONST             2 (100)
            28 COMPARE_OP             4 (>)
            31 POP_JUMP_IF_FALSE     16
            34 LOAD_FAST              0 (x)
            37 LIST_APPEND            2
            40 JUMP_ABSOLUTE         16
       >>   43 STORE_FAST             1 (l)
            46 LOAD_CONST             0 (None)
            49 RETURN_VALUE

In [28]: dis.dis(loop)
  2          0 BUILD_LIST             0
             3 STORE_FAST             0 (1)

  3          6 SETUP_LOOP            51 (to 60)
             9 LOAD_GLOBAL            0 (range)
            12 LOAD_CONST             1 (1)
            15 LOAD_CONST             2 (100)
            18 CALL_FUNCTION          2
            21 GET_ITER
       >>   22 FOR_ITER              34 (to 59)
            25 STORE_FAST             1 (x)

  4         28 LOAD_FAST              1 (x)
            31 LOAD_CONST             3 (10)
            34 COMPARE_OP             4 (>)
            37 POP_JUMP_IF_FALSE     22

  5         40 LOAD_FAST              0 (l)
            43 LOAD_ATTR              1 (append)
            46 LOAD_FAST              1 (x)
            49 CALL_FUNCTION          1
            52 POP_TOP
            53 JUMP_ABSOLUTE         22
            56 JUMP_ABSOLUTE         22
       >>   59 POP_BLOCK
       >>   60 LOAD_CONST             0 (None)
            63 RETURN_VALUE

In [29]: dis.dis(loop_fast)
  2          0 BUILD_LIST             0
             3 STORE_FAST             0 (1)

  3          6 SETUP_LOOP            38 (to 47)
             9 LOAD_GLOBAL            0 (range)
            12 LOAD_CONST             1 (1)
            15 LOAD_CONST             2 (100)
            18 CALL_FUNCTION          2
            21 GET_ITER
       >>   22 FOR_ITER              21 (to 46)
            25 STORE_FAST             1 (x)

  4         28 LOAD_FAST              1 (x)
            31 LOAD_CONST             3 (10)
            34 COMPARE_OP             4 (>)
            37 POP_JUMP_IF_FALSE     22

  5         40 JUMP_ABSOLUTE         22
            43 JUMP_ABSOLUTE         22
       >>   46 POP_BLOCK
       >>   47 LOAD_CONST             0 (None)
            50 RETURN_VALUE

【讨论】:

    【解决方案2】:

    使用简单的forlist comprehesion 快。它几乎快了 2 倍。检查以下结果:

    使用list comprehension58 微秒

    moin@moin-pc:~$ python -m timeit "[i for i in range(1000)]"
    10000 loops, best of 3: 58 usec per loop
    

    使用for 循环:37.1 微秒

    moin@moin-pc:~$ python -m timeit "for i in range(1000): i"
    10000 loops, best of 3: 37.1 usec per loop
    

    但在您的情况下,for 花费的时间比列表理解更多,不是因为您的 for 循环很慢。但是因为 .append() 你在代码中使用了。

    for 循环中使用append()114 微秒

    moin@moin-pc:~$ python -m timeit "my_list = []" "for i in range(1000): my_list.append(i)"
    10000 loops, best of 3: 114 usec per loop
    

    这清楚地表明 .append() 所花费的时间是 for 循环所用时间的两倍

    然而,在storing the "list.append" in different variable69.3 微秒

    moin@moin-pc:~$ python -m timeit "my_list = []; append = my_list.append" "for i in range(1000): append(i)"
    10000 loops, best of 3: 69.3 usec per loop
    

    在上面的比较中,与最后一种情况相比,性能有了很大的提高,结果与list comprehension相当。这意味着,不是每次都调用my_list.append(),而是可以通过将函数的引用存储在另一个变量(即append_func = my_list.append)中并使用该变量append_func(i)进行调用来提高性能。

    这也证明,调用存储在变量中的类函数比直接使用类的对象调用函数更快

    感谢 Stefan 通知最后一个案例。

    【讨论】:

    • 一个是在大小为 n 的列表上的三个单独的循环,另一个是单个循环。可以通过显示 append 的成本来提出更好的论点。
    • @sdsmith:是的。我应该提供这些见解。更新了答案
    • python -m timeit "my_list = []; append = my_list.append" "for i in range(1000): append(i)" 得到什么?
    • 更新了答案。谢谢你把它放在盘子里。
    • @Moinuddin Quadri。关于@sdsmith 和@Stefan Pochmann cmets,您似乎需要更新答案的第一段,“使用简单的 for 比列表理解更快”。你的第一个比较是错误的。让我们简单地比较可比较的东西:比如python -m timeit "my_list = []; append = my_list.append" "for i in range(1000): append(i)" vs python -m timeit "a=[i for i in range(1000)]; a"
    【解决方案3】:

    让我们打消这个疑问: 第二个版本稍快:列表理解更快,但在一次迭代中会丢弃两个数组循环和尽可能多的条件。

    def kth_order_statistic1(array,k):
        pivot = (array[0] + array[len(array) - 1]) // 2
        l = [x for x in array if x < pivot]
        m = [x for x in array if x == pivot]
        r = [x for x in array if x > pivot]
    
        if k <= len(l):
            return kth_order_statistic1(l, k)
        elif k > len(l) + len(m):
            return kth_order_statistic1(r, k - len(l) - len(m))
        else:
            return m[0]
    
    
    def kth_order_statistic2(array,k):
        pivot = (array[0] + array[len(array) - 1]) // 2
        l = []
        m = []
        r = []
        for x in array:
            if x < pivot:
                l.append(x)
            elif x > pivot:
                r.append(x)
            else:
                m.append(x)
    
        if k <= len(l):
            return kth_order_statistic2(l, k)
        elif k > len(l) + len(m):
            return kth_order_statistic2(r, k - len(l) - len(m))
        else:
            return m[0]
    
    def kth_order_statistic3(array,k):
        pivot = (array[0] + array[len(array) - 1]) // 2
        l = []
        m = []
        r = []
    
        for x in array: 
           if x < pivot: l.append(x)
        for x in array: 
           if x== pivot: m.append(x)
        for x in array: 
           if x > pivot: r.append(x)
    
        if k <= len(l):
            return kth_order_statistic3(l, k)
        elif k > len(l) + len(m):
            return kth_order_statistic3(r, k - len(l) - len(m))
        else:
            return m[0]
    
    import time
    import random
    if __name__ == '__main__':
    
        A = range(100000)
        random.shuffle(A)
        k = random.randint(1, len(A)-1)
    
        start_time = time.time()
        for x in range(1000) :
            kth_order_statistic1(A,k)
        print("--- %s seconds ---" % (time.time() - start_time))
    
        start_time = time.time()
        for x in range(1000) :
            kth_order_statistic2(A,k)
        print("--- %s seconds ---" % (time.time() - start_time))
    
        start_time = time.time()
        for x in range(1000) :
            kth_order_statistic3(A,k)
        print("--- %s seconds ---" % (time.time() - start_time))
    


    python :
    --- 25.8894710541 seconds ---
    --- 24.073086977 seconds ---
    --- 32.9823839664 seconds ---
    
    ipython
    --- 25.7450709343 seconds ---
    --- 22.7140650749 seconds ---
    --- 35.2958850861 seconds ---
    

    时间可能会根据随机抽签而有所不同,但三者之间的差异几乎是一样的。

    【讨论】:

    • 你没有考虑到函数修改了数组,在这种情况下,下面对同一个数组的函数调用会减少功函数。因此,我用随机播放功能测量了时间。但这不会影响您的结果。谢谢回答
    • 什么?不,数组没有写回,它仅用作输入。此外,如果你给函数提供不同的数组,你会得到有偏差的结果,因为工作量也取决于随机分布。
    【解决方案4】:

    算法结构不同,条件结构是有罪的。附加到 r 和 m 的测试可以被先前的测试丢弃。对 for 循环与 append 和列表理解进行更严格的比较将反对非最佳跟随

    for x in array:
            if x < pivot:
                l.append(x)
    for x in array:
            if x== pivot:
                m.append(x)
    for x in array:
            if x > pivot:
                r.append(x)
    

    【讨论】:

      猜你喜欢
      • 2023-03-24
      • 2014-04-02
      • 1970-01-01
      • 2013-07-18
      • 2019-04-15
      • 1970-01-01
      • 2014-01-08
      • 2018-10-04
      相关资源
      最近更新 更多