TLDR我正在执行一个数组运算(没有数学运算),我发现Cython要快得多。有没有一种方法可以在NumPy中加快速度;还是Cython?
上下文
我正在编写一个函数,该函数旨在将NxN
数组的一个子集从index
向两个方向(其上角沿对角线)向前移动,并沿对角线向上移动一个位置。其次,我需要将最上面的一行从index
向前向左移动一个位置。最后,我需要在操作之后将数组中的最后一列设置为零。
该数组是一个严格上三角矩阵,意味着从对角线向下的所有元素都设置为0。这是我尝试用一种优雅的方式来存储对象对之间的历史冲突数据(其索引由矩阵中的索引表示)。这将类似于制作大小为n!/(2(n-2)!)
的嵌套列表,其表示长度为n
的索引列表的有序对。在这个算法中;删除";来自冲突配对矩阵的对象。
我在这种实现方式中发现的优点是;移除碰撞对";与从嵌套列表中移除对并将成对索引移位超过"0"相比,从矩阵中移除对的计算密集度要低得多;要删除的索引";指向
整个项目围绕着自动化的";包装";将3D模型转化为用于粉末床融合增材制造的构建体积。该算法使用模拟退火,因此修剪冲突集、存储历史信息、添加/删除几何体的能力是最重要的,需要进行很好的优化。
示例
假设我们的数组采用这种形式(不代表实际数据)。
arr =
[[0. 1. 2. 3. 4. 5. 6. 7. 8. 9.]
[0. 0. 2. 3. 4. 5. 6. 7. 8. 9.]
[0. 0. 0. 3. 4. 5. 6. 7. 8. 9.]
[0. 0. 0. 0. 4. 5. 6. 7. 8. 9.]
[0. 0. 0. 0. 0. 5. 6. 7. 8. 9.]
[0. 0. 0. 0. 0. 0. 6. 7. 8. 9.]
[0. 0. 0. 0. 0. 0. 0. 7. 8. 9.]
[0. 0. 0. 0. 0. 0. 0. 0. 8. 9.]
[0. 0. 0. 0. 0. 0. 0. 0. 0. 9.]
[0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]]
然后使用index = 3
,我们应该取子集index+1:n, index+1:n
中的所有内容,并将其设置为等于index:n-1, index:n-1
。然后将最上面一行向左移动;再次从CCD_ 9开始。然后将最后一列设置为0。
fun(3, arr)
[[0. 1. 2. 4. 5. 6. 7. 8. 9. 0.]
[0. 0. 2. 3. 4. 5. 6. 7. 8. 0.]
[0. 0. 0. 3. 4. 5. 6. 7. 8. 0.]
[0. 0. 0. 0. 5. 6. 7. 8. 9. 0.]
[0. 0. 0. 0. 0. 6. 7. 8. 9. 0.]
[0. 0. 0. 0. 0. 0. 7. 8. 9. 0.]
[0. 0. 0. 0. 0. 0. 0. 8. 9. 0.]
[0. 0. 0. 0. 0. 0. 0. 0. 9. 0.]
[0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]]
实现1:Pure NumPy
再次假设CCD_ 10是CCD_。
def fun(index, n, arr):
arr[index:-1, index:-1] = arr[index + 1:, index + 1:]
arr[0, index:-1] = arr[0, index + 1:]
arr[:, n-1:] = 0
return arr
实现2:Cython
请耐心等待,因为这是我第一次实现Cython。
@cython.boundscheck(False)
def remove_from_collision_array(int index, int n, double[:,:] arr):
cdef int i, j, x_shape, y_shape
x_shape = arr.shape[0]
for i in range(index, x_shape):
for j in range(index, x_shape):
if j <= i:
# We are below the diagonal, do nothing
continue
elif i >= n-1 or j >= n-1:
arr[i, j] = 0
else:
arr[i, j] = arr[i+1, j+1]
arr[0, index:-1] = arr[0, index+1:]
arr[:, n-1:] = 0
return np.asarray(arr)
讨论
在任何人生气之前,是的,我不知道我在Cython做什么。我禁用了bounds_checking
,因为它确实加快了速度。我在循环中用我的一个elif
语句执行边界检查。
我最初认为在循环中执行此操作不可能比NumPy更快。我预先分配了一个5000x5000
大小的NumPy数组,以避免在运行中进行追加等操作。我甚至使用与Numpy相同的3行测试了Cython实现,但它的性能也很差。
您可以看到,使用index=0
将需要最多的计算。所以我把它作为一个基准。在循环测试时,我发现Cython的实现比Numpy版本快50%以上。也许这是因为我没有充分使用NumPy提供的工具?
我绝不是一个计算机科学家,也不知道这是否是最好的途径。我是一名系统原型设计师。如果有人对如何让这声尖叫更快有任何见解,请告诉我!
答案更新
感谢Jerome今天教了我一些东西!这将有助于使该软件包以闪电般的速度运行。我将他的见解添加到了我的代码中,导致了巨大的性能提升,原因有两个:
- 我通过在对角线上方启动
j
-循环,将循环迭代次数减少了n*(n-1)/2
- 我已经删除了所有的条件语句
这是更新的Cython:
@cython.boundscheck(False)
@cython.wraparound(False)
def remove_from_collision_arrayV2(int index, int n, double[:,:] arr):
cdef int i, j
# Shift the diagonal matrix
for i in range(index, n-1):
for j in range(i, n-1):
arr[i, j] = arr[i+1, j+1]
# Shift the rop row
for j in range(index, n-1):
arr[0, j] = arr[0, j+1]
# Set Column column n-1 to zero
for i in range(n):
arr[i, n-1] = 0
return np.asarray(arr)
用于基准测试。在500x500
矩阵上使用index=0
执行此迭代500次:
原始NumPy代码:52.8s
原始Cython代码:16.47s
-3.2x加速
更新的Cython代码:0.014s
-3550x加速
表达式arr[index:-1, index:-1] = arr[index + 1:, index + 1:]
在Numpy和Cython中都很慢,Cython代码也快得多的原因有点违背直觉:此表达式在Numpy与Cython中都无法有效实现。
实际上,Numpy在动态分配的临时数组中复制右侧(arr[index + 1:, index + 1:]
)。然后将临时阵列复制到左侧(arr[index:-1, index:-1]
)。这意味着执行两次内存复制,而只能使用一次。更糟糕的是:复制的内存非常大,无法放入缓存,导致更大的开销(在一些处理器上,如主流的x86/x86-64处理器,写回策略会导致额外的慢速读取)。此外,新的临时数组会导致多页错误,甚至会使复制速度减慢。
Numpy这样做是因为左手边和右手边可能重叠(这里是这种情况),因此复制内存字节的顺序非常重要。Numpy使用缓慢保守的方法,而不是优化的实现。这是一个遗漏的优化。Cython也做同样的事情。
您的Cython代码不会受到所有这些开销的影响:它可以相对高效地直接将数组复制到位。读取的值保留在缓存中,然后立即写入,这样写回策略就不会成为问题。此外,不存在临时数组或页面错误。最后,与前面提到的表达式相比,您的Cython代码不复制三角矩阵的下部,从而导致要复制的字节更少。
减少Numpy表达式开销的一种方法是逐块复制矩阵块,并为此分配一个小的临时缓冲区(通常是矩阵的几行)。然而,这远不是一件容易的事,因为CPython循环通常非常慢,并且块大小应该适合缓存,所以该方法可能很有用。。。
进一步优化:条件很慢。您可以通过在i+1
处启动基于j
的循环并在n-1
处结束来移除它们。然后,另一个基于j
的循环可以填充大于n-1
的值。出于同样的原因,基于i
的循环应该在n-1
结束,然后另一个循环可以填充数组的剩余部分。一个好的编译器应该使用更快的SIMD指令。