【问题标题】:How to shove this loop into numpy?如何将此循环推入numpy?
【发布时间】:2014-03-15 16:54:11
【问题描述】:

我有一个慢循环,我想通过将它推入 numpy.我花了几天时间玩这个代码却没有得到任何地方。甚至有可能,还是我缺少一个麻木的把戏?我可以做一些重构来提供帮助吗?

如您所见,我想将 mixin 相加为 xs 的偏移量。

import numpy as np

blocksize = 1000 # Chosen at runtime.
mixinsize = 100 # Chosen at runtime.
count = 10000 # Chosen at runtime.
xs = np.random.randint(0, blocksize + 1, count) # In practice this is data.
mixins = np.empty((count, mixinsize)) # In practice this is data.

# The slow part:
accumulator = np.zeros(blocksize + mixinsize)
for i in xrange(count):
    accumulator[xs[i]:xs[i] + mixinsize] += mixins[i]

对已接受答案的评论

  • 要让 numba 工作,我首先必须确保我在任何地方都使用了适当的 numpy 类型,当然不是普通的 python 类型。这本身就是一个巨大的性能提升。
  • Numba 改进了特定情况下的速度,因为计数变大了,这正是我想要的(也许我应该在问题中强调这一点)。
  • 在我当前的测试用例中,不同的 numba 版本有明显不同的时间安排,但我认为 numba 是一个很有潜力的好主意,而且它正在积极开发中,所以我会坚持下去。
  • 使用 anaconda 安装 numba,特别是它可以轻松地在 numba 版本之间切换。

【问题讨论】:

  • 你能举一个例子,说明数据的前几个元素和期望的结果吗?
  • 我看不出有明显的方法来做你想做的事。您应该使用 Cython 获得更好的结果,并且您可以在一个下午编写 cython 解决方案。
  • @wwii 您可以摆弄 blocksize/mixinsize/count 并在上述约束范围内提出 xs/mixins,然后所需的结果是“慢速部分”计算的累加器。我自己已经这样做了,但我认为这些数字更多的是分散注意力而不是有用,所以我没有将它们添加到问题中。我对任何替代“慢速部分”的解决方案感兴趣,并且给定相同的 xs/mixins 返回相同的累加器。
  • @jb。谢谢,Cython 看起来不错,如果没有人能想出一个 numpy 解决方案,我会调查它。我对 numpy 解决方案特别感兴趣,因为我的项目的目标之一是将 python/numpy 推向极限。

标签: python performance loops for-loop numpy


【解决方案1】:

从 numba.pydata.org 获取 Numba 0.11(还不是 0.12)。现在我们可以用 LLVM jit 编译这段代码了:

# plain NumPy version
import numpy as np

def foobar(mixinsize, count, xs, mixins, acc):
    for i in xrange(count):
        k = xs[i]
        acc[k:k + mixinsize] += mixins[i,:]


# LLVM compiled version
from numba import jit, void, int64, double
signature = void(int64,int64,int64[:],double[:,:],double[:])
foobar_jit = jit(signature)(foobar)

if __name__ == "__main__":   
    from time import clock

    blocksize = 1000 # Chosen at runtime.
    mixinsize = 100 # Chosen at runtime.
    count = 100000 # Chosen at runtime. 
    xs = np.random.randint(0, blocksize + 1, count)
    mixins = np.empty((count, mixinsize))
    acc = np.zeros(blocksize + mixinsize)

    t0 = clock()
    foobar(mixinsize, count, xs, mixins, acc)
    t1 = clock()
    print("elapsed time: %g ms" % (1000*(t1-t0),))

    t2 = clock()
    foobar_jit(mixinsize, count, xs, mixins, acc)
    t3 = clock()
    print("elapsed time with numba jit: %g ms" % (1000*(t3-t2),))

    print("speedup factor: %g" % ((t1-t0)/(t3-t2),))

$ python test_numba.py
elapsed time: 590.632 ms
elapsed time with numba jit: 12.31 ms
speedup factor: 47.9799

