Python性能:嵌套列表上的迭代和操作

时间:2010-03-21 20:49:03

标签: performance python

问题嘿伙计们。我正在寻找有关python性能的一些建议。关于我的问题的一些背景:

鉴于:

  1. (x,y)个节点网格,每个网格的值(0...255)从0
  2. 开始
  3. N
  4. 范围内指定位置的(0...x, 0...y)输入坐标列表
  5. 定义节点数“邻域”的值Z
  6. 增加输入坐标和节点邻居的节点值。网格边缘之外的邻居将被忽略。 (没有包装)

    BASE CASE:一个大小为1024x1024个节点的网格,其中400个输入坐标和Z75个节点。

    处理应为O(x*y*Z*N)。我希望x,y和Z大致保持在基本情况下的值附近,但输入坐标N的数量可以增加到100,000。 我的目标是最大限度地缩短处理时间。

    当前结果在我的开始和下面的评论之间,我们有几个实现。

    使用Python 2.6.1在我的2.26 GHz Intel Core 2 Duo上运行速度:

      f1: 2.819s
      f2: 1.567s
      f3: 1.593s
       f: 1.579s
     f3b: 1.526s
      f4: 0.978s
    

    f1是最初的天真实现:三个嵌套的for循环。 f2将使用列表解析替换内部for循环。 f3基于Andrei在评论中的建议,并将for替换为map() f是克里斯在以下答案中的建议 f3b是kriss对f3的看法 f4是Alex的贡献。

    下面包含代码供您阅读。

    问题如何进一步缩短处理时间?我更喜欢低于1.0s的测试参数。

    将建议保留给原生Python。我知道我可以转到第三方软件包,例如numpy,但我正在尝试避免任何第三方软件包。此外,我已生成随机输入坐标,并简化了节点值更新的定义,以使我们的讨论简单。具体细节必须略有改变,不在我的问题范围内。

    非常感谢!

    <小时/> f1是最初的天真实现:三个嵌套for循环。

    def f1(x,y,n,z):
        rows = [[0]*x for i in xrange(y)]
    
        for i in range(n):
            inputX, inputY = (int(x*random.random()), int(y*random.random()))
            topleft = (inputX - z, inputY - z)
            for i in xrange(max(0, topleft[0]), min(topleft[0]+(z*2), x)):
                for j in xrange(max(0, topleft[1]), min(topleft[1]+(z*2), y)):
                    if rows[i][j] <= 255: rows[i][j] += 1
    

    f2将使用列表推导替换内部for循环。

    def f2(x,y,n,z):
        rows = [[0]*x for i in xrange(y)]
    
        for i in range(n):
            inputX, inputY = (int(x*random.random()), int(y*random.random()))
            topleft = (inputX - z, inputY - z)
            for i in xrange(max(0, topleft[0]), min(topleft[0]+(z*2), x)):
                l = max(0, topleft[1])
                r = min(topleft[1]+(z*2), y)
                rows[i][l:r] = [j+(j<255) for j in rows[i][l:r]]
    

    更新: f3基于评论中的Andrei建议,并将for替换为map()。我对此的第一次攻击需要几个超出本地范围的查找,特别是Guido的recommended against局部变量查找比全局或内置变量查找快得多我硬编码除了对主数据结构本身的引用,以最小化开销。

    rows = [[0]*x for i in xrange(y)]
    
    def f3(x,y,n,z):
        inputs = [(int(x*random.random()), int(y*random.random())) for i in range(n)]
        rows = map(g, inputs)
    
    def g(input):
        inputX, inputY = input
        topleft = (inputX - 75, inputY - 75)
        for i in xrange(max(0, topleft[0]), min(topleft[0]+(75*2), 1024)):
            l = max(0, topleft[1])
            r = min(topleft[1]+(75*2), 1024)
            rows[i][l:r] = [j+(j<255) for j in rows[i][l:r]]
    

    UPDATE3: ChristopeD也指出了一些改进。

    def f(x,y,n,z):
        rows = [[0] * y for i in xrange(x)]
        rn = random.random
        for i in xrange(n):
            topleft = (int(x*rn()) - z, int(y*rn()) - z)
            l = max(0, topleft[1])
            r = min(topleft[1]+(z*2), y)
            for u in xrange(max(0, topleft[0]), min(topleft[0]+(z*2), x)):
                rows[u][l:r] = [j+(j<255) for j in rows[u][l:r]]
    

    UPDATE4: krissf3添加了一些改进,用新的三元运算符语法替换了min / max。

    def f3b(x,y,n,z):
        rn = random.random    
        rows = [g1(x, y, z) for x, y in [(int(x*rn()), int(y*rn())) for i in xrange(n)]]
    
    def g1(x, y, z):
        l = y - z if y - z > 0 else 0
        r = y + z if y + z < 1024 else 1024
        for i in xrange(x - z if x - z > 0 else 0, x + z if x + z < 1024 else 1024 ):
            rows[i][l:r] = [j+(j<255) for j in rows[i][l:r]]
    

    UPDATE5: Alex进行了实质性修订,添加了一个单独的map()操作,将值限制为255并删除所有非本地范围的查找。性能差异非常重要。

    def f4(x,y,n,z):
        rows = [[0]*y for i in range(x)]
        rr = random.randrange
        inc = (1).__add__
        sat = (0xff).__and__
    
        for i in range(n):
            inputX, inputY = rr(x), rr(y)
            b = max(0, inputX - z)
            t = min(inputX + z, x)
            l = max(0, inputY - z)
            r = min(inputY + z, y)
            for i in range(b, t):
                rows[i][l:r] = map(inc, rows[i][l:r])
        for i in range(x):
          rows[i] = map(sat, rows[i])
    

    此外,由于我们似乎都在讨论各种变化,这是我的测试工具来比较速度:(由ChristopheD改进)

    def timing(f,x,y,z,n):
        fn = "%s(%d,%d,%d,%d)" % (f.__name__, x, y, z, n)
        ctx = "from __main__ import %s" % f.__name__ 
        results = timeit.Timer(fn, ctx).timeit(10)
        return "%4.4s: %.3f" % (f.__name__, results / 10.0)
    
    if __name__ == "__main__":
        print timing(f, 1024, 1024, 400, 75)
        #add more here.
    

