加速NumPy循环

2 投票
3 回答
1990 浏览
提问于 2025-04-16 05:56

我在用Python运行一个模型,想要加快执行速度。通过分析代码,我发现大部分处理时间都花在了下面的cell_in_shadow函数上。我在想有没有什么办法可以让它更快?

这个函数的目的是判断NumPy数组中指定的单元格是否被另一个单元格遮挡(只在x方向上)。它的做法是从当前行向后检查每个单元格,看看它的高度是否足够低,以至于让指定的单元格处于阴影中。shadow_map中的值是通过另一个没有展示的函数计算出来的——在这个例子中,可以把shadow_map看作是一个包含类似以下值的数组:

[0] = 0 (not used)
[1] = 3
[2] = 7
[3] = 18

为了确保数组的索引可以循环(就像时钟一样),add_x函数被用来处理,因为这个网格有周期性边界(从一边出去的东西会在另一边重新出现)。

def cell_in_shadow(x, y):
   """Returns True if the specified cell is in shadow, False if not."""

   # Get the global variables we need
   global grid
   global shadow_map
   global x_len

   # Record the original length and move to the left
   orig_x = x
   x = add_x(x, -1)

   while x != orig_x:
    # Gets the height that's needed from the shadow_map (the array index is the distance using clock-face arithmetic)
      height_needed = shadow_map[( (x - orig_x) % x_len)]
      if grid[y, x] - grid[y, orig_x] >= height_needed:
          return True

    # Go to the cell to the left
    x = add_x(x, -1)

def add_x(a, b):
   """Adds the two numbers using clockface arithmetic with the x_len"""
   global x_len

   return (a + b) % x_len

3 个回答

1

以下指南比较了几种在Python中优化数值代码的方法:

Scipy PerformancePython

这个指南有点过时,但仍然很有帮助。需要注意的是,它提到了pyrex,而pyrex后来被分支出来,形成了Cython项目,正如Sancho所提到的。

就个人而言,我更喜欢f2py,因为我觉得Fortran 90有很多numpy的好特性(比如可以用一个操作把两个数组相加),而且运行速度快,像编译过的代码一样。不过,如果你不懂Fortran,那可能就不太适合你了。

我简单尝试过Cython,发现它默认生成的代码可以处理各种Python类型,但速度还是很慢。你需要花时间添加所有必要的Cython声明,才能让代码更具体、更快。而如果你选择C或Fortran,通常能直接得到快速的代码。再说了,我对这些语言比较熟悉,所以这个看法可能有偏见。如果你只会Python,Cython可能会更合适。

2

如果你不一定要用严格的Python,我建议你试试Cython。它可以让你给索引加上静态类型,并且能以C语言的速度高效地直接访问numpy数组的底层数据。

你可以看看这个简短的教程/示例,地址是 http://wiki.cython.org/tutorials/numpy

在那个例子中,它做的操作和你很像(比如增加索引,访问numpy数组的单个元素),给索引变量添加类型信息后,运行时间减少了一半。通过给numpy数组添加高效的索引和类型信息,运行时间缩短到了原来的大约1%。

大多数Python代码其实已经是有效的Cython代码,所以你可以直接用现有的代码,只需要在需要的地方添加一些注释和类型信息,就能提高速度。

我觉得你可以通过给索引 xyorig_x 和numpy数组添加类型信息,获得最大的提升。

3

我同意Sancho的看法,Cython可能是个不错的选择,但这里有几个小的提速方法:

A. 在你开始while循环之前,把grid[y, orig_x]存储到一个变量里,然后在循环中使用这个变量。这样可以减少很多对grid数组的查找。

B. 你其实是从shadow_map的x_len - 1开始,一直往下到1,所以可以少用一些取余运算。简单来说,把:

while x != orig_x:
    height_needed = shadow_map[( (x - orig_x) % x_len)]

改成

for i in xrange(x_len-1,0,-1):
    height_needed = shadow_map[i]

或者直接把height_needed这个变量去掉,改成:

    if grid[y, x] - grid[y, orig_x] >= shadow_map[i]:

这些都是小改动,但可能会稍微有点帮助。

另外,如果你打算使用Cython,我建议让你的函数处理整个grid,或者至少一行一行地处理。这样可以减少很多函数调用的开销。不过,这可能要看你是如何使用结果的。

最后,你试过用Psyco吗?它的工作量比Cython少,虽然可能不会给你带来那么大的速度提升。我建议你先试试这个。

撰写回答