繁体   English   中英

Numpy 数组到 ctypes 与 FORTRAN 订购

[英]Numpy array to ctypes with FORTRAN ordering

有没有一种高效的方法可以将 numpy 数组转换为 FORTRAN 有序 ctypes 数组,理想情况下不需要复制,并且不会触发与步幅相关的问题?

import numpy as np

# Sample data
n = 10000
A = np.zeros((n,n), dtype=np.int8)
A[0,1] = 1

def slow_conversion(A):
    return np.ctypeslib.as_ctypes(np.ascontiguousarray(A.T))

assert slow_conversion(A)[1][0] == 1

仅调用 as_ctypes 的性能分析:

%%timeit
np.ctypeslib.as_ctypes(A)

每个循环 3.35 µs ± 10.5 ns(平均值 ± 标准偏差。7 次运行,每次 100000 次循环)

提供的(慢速)转换的性能分析

%%timeit
slow_conversion(A)

每个循环 206 毫秒 ± 10.4 毫秒(平均值 ± 标准偏差。7 次运行,每个循环 1 个)

理想的结果是获得与as_ctypes调用类似的性能。

要求:

  • 列主顺序中的 numpy 数组(Fortran 或“F”顺序)
  • 快速转换为 ctypes 类型
  • 避免步幅问题

一种可能的方法是使用内部 Fortran memory 布局创建阵列:

A = np.zeros((n, n), dtype=np.int8, order='F')

然后转换可能如下所示:

def fast_conversion(arr):
    return np.ctypeslib.as_ctypes(arr.flatten('F').reshape(arr.shape))

如果您只需要一个一维数组,则可以省略.reshape(arr.shape) - 但就性能而言,应该没有区别。

它是如何工作的?

arr.flatten('F')返回折叠成一维的数组。 因为我们有一个F阶数组,所以速度很快。 之后通过reshape ,我们将数组的形状应用回它而不更改其数据。 顺便说一句:由于我们使用的是F阶数组,我们还可以使用arr.flatten('K') ,文档中说:

“K”表示按照元素在 memory 中出现的顺序将 a 展平。

https://numpy.org/doc/stable/reference/generated/numpy.ndarray.flatten.html

必须使用F顺序创建数组,这一点很重要。 否则fast_conversion会和slow_conversion一样慢。

测试

import timeit
import numpy as np

# Sample data
n = 10000
A = np.zeros((n, n), dtype=np.int8)
A[0, 1] = 1

B = np.zeros((n, n), dtype=np.int8, order='F')
B[0, 1] = 1


def slow_conversion(arr):
    return np.ctypeslib.as_ctypes(np.ascontiguousarray(arr.T))


def fast_conversion(arr):
    return np.ctypeslib.as_ctypes(arr.flatten('F').reshape(arr.shape))


assert slow_conversion(A)[1][0] == 1
assert fast_conversion(B)[1][0] == 1

loops = 10
slow_result = timeit.timeit(lambda: slow_conversion(A), number=loops)
print(f'slow: {slow_result / loops}')

fast_result = timeit.timeit(lambda: fast_conversion(B), number=loops)
print(f'fast: {fast_result / loops}')

测试结果为 output:

slow: 0.45553940839999996
fast: 0.02264067879987124

因此,快速版本比慢速版本快约 20 倍。

正如您所指出的np.ctypeslib.as_ctypes(...)很快。

您的计算瓶颈在np.ascontiguousarray(AT) - 它相当于np.asfortranarray(A) ,在大型 arrays 上同样慢。


这使我相信仅使用 numpy 函数无法使这更快。 我的意思是,既然已经存在一个完整的专用 function 来做到这一点 - 我们假设它具有最佳性能。

默认情况下,Numpy 创建 C-ordered arrays(因为它是用 C 编写的),即row-major arrays。 使用AT进行转置会创建一个数组视图,其中步幅反转(即没有副本)。 话虽如此, np.ascontiguousarray会进行复制,因为该数组现在不再连续并且副本很昂贵。 这就是为什么slow_conversion很慢。 请注意,可以使用yourarray.flags['F_CONTIGUOUS']和检查yourarray.strides来测试连续性。 另请注意, yourarray.flagsyourarray.__array_interface__提供有关数组是否已复制的信息以及有关步幅的信息。

np.asfortranarray在有关文档的 memory 中返回按 Fortran 顺序排列的数组。 如果需要,它可以执行复制。 事实上, np.asfortranarray(A)会进行复制,而np.asfortranarray(AT)不会。 您可以查看 function 的C 代码以获取有关此行为的更多信息。 由于两者都被视为 FORTRAN 连续的,因此最好使用在这种情况下不进行任何复制的np.asfortranarray(AT)

