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

4 投票
5 回答
2365 浏览
提问于 2025-04-15 20:40

问题 大家好。我想请教一下关于Python性能方面的建议。以下是我问题的一些背景信息:

给定:

  1. 一个 (x,y) 的节点网格,每个节点的值在 (0...255) 之间,初始值为0
  2. 一组 N 个输入坐标,每个坐标在范围 (0...x, 0...y)
  3. 一个值 Z,定义了“邻域”的节点数量

在输入坐标的节点及其邻居节点上增加值。超出网格边界的邻居会被忽略。(不进行循环处理)

基本情况: 一个大小为 1024x1024 的节点网格,400 个输入坐标,邻域范围 Z75 个节点。

处理时间应该是 O(x*y*Z*N)。我希望 x、y 和 Z 的值大致保持在基本情况的范围内,但输入坐标 N 的数量可能会增加到10万。我的目标 是尽量缩短处理时间。

当前结果 从我开始到下面的评论中,我们有几个实现方案。

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

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

f1 是最初的简单实现:三个嵌套的 for 循环。
f2 用列表推导式替换了内层的 for 循环。
f3 是基于Andrei在评论中的建议,用 map() 替换了外层的 for 循环。
f 是Chris在下面回答中的建议。
f3b 是kriss对 f3 的改进。
f4 是Alex的贡献。

代码在下面供你参考。

问题 我该如何进一步减少处理时间?我希望测试参数的处理时间能低于1.0秒。

保持建议使用原生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 在评论中的建议,用 map() 替换了外层 for 循环。我的第一次尝试需要进行几次超出局部范围的查找,这在Guido的建议中是被反对的:局部变量查找比全局或内置变量查找快得多。我将除了对主数据结构的引用以外的所有内容都硬编码,以减少这种开销。

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]]

更新3: 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]]

更新4: krissf3 进行了几处改进,用新的三元运算符语法替换了最小值/最大值。

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]]

更新5: 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 个回答

1

根据你提到的f3版本,我对代码做了一些调整。因为l和r是常量,所以在g1循环中可以不需要每次都计算它们。而且用新的三元运算符代替最小值和最大值的计算,似乎速度更快。此外,我还简化了与左上角相关的表达式。在我的系统上,使用下面的代码速度大约快了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]]
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]]

编辑:用 timeit 测试了一下(跑了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
2

在我这台(有点慢的)第一天使用的Macbook Air上,配置是1.6GHz的Core 2 Duo,系统是MacOSX 10.5,Python版本是2.5。在我把你的代码保存为op.py后,得到了以下的运行时间:

$ 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许可证或类似的条件下重新发布这段代码;-)。

撰写回答