5 个答案:

答案 0 :(得分:2)

<强> 1 即可。 (较小的)加速绝对可以是rows ...

的初始化

替换

rows = []
for i in range(x):
    rows.append([0 for i in xrange(y)])

rows = [[0] * y for i in xrange(x)]

<强> 2 即可。您还可以通过将random.random移出循环(稍微保存一下)来避免某些查找。

3。编辑:经过更正后 - 你可以得到类似的结果:

def f(x,y,n,z):
    rows = [[0] * y for i in xrange(x)]
    rn = random.random
    for i in xrange(n):
        topleft = (int(x*rn()) - z, int(y*rn()) - z)
        l = max(0, topleft[1])
        r = min(topleft[1]+(z*2), y)
        for u in xrange(max(0, topleft[0]), min(topleft[0]+(z*2), x)):
            rows[u][l:r] = [j+(j<255) for j in rows[u][l:r]]

编辑:一些带时间的新时间(10次运行) - 似乎只能提供较小的加速:

import timeit
print timeit.Timer("f1(1024,1024,400,75)", "from __main__ import f1").timeit(10)
print timeit.Timer("f2(1024,1024,400,75)", "from __main__ import f2").timeit(10)
print timeit.Timer("f(1024,1024,400,75)", "from __main__ import f3").timeit(10)
f1 21.1669280529
f2 12.9376120567
f  11.1249599457

答案 1 :(得分:2)

op.py中保存代码后,在我的(慢速;-)第一天的Macbook Air,1.6GHz Core 2 Duo,MacOSX 10.5上的系统Python 2.5中,我看到以下时间:

$ python -mtimeit -s'import op' 'op.f1()'
10 loops, best of 3: 5.58 sec per loop
$ python -mtimeit -s'import op' 'op.f2()'
10 loops, best of 3: 3.15 sec per loop

所以,我的机器比你的机器慢了1.9倍。

我执行此任务的最快代码是:

