【问题标题】:More efficient way to loop?更有效的循环方式?
【发布时间】:2016-04-14 14:00:15
【问题描述】:

我有一小段代码来自一个更大的脚本。我发现当函数t_area 被调用时,它负责大部分的运行时间。我自己测试了这个功能,它并不慢,我相信它需要运行很多次,所以需要很多时间。下面是调用函数的代码:

tri_area = np.zeros((numx,numy),dtype=float)
for jj in range(0,numy-1):
    for ii in range(0,numx-1):
      xp = x[ii,jj]
      yp = y[ii,jj]
      zp = surface[ii,jj]
      ap = np.array((xp,yp,zp))

      xp = xp+dx
      zp = surface[ii+1,jj]
      bp = np.array((xp,yp,zp))

      yp = yp+dx
      zp = surface[ii+1,jj+1]
      dp = np.array((xp,yp,zp))

      xp = xp-dx
      zp = surface[ii,jj+1]
      cp = np.array((xp,yp,zp))

      tri_area[ii,jj] = t_area(ap,bp,cp,dp)

这里使用的数组大小为216 x 217xy 的值也是如此。我对 python 编码很陌生,我过去使用过 MATLAB。所以我的问题是,有没有办法绕过这两个 for 循环,或者有一种更有效的方法来运行这段代码?寻求任何帮助加快这一进程!谢谢!

编辑:

感谢大家的帮助,这解决了很多困惑。我被问到循环中使用的函数 t_area,下面是代码:

def t_area(a,b,c,d):
ab=b-a
ac=c-a
tri_area_a = 0.5*linalg.norm(np.cross(ab,ac))

db=b-d
dc=c-d
tri_area_d = 0.5*linalg.norm(np.cross(db,dc))

ba=a-b
bd=d-b
tri_area_b = 0.5*linalg.norm(np.cross(ba,bd))

ca=a-c
cd=d-c
tri_area_c = 0.5*linalg.norm(np.cross(ca,cd))

av_area = (tri_area_a + tri_area_b + tri_area_c + tri_area_d)*0.5
return(av_area)

对不起,符号混乱,当时它是有道理的,现在回头看我可能会改变它。谢谢!

【问题讨论】:

  • 至少,您不必在每次迭代中分配新数组。
  • 您可以通过将ap,bp,cp,dp 存储在数组中并重写t_area 以迭代这些数组来保存〜40000 次函数调用。此外,也许您可​​以不使用 np.array 来仅存储 3 个值。
  • 你能告诉我们函数t_area()发生了什么吗?是矢量化的吗?
  • 好消息是您的函数可以完全向量化。我已经编辑了我的答案以考虑到这一点。我已经对措辞进行了一些调整,以便将问题向量化放在首位。否则,没有添加太多。
  • 非常感谢!你让我对python结构有了更好的理解!我在完整脚本中还有其他几个地方,这个实现将非常有用!与最终计算的数据相比,现在输入到此脚本中的数据很小。我之前提到的这个数组大小来自一个 ~ 2mb 的文本文件,而稍后来自文本文件的数据将是 ~ 1.5-2 gb!所以这肯定会加快速度!

标签: python performance for-loop vectorization micro-optimization


【解决方案1】:

开始之前的警告。 range(0, numy-1) 等于 range(numy-1),产生从 0 到 numy-2 的数字,不包括 numy-1。那是因为你有从 0 到 numy-2 的 numy-1 值。虽然 MATLAB 具有基于 1 的索引,但 Python 具有基于 0 的索引,因此在转换时要小心索引。考虑到您有tri_area = np.zeros((numx, numy), dtype=float)tri_area[ii,jj] 永远不会以您设置循环的方式访问最后一行或最后一列。因此,我怀疑正确的意图是写range(numy)

由于函数t_area() 是可向量化的,因此您可以完全取消循环。矢量化意味着 numpy 通过处理引擎盖下的循环同时对整个数组应用一些操作,这样它们会更快。

首先,我们为 (m, n, 3) 数组中的每个 (i, j) 元素堆叠所有 aps,其中 (m, n) 是 x 的大小。如果我们取两个 (m, n, 3) 数组的叉积,则默认情况下将在最后一个轴上应用该操作。这意味着np.cross(a, b) 将为每个元素 (i, j) 取 a[i,j]b[i,j] 中的 3 个数字的叉积。同样,np.linalg.norm(a, axis=2) 将为每个元素 (i, j) 计算a[i,j] 中的 3 个数字的范数。这也将有效地将我们的数组减小到大小(m,n)。不过这里要小心一点,因为我们需要明确声明我们希望在第二轴上完成此操作。

请注意,在以下示例中,我的索引关系可能与您的不对应。完成这项工作的最低要求是让surface 拥有来自xy 的额外行和列。

import numpy as np

def _t_area(a, b, c):
    ab = b - a
    ac = c - a
    return 0.5 * np.linalg.norm(np.cross(ab, ac), axis=2)

def t_area(x, y, surface, dx):
    a = np.zeros((x.shape[0], y.shape[0], 3), dtype=float)
    b = np.zeros_like(a)
    c = np.zeros_like(a)
    d = np.zeros_like(a)

    a[...,0] = x
    a[...,1] = y
    a[...,2] = surface[:-1,:-1]

    b[...,0] = x + dx
    b[...,1] = y
    b[...,2] = surface[1:,:-1]

    c[...,0] = x
    c[...,1] = y + dx
    c[...,2] = surface[:-1,1:]

    d[...,0] = bp[...,0]
    d[...,1] = cp[...,1]
    d[...,2] = surface[1:,1:]

    # are you sure you didn't mean 0.25???
    return 0.5 * (_t_area(a, b, c) + _t_area(d, b, c) + _t_area(b, a, d) + _t_area(c, a, d))

nx, ny = 250, 250

dx = np.random.random()
x = np.random.random((nx, ny))
y = np.random.random((nx, ny))
surface = np.random.random((nx+1, ny+1))

tri_area = t_area(x, y, surface, dx)

x 在此示例中支持索引 0-249,而 surface 0-250。 surface[:-1]surface[0:-1] 的简写形式,它将返回从 0 到最后一行的所有行,但不包括它。 -1 在 MATLAB 中提供相同的功能和 end。因此,surface[:-1] 将返回索引 0-249 的行。同样,surface[1:] 将返回索引 1-250 的行,这与您的 surface[ii+1] 相同。


注意:在知道t_area() 可以完全矢量化之前,我已经写了这一部分。因此,虽然这里的内容对于这个答案来说已经过时了,但我会将其保留为遗产,以展示如果该函数不可矢量化,可以进行哪些优化。

您应该传递xy,surfacedx 并在内部进行迭代,而不是为每个元素调用该函数。这意味着只需一次函数调用,开销更少。

此外,您不应在每个循环中为apbpcpdp 创建一个数组,这又会增加开销。在循环外将它们分配一次,然后更新它们的值。

最后一个变化应该是循环的顺序。 Numpy 数组默认是行主要的(而 MATLAB 是列主要的),所以 ii 作为外循环表现更好。您不会注意到您大小的数组的差异,但是,为什么不呢?

总的来说,修改后的函数应该是这样的。

def t_area(x, y, surface, dx):
    # I assume numx == x.shape[0]. If not, pass it as an extra argument.
    tri_area = np.zeros(x.shape, dtype=float)

    ap = np.zeros((3,), dtype=float)
    bp = np.zeros_like(ap)
    cp = np.zeros_like(ap)
    dp = np.zeros_like(ap)

    for ii in range(x.shape[0]-1): # do you really want range(numx-1) or just range(numx)?
        for jj in range(x.shape[1]-1):
            xp = x[ii,jj]
            yp = y[ii,jj]
            zp = surface[ii,jj]
            ap[:] = (xp, yp, zp)

            # get `bp`, `cp` and `dp` in a similar manner and compute `tri_area[ii,jj]`

【讨论】:

    猜你喜欢
    • 2012-05-26
    • 2011-09-25
    • 1970-01-01
    • 2021-11-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-03-18
    相关资源
    最近更新 更多