【问题标题】:Faster way of generating a sequence of numbers生成数字序列的更快方法
【发布时间】:2020-06-15 22:42:30
【问题描述】:

假设我要生成以下递归序列:

与以下封闭形式的序列相同:

但目标是编写一个函数来生成具有递归定义的序列。所以,以下是我的功能:

def gen_seq(n):
    '''
    n is the number of terms to generate in the sequence
    '''
    lis = [1] # starting number of the sequence
    for i in range(1,n):
        lis.append(lis[-1] + 2*(i+1) - 1)
    return lis

我们可以看一下序列的前 10 个词:

gen_seq(10)
>>> [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

如果我们查看生成序列中前 1000 万个数字所花费的时间:

import time
t = time.time()
foo = gen_seq(10000000)
print('Time taken: {}s'.format(time.time()-t))

>>> Time taken: 6.146637201309204s

需要 6.1 秒。对于 1 亿个数字,所用时间为 1 分钟。序列越复杂,所花费的时间就会越多。

如何优化此功能以使其更快?

【问题讨论】:

  • 你可能想看看像 numba 这样的编译器:numba.pydata.org
  • 从 1000 万到 10000 万个元素的泛化通常是不正确的,因为它取决于底层列表调整大小的长度,因此需要分配新内存和复制旧元素。如果您事先知道大小,最好先创建列表(例如[None] * n),然后再填写(这也适用于递归问题)。当然,您也可以使用生成器。
  • 你真的需要一次存储所有 100,000,000 个数字吗?
  • @chepner 实际上我正在生成 1000 万个具有 2 个不同起始数字的序列,然后分析这两个序列之间的差异。所以,我并没有过多考虑记忆和所有这些。无论如何,即使我不将单个序列存储在内存中,我也至少需要将序列的差异存储在内存中。
  • @SergeBallesta 列表理解在 C 中类似地循环,因此仅就循环而言,与 numpy 没有区别。加速来自 Python 中结果列表中的每个数字都由一个对象表示,因此会带来一些开销,而 numpy 使用它自己的数据类型 (np.int64) 并只存储数字。比较np.arange(100_000, dtype=object),加速就没有了。

标签: python python-3.x performance loops optimization


【解决方案1】:

如果封闭形式可用,您可以使用列表推导式。这样做的好处是可以在开始时就分配所有需要的内存:

seq = [(i + 1)**2 for i in range(1, n+1)]

重复附加到列表会导致底层内存的内部调整大小,因此涉及不必要的内存分配和复制。

没有封闭形式,您仍然可以使用生成器:

def gen_seq(n):
    a = 0
    for i in range(1, n+1):
        a += 2*i - 1
        yield a

性能对比

In [1]: def f1(n): 
   ...:     return [(i + 1)**2 for i in range(1, n+1)] 
   ...:                                                                                       

In [2]: def gen_seq(n): 
   ...:     a = 0 
   ...:     for i in range(1, n+1): 
   ...:         a += 2*i - 1 
   ...:         yield a 
   ...:                                                                                       

In [3]: def f2(n): 
   ...:     return list(gen_seq(n)) 
   ...:                                                                                       

In [4]: %timeit f1(100_000)                                                                   
29.7 ms ± 271 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

In [5]: %timeit f2(100_000)                                                                   
16.1 ms ± 176 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

生成器版本的速度几乎是列表推导式的 2 倍。这是因为递归版本受益于所涉及的相对简单的操作。将一个整数乘以2 只是一个 1-lshift 并且添加或减去一个数字是一个 O(N) 操作,其中 N 是位数。然而,将两个整数相乘是 O(N*log(N)),因此需要更多时间来计算。递归版本受益于已经计算的部分a_{n-1},它可以在每个步骤中重复使用。

【讨论】:

  • 好吧,我不想在这个例子中使用复杂的序列。这就是为什么我使用了我能想到的最简单的递归序列。现在我将序列的示例修改为稍微复杂一点的示例。这不能由简单的范围生成。目标是优化 for 循环。
【解决方案2】:

列表的追加功能让我相信我们可以做得更快,让我们尝试使用生成器:

def seq(n,i=1):
    while i < n+1:
        yield i
        i+=1

import time
t = time.time()
foo = list(seq(10000000))
print('Time taken: {}s'.format(time.time()-t))

对我来说快了一秒多。我认为它可以进一步改进。

【讨论】:

  • 内置的range 已经允许创建这样的序列。无需定义单独的助手。
【解决方案3】:

特别是在这种情况下,您可以使用更快的list(range(1, n))

import time
t = time.time()
print(list(range(1, 1_000_000)))
print(time.time() - t)

不到 0.5 秒。

【讨论】:

  • 好吧,我不想在这个例子中使用复杂的序列。这就是为什么我使用了我能想到的最简单的递归序列。现在我将序列的示例修改为稍微复杂一点的示例。这个不能由简单的范围生成。目标是优化 for 循环。
猜你喜欢
  • 2019-11-20
  • 1970-01-01
  • 2015-07-17
  • 2023-03-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多