好的,只需增加三行 Python 代码,速度就几乎提高了 50 倍。

现在我们还可以使用 clang/LLVM 作为编译器来测试一个普通的 C 版本进行比较。


void foobar(long mixinsize, long count, 
    long *xs, double *mixins, double *accumulator)
{
    long i, j, k;
    double *cur, *acc;   
    for (i=0;i<count;i++) {
        acc = accumulator + xs[i];
        cur = mixins + i*mixinsize;
        for(j=0;j<mixinsize;j++) *acc++ += *cur++;
    }
}

from numpy.ctypeslib import ndpointer
import ctypes
so = ctypes.CDLL('plainc.so')
foobar_c = so.foobar
foobar_c.restype = None
foobar_c.argtypes = (
    ctypes.c_long,
    ctypes.c_long,
    ndpointer(dtype=np.int64, ndim=1),
    ndpointer(dtype=np.float64, ndim=2),
    ndpointer(dtype=np.float64, ndim=1)
)


t4 = clock()
foobar_c(mixinsize, count, xs, mixins, acc)
t5 = clock()
print("elapsed time with plain C: %g ms" % (1000*(t5-t4),))

$ CC -Ofast -shared -m64 -o plainc.so plainc.c
$ python test_numba.py
elapsed time: 599.136 ms
elapsed time with numba jit: 11.958 ms
speedup factor: 50.1034
elapsed time with plain C: 5.472 ms

因此,当使用 -Ofast 进行优化时,Numba 的速度大约是普通 C 版本的一半。相比之下,使用 -O2 的运行时间约为 8 毫秒。这意味着在这种情况下 Numba JIT 编译 Python 的速度大约是带有 -O2 优化标志的 C 的 75%。这对于仅仅增加三行 Python 代码来说还不错。

我们可以看一个普通的 Python 版本进行比较:

def foobar_py(mixinsize, count, xs, mixins, acc):
    for i in xrange(count):
        k = xs[i]
        for j in xrange(mixinsize):
            acc[j+k] += mixins[i][j]


# covert NumPy arrays to lists
_xs = map(int,xs)
_mixins = [map(float,mixins[i,:]) for i in xrange(count)]
_acc = map(float,acc)


t6 = clock()
foobar_py(mixinsize, count, _xs, _mixins, _acc)
t7 = clock()
print("elapsed time with plain Python: %g ms" % (1000*(t7-t6),))

此 Python 代码在 1775 毫秒内执行。因此,相对于普通 Python,我们可以使用 NumPy 获得大约 3 倍的加速,使用 Numba 可以获得 150 倍的加速,使用 C 和 -Ofast 可以获得 350 倍的加速。

警告来自 Donald Knuth,他将此归因于 CAR Hoare:“过早的优化是计算机编程中万恶之源。”要获得令人印象深刻的相对加速,沿着这条路线走下去的绝对加速只能让我们节省几毫秒的 CPU 时间。 我的时间真的值得从大量的劳动力中节省 CPU 吗?值得您的时间吗?自己决定。

【讨论】:

  • 谢谢,我对 Numba 方法感到兴奋,下次有机会时会对其进行评估。提醒一下,我想优化这个特定的循环,因为它出现在 cProfile 的顶部,因此随着计数的增加,用户可见速度变慢的可能原因。所以这可能值得你花时间。
  • 用户可见的缓慢是优化的正当理由,只是想节省几毫秒不是。不幸的是,大多数抱怨 Python 的“缓慢”的人都想要后者。但重要的是要意识到几毫秒低于人类感知的极限,在 UI 事件处理程序的情况下。对于程序的其他部分,使用 C 永远不会为我们提供更快的磁盘或更快的网络连接。所以实际上分析代码总是一个好主意。 :-)
猜你喜欢
  • 2013-09-28
  • 1970-01-01
  • 2023-04-01
  • 2020-09-18
  • 2014-06-23
  • 1970-01-01
  • 1970-01-01
  • 2013-07-24
  • 2021-02-17
相关资源
最近更新 更多