【问题标题】:List comprehension, map, and numpy.vectorize performance列表理解、映射和 numpy.vectorize 性能
【发布时间】:2011-02-11 19:08:09
【问题描述】:

我有一个函数 foo(i),它需要一个整数并且需要大量时间来执行。以下任何一种初始化a的方式是否会有显着的性能差异:

a = [foo(i) for i in xrange(100)]

a = map(foo, range(100))

vfoo = numpy.vectorize(foo)
a = vfoo(range(100))

(我不在乎输出是列表还是 numpy 数组。)

有没有更好的办法?

【问题讨论】:

  • 为什么不试试计时呢?

标签: python performance numpy list-comprehension


【解决方案1】:

如果函数本身需要大量时间来执行,那么如何将其输出映射到数组就无关紧要了。但是,一旦您开始处理包含数百万个数字的数组,numpy 可以为您节省大量内存。

【讨论】:

  • 同意...大量数据结构在 C 与纯 Python 中处理时速度最快
  • 请注意,使用 numpy.vectorize 实际上并没有像使用真正的 numpy 操作那样有效地将东西移到 C 中。
【解决方案2】:

第一条评论:不要在您的示例中混合使用 xrange() 或 range()...在您比较苹果和橙子时,这样做会使您的问题无效。

我赞同@Gabe 的观点,即如果您有许多大型数据结构,numpy 应该总体上胜出……请记住,大多数时候 C 比 Python 快,但话又说回来,大多数时候,PyPy 更快比 CPython。 :-)

就 listcomps 与 map() 调用而言...一个进行 101 次函数调用,而另一个进行 102 次调用。这意味着您不会看到时间上有显着差异,如下所示使用 @ 987654321@ 模块作为@Mike 建议:

  • 列表理解

    $ python -m timeit "def foo(x):pass; [foo(i) for i in range(100)]"
    1000000 loops, best of 3: 0.216 usec per loop
    $ python -m timeit "def foo(x):pass; [foo(i) for i in range(100)]"
    1000000 loops, best of 3: 0.21 usec per loop
    $ python -m timeit "def foo(x):pass; [foo(i) for i in range(100)]"
    1000000 loops, best of 3: 0.212 usec per loop

  • map()函数调用

    $ python -m timeit "def foo(x):pass; map(foo, range(100))"
    1000000 loops, best of 3: 0.216 usec per loop
    $ python -m timeit "def foo(x):pass; map(foo, range(100))"
    1000000 loops, best of 3: 0.214 usec per loop
    $ python -m timeit "def foo(x):pass; map(foo, range(100))"
    1000000 loops, best of 3: 0.215 usec per loop

话虽如此,除非您打算使用通过这些技术中的任何一种创建的列表,否则请尽量避免使用它们(使用列表)。 IOW,如果您所做的只是对它们进行迭代,那么当您只关心一次查看每个元素时,不值得消耗内存(并且可能在内存中创建一个潜在的庞大列表)只需丢弃列表你已经完成了。

在这种情况下,我强烈建议使用 生成器表达式,因为它们不会在内存中创建整个列表...这是一种更内存友好、惰性迭代的循环方式通过元素进行处理,而无需在内存中创建一个较大的数组。最好的部分是它的语法几乎与 listcomps 相同:

a = (foo(i) for i in range(100))

仅限 2.x 用户:根据更多迭代,将所有旧 2.x 代码的所有 range() 调用更改为 xrange(),然后在移植到时切换到 range() Python 3 其中xrange() 替换并重命名为range()

【讨论】:

  • 请注意,map 需要定义一个函数,但列表推导不需要,这可能是一个好处。与您的代码执行相同操作的现实列表理解使用可能内联fpython -m timeit "def foo(x):pass; [None for i in range(100)]" 在我的机器上给出的结果大约是你使用列表理解时间的 2/3。这实际上是OP想要的吗?我不知道是这样,但它确实表明这些问题是微妙的,并且结论可以更强烈地反映我们如何设计我们的例子而不是实际用途。
  • 不完全。 map() 也不需要函数,如map(None, range(100))。我对性能和 listcomps 与map() 有更长的讨论,但 OP 没有问这个问题,所以我不能在这里回答。我能说的是真正加速listcomps,你必须将该函数简化为一个表达式并使用that(而不是函数)。进行函数调用会导致性能损失,在紧密循环中会被放大。
  • 请记住,C 总是比 Python 快请注意,这实际上并非如此。
  • 我已经改写了那句话中的语言。
【解决方案3】:

列表理解是最快的,然后是地图,然后是我机器上的 numpy。 numpy 代码实际上比其他两个慢很多,但是如果你使用 numpy.arange 而不是 range(或 xrange),就像我在下面列出的时间中所做的那样,差异会小得多。此外,如果您使用 psyco,列表理解会加快,而其他两个对我来说会减慢。我还使用了比您的代码更大的数字数组,而我的 foo 函数只计算了平方根。以下是一些典型的时间。

没有精神科:

list comprehension: 47.5581952455 ms
map: 51.9082732582 ms
numpy.vectorize: 57.9601876775 ms

使用心理治疗:

list comprehension: 30.4318844993 ms
map: 96.4504427239 ms
numpy.vectorize: 99.5858691538 ms

我使用了 Python 2.6.4 和 timeit 模块。

基于这些结果,我想说,您选择哪一个进行初始化可能并没有真正的区别。我可能会根据速度选择 numpy 或列表理解,但最终你应该让你对数组所做的事情指导你的选择。

【讨论】:

  • 嗯,抱歉,来晚了。我已经尝试发布一段时间了,但遇到了错误。
【解决方案4】:
  • 为什么要优化这个?您是否编写了工作、测试过的代码,然后检查了您的算法profiled 您的代码并发现优化它会产生效果?您是否在发现自己花费时间的深层内部循环中这样做?如果没有,请不要打扰。

  • 您只有通过计时才能知道哪个对您最有效。要以有用的方式计时,您必须将其专门用于您的实际用例。例如,列表推导式中的函数调用与内联表达式中的函数调用之间存在明显的性能差异;目前尚不清楚您是否真的想要前者,或者您是否将其简化为使您的案例相似。

  • 您说最终得到一个 numpy 数组还是一个 list 并不重要,但如果您正在进行这种微优化,它确实很重要,因为当您之后使用它们时,它们的性能会有所不同。解决这个问题可能会很棘手,所以希望它会证明整个问题还为时过早。

  • 为了清晰、易读等,最好简单地使用正确的工具来完成工作。我很难在这些事情之间做出决定。

    • 如果我需要 numpy 数组,我会使用它们。我将使用它们来存储大型同构数组或多维数据。我经常使用它们,但很少在我认为我想使用列表的地方使用它们。
      • 如果我使用这些,我会尽我所能编写我的函数已经矢量化,这样我就不必使用numpy.vectorize。例如,下面的 times_five 可以用于没有修饰的 numpy 数组。
    • 如果我没有理由使用 numpy,也就是说,如果我没有解决数值数学问题或使用特殊的 numpy 功能或存储多维数组或其他什么...
      • 如果我有一个已经存在的函数,我会使用map。这就是它的用途。
      • 如果我有一个适合小表达式的操作并且我不需要函数,我会使用列表推导式。
      • 如果我只想对所有情况进行操作,但实际上不需要存储结果,我会使用普通的 for 循环。
      • 在许多情况下,我实际上会使用map 并列出推导式的惰性等效项:itertools.imap 和生成器表达式。在某些情况下,这些可以将内存使用量减少 n 倍,并且有时可以避免执行不必要的操作。

如果事实证明这就是性能问题所在,那么正确处理这类事情是很棘手的。 非常人们经常为他们的实际问题计时错误的玩具箱。更糟糕的是,非常普通的人会根据它制定愚蠢的通用规则。

考虑以下情况(timeme.py 贴在下面)

python -m timeit "from timeme import x, times_five; from numpy import vectorize" "vectorize(times_five)(x)"
1000 loops, best of 3: 924 usec per loop

python -m timeit "from timeme import x, times_five" "[times_five(item) for item in x]"
1000 loops, best of 3: 510 usec per loop

python -m timeit "from timeme import x, times_five" "map(times_five, x)"
1000 loops, best of 3: 484 usec per loop

一个天真的观察者会得出结论,地图是这些选项中表现最好的,但答案仍然是“视情况而定”。考虑使用您正在使用的工具的优势:列表推导式让您避免定义简单的函数;如果您做正确的事情,numpy 可以让您在 C 中对事物进行矢量化。

python -m timeit "from timeme import x, times_five" "[item + item + item + item + item for item in x]"
1000 loops, best of 3: 285 usec per loop

python -m timeit "import numpy; x = numpy.arange(1000)" "x + x + x + x + x"
10000 loops, best of 3: 39.5 usec per loop

但这还不是全部——还有更多。考虑算法更改的力量。它可以更具戏剧性。

python -m timeit "from timeme import x, times_five" "[5 * item for item in x]"
10000 loops, best of 3: 147 usec per loop

python -m timeit "import numpy; x = numpy.arange(1000)" "5 * x"
100000 loops, best of 3: 16.6 usec per loop

有时,算法更改可能会更有效。随着数字越来越大,这将越来越有效。

python -m timeit "from timeme import square, x" "map(square, x)"
10 loops, best of 3: 41.8 msec per loop

python -m timeit "from timeme import good_square, x" "map(good_square, x)"
1000 loops, best of 3: 370 usec per loop

即使是现在,这一切也可能对您的实际问题影响不大。如果你能正确使用 numpy,它看起来很棒,但它有其局限性:这些 numpy 示例都没有使用数组中的实际 Python 对象。这使必须做的事情变得复杂;甚至很多。如果我们确实可以使用 C 数据类型呢?这些不如 Python 对象健壮。它们不可为空。整数溢出。你必须做一些额外的工作来检索它们。它们是静态类型的。有时这些事情被证明是问题,甚至是意想不到的问题。

所以你去:一个明确的答案。 “这取决于。”


# timeme.py

x = xrange(1000)

def times_five(a):
    return a + a + a + a + a

def square(a):
    if a == 0:
        return 0

    value = a
    for i in xrange(a - 1):
        value += a
    return value

def good_square(a):
    return a ** 2

【讨论】:

    猜你喜欢
    • 2012-03-30
    • 1970-01-01
    • 2017-01-20
    • 2018-12-03
    • 2011-02-28
    • 2015-08-03
    • 1970-01-01
    • 2010-11-21
    相关资源
    最近更新 更多