def f3(x=x,y=y,n=n,z=z):
    rows = [[0]*y for i in range(x)]
    rr = random.randrange
    inc = (1).__add__
    sat = (0xff).__and__

    for i in range(n):
        inputX, inputY = rr(x), rr(y)
        b = max(0, inputX - z)
        t = min(inputX + z, x)
        l = max(0, inputY - z)
        r = min(inputY + z, y)
        for i in range(b, t):
            rows[i][l:r] = map(inc, rows[i][l:r])
    for i in range(x):
      rows[i] = map(sat, rows[i])

其中时间为:

$ python -mtimeit -s'import op' 'op.f3()'
10 loops, best of 3: 3 sec per loop

所以,一个非常适度的加速,在您的机器上投射超过1.5秒 - 远高于您的目标1.0: - (。

使用简单的C编码扩展名exte.c ...:

#include "Python.h"

static PyObject*
dopoint(PyObject* self, PyObject* args)
{
    int x, y, z, px, py;
    int b, t, l, r;
    int i, j;
    PyObject* rows;

    if(!PyArg_ParseTuple(args, "iiiiiO",
                         &x, &y, &z, &px, &py, &rows
        ))
        return 0;

    b = px - z;
    if (b < 0) b = 0;
    t = px + z;
    if (t > x) t = x;
    l = py - z;
    if (l < 0) l = 0;
    r = py + z;
    if (r > y) r = y;

    for(i = b; i < t; ++i) {
        PyObject* row = PyList_GetItem(rows, i);
        for(j = l; j < r; ++j) {
            PyObject* pyitem = PyList_GetItem(row, j);
            long item = PyInt_AsLong(pyitem);
            if (item < 255) {
                PyObject* newitem = PyInt_FromLong(item + 1);
                PyList_SetItem(row, j, newitem);
            }
        }
    }

    Py_RETURN_NONE;
}

static PyMethodDef exteMethods[] = {
    {"dopoint", dopoint, METH_VARARGS, "process a point"},
    {0}
};

void
initexte()
{
    Py_InitModule("exte", exteMethods);
}

(注意:我没有仔细检查过 - 我认为由于参考窃取和借用的正确相互作用,它不会泄漏内存,但在投入生产之前应该仔细检查代码;-) ,我们可以做到

import exte
def f4(x=x,y=y,n=n,z=z):
    rows = [[0]*y for i in range(x)]
    rr = random.randrange

    for i in range(n):
        inputX, inputY = rr(x), rr(y)
        exte.dopoint(x, y, z, inputX, inputY, rows)

和时间

$ python -mtimeit -s'import op' 'op.f4()'
10 loops, best of 3: 345 msec per loop

显示加速度为8-9次,这应该会让你进入你想要的球场。我看过一条评论说你不想要任何第三方扩展,但是,这个微小的扩展你可以完全属于你自己;-)。 ((不确定哪些许可条件适用于Stack Overflow上的代码,但我很乐意在Apache 2许可证等下重新发布,如果您需要; - ))。

答案 2 :(得分:1)

在你的f3重写中,g可以简化。 (也可以应用于f4)

你在for循环中有以下代码。

l = max(0, topleft[1])
r = min(topleft[1]+(75*2), 1024)

但是,似乎这些值在for循环中永远不会改变。因此,在循环之外计算它们一次。

答案 3 :(得分:1)

根据您的f3版本,我使用了代码。由于l和r是常量,因此可以避免在g1循环中计算它们。同样使用新的三元组而不是最小值和最大值似乎一直更快。也用topleft简化了表达式。在我的系统上,使用下面的代码看起来速度提高了大约20%。

def f3b(x,y,n,z):
    rows = [g1(x, y, z) for x, y in [(int(x*random.random()), int(y*random.random())) for i in range(n)]]

def g1(x, y, z):
    l = y - z if y - z > 0 else 0
    r = y + z if y + z < 1024 else 1024
    for i in xrange(x - z if x - z > 0 else 0, x + z if x + z < 1024 else 1024 ):
        rows[i][l:r] = [j+(j<255) for j in rows[i][l:r]]

答案 4 :(得分:0)

您可以在C中创建自己的Python模块,并根据需要控制性能: http://docs.python.org/extending/