关于 ctypes,它处理 C arrays 以行优先顺序存储,而不是 FORTRAN 以列优先顺序存储。 此外,与 FORTRAN 相比,C arrays 本身不支持步幅。 这意味着一行基本上是 memory 中存储的连续数据的 memory 视图。 由于slow_conversion(A)[1][0] == 1必须为真,这意味着返回的 object 的第二行的第一项应该是 1,因此这些列必须连续存储在 memory 中。 问题是初始数组不是 FORTRAN 连续的,而是 C 连续的,因此需要转置 转置非常昂贵(尽管 Numpy 的实现不是最理想的)。

假设您不想支付复制/转置的开销,则需要放宽该问题。 有几个可能的选择:

  • 使用例如np.zeros((n,n), dtype=np.int8, order='F')直接使用 Numpy 创建 FORTRAN 有序数组。 This create a C array with transposed strides so to behave like a FORTRAN array where computations operating on columns are fast (remember that Numpy is written in C so row-major ordered array are the reference). 这样,ctypes 中的第一行实际上是一列。 请注意,为了性能,在混合 C 和 FORTRAN 有序数组时应该非常小心,因为非连续访问要慢得多。
  • 使用跨步 FORTRAN 阵列。 这种解决方案基本上意味着基本的基于列的计算会慢得多,并且需要编写在 FORTRAN 中非常不寻常的基于行的计算。 您可以使用A.ctypes.data_as(POINTER(c_double))提取指向 C 连续数组的指针,使用 A.strides 提取步幅,使用A.strides A.shape 话虽如此,这个解决方案似乎并不是真正的便携/标准。 标准方式似乎是在 FORTRAN 中使用 C 绑定。 我对此不是很熟悉,但你可以在这个答案中找到一个完整的例子。

最后一种解决方案是使用快速转置算法手动就地转置数据。 这比异地转置要快,但这需要一个方阵,并且不能直接使用 Numpy 来完成。 此外,它会改变以后不应该使用的输入数组(除非可以对转置数组进行操作)。 一种解决方案是在 Numba 中进行,或在 C或直接在 FORTRAN 中进行(在所有情况下都使用包装器 function)。 这应该比 Numpy 所做的要快得多,但仍然比基本的 ctypes 包装慢得多。

有一个方面可以改进。

该操作不仅是制作副本,而且因为它正在加载和存储到主 memory 而不是使用缓存。

通常,只要 memory 和从缓存中使用它,处理器就会访问多个块多个连续字节。 如果缓存空间用完,一些旧块将被逐出。

为了论证的缘故,假设您的 CPU 工作在 8 个字节的块上,并且行是连续的。 在一个矩阵中,您将访问列,而在另一个矩阵中,您将访问行。 当您写下一列时,您正在加载多列但只更新一列。 通过复制几列可以看出这一单列的开销

n = 2**14
A = np.random.randint(0, 100, (n,n), dtype=np.int8)
B = np.empty_like(A)
%%timeit
B[:1,:] = A[:1,:]
%%timeit
B[:4,:] = A[:4,:]

如果你在行上做同样的事情,你应该注意到一些大致线性的东西。 如果复制列,则复制一列的成本非常接近复制两列甚至 8 或 16 列的成本,具体取决于硬件。

我将使用n=2**14使事情变得更容易,但该原则适用于任何维度。

  • 如果你有一个足够小的让我们说 8 x 8 整个矩阵适合缓存,所以你可以在不访问任何缓存的情况下转置它。
  • 如果您正在复制大量连续数据块,即使您无法在缓存上执行整个操作,您也可以减少给定数据再次从 memory 加载/加载到 memory 的次数。

基于此,我尝试在较小的连续块矩阵中重新排列矩阵,首先我转置块中的元素,然后转置矩阵中的块。

对于基线

B = np.ascontiguousarray(A.T)
3.12 s ± 446 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

使用 8x8 块

T0 = A.reshape(2048,8,2048,8)
T1 = np.ascontiguousarray(T0.transpose(0,2,3,1))
T2 = np.ascontiguousarray(T1.transpose(1,0,2,3))
T3 = np.ascontiguousarray(T2.transpose(0,2,1,3))
B = T3.reshape(A.shape)
786 ms ± 54.2 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
assert np.all(B == A.T) # 2.8s

它仍然比简单复制慢 200 倍,但已经比原始方法快 4 倍

也只分配两个而不是 3 个临时 arrays 帮助

T0 = np.empty_like(A)
T1 = np.empty_like(A)
T0.reshape(2048,2048,8,8)[:] = A.reshape(2048,8,2048,8).transpose(0,2,3,1)
T1.reshape(2048,2048,8,8)[:] = T0.reshape(2048,2048,8,8).transpose(1,0,2,3)
T0.reshape(2048,8,2048,8)[:] = T1.reshape(2048,2048,8,8).transpose(0,2,1,3)
B = T0
686 ms ± 60.5 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

你必须检查这个网站 我相信你会得到你的答案https://leherchat.com

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM