加速 Numpy 中 3D 矩阵的初始化
Speed up the initialization of 3D matrices in Numpy
我最近注意到,与已经初始化的数组相比,处理新初始化的 Numpy 数组时速度明显变慢。
一般来说,初始化数组需要更长的时间似乎是合乎逻辑的,但我没想到会有这么大的差异。该片段是我需要的函数的示意图部分,仅这两行创建 dim3 就占用了函数总运行时间的一半。
import numpy as np
mask = np.where(np.random.rand(150,150) > 0.98)
very_important_data = np.random.rand(len(mask[0]), 1000)
dim3 = np.zeros((150,150,1000))
%timeit dim3[mask] = very_important_data # --> 114 µs ± 5.24 µs per loop
%timeit dim3 = np.zeros((150,150,1000)); dim3[mask] = very_important_data # --> 9.4 ms ± 585 µs per loop
是否有更有效的预初始化 dim3 矩阵的方法?还是在分配新值之前将矩阵设置为零的有效方法?
谢谢!
您可以使用 np.empty()
代替:
%timeit dim3 = np.zeros((150,150,1000)); dim3[mask] = very_important_data
%timeit dim3 = np.empty((150, 150, 1000)); dim3[mask] = very_important_data
输出(在 macOS 12.4 上):
5.3 ms ± 17.4 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
190 µs ± 382 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
注意:如果您设置所有元素,这只会给出相同的结果,请参阅文档:https://numpy.org/doc/stable/reference/generated/numpy.empty.html
要了解是否有更有效的方法,首先需要了解为什么当前代码很慢。
创建像 np.zeros
或 np.empty
这样的数组的 Numpy 方法(或任何创建像乘法、加法等临时数组的方法)从 CPython 分配器请求一个内存缓冲区,它转发它分配给 default libc 分配器(不同于 OS)或自定义分配器(如果有的话)。 np.zeros
请求一个带零的缓冲区 pre-filled 而 np.empty
只请求一个原始缓冲区。
默认分配器根据平台(主要是操作系统)表现不同。在 Windows 上,它向 OS 请求内存并系统地释放它用于大缓冲区,而 Mac 和 Linux 的默认内存分配器往往更保守:它们保持漂亮本地大块内存并尝试尽可能多地重用它们而不是将 space 释放到 OS.
此默认策略对性能和内存使用有重大影响。实际上,当从 Numpy 请求 zero-filled 内存缓冲区并且缓冲区从先前分配的 space 回收(尚未释放到 OS 时,分配器需要将所有值填充为 0 ).但是,当直接从 OS 请求 zero-filled 内存时,OS 可以 return 一个虚拟内存缓冲区,它将 延迟填充 仅当 first-touch 在特定 内存页 上执行时。这意味着对于大数组的分配可以快得多,但是用零填充数组的开销被延迟。最后,只要所有页都是read/written(即数组被完全读取或写入一些值),填充数组的开销就会被支付。实际上,这种惰性内存填充比缓冲区因 page-faults 而被分配器回收的代价更高。一些 OS 预填充内存块(可能在单独的线程中)以加速此类 zero-filled 缓冲区请求。因此,您应该非常注意对应用程序进行基准测试的方式。
在实践中,OS 请求的内存在主流平台上总是用零填充(默认情况下在 Windows、Linux 和 Mac 上),因为安全原因:一个进程之前分配、填充和释放的内存不能从另一个进程访问,因为内存块可能包含敏感信息(例如,您的浏览器可以在内存中存储密码,而您不希望使用 Numpy python脚本以便能够在没有任何权限的情况下读取它们)。这个补零一般在page-fault时间完成。因此,当从 OS 请求数组时,调用 np.empty
或 np.zeros
会得到相同的结果。但是,当数组被分配器回收时,np.empty
可以更快,并且(通常)没有 page-fault 开销需要支付(page-faults 每页完成一次,只要当你 运行 内存不足时,内存页不会存储在其他地方,比如交换中。
简而言之,没有办法(仅来自Python)加快创建数组的速度,只要您请求创建一个新数组并且您read/write所有目标数组。使用自定义系统分配器并没有多大帮助,因为无论如何都必须填充数组。如果您可以逐步支付开销,那么您需要使用手动内存映射。否则你可以预先分配一些缓冲区并自己回收它们。它可以更快,因为您可能不需要将它们完全填充为零,并且您不会支付页面错误的成本。天下没有免费的午餐。
相关帖子:
我最近注意到,与已经初始化的数组相比,处理新初始化的 Numpy 数组时速度明显变慢。 一般来说,初始化数组需要更长的时间似乎是合乎逻辑的,但我没想到会有这么大的差异。该片段是我需要的函数的示意图部分,仅这两行创建 dim3 就占用了函数总运行时间的一半。
import numpy as np
mask = np.where(np.random.rand(150,150) > 0.98)
very_important_data = np.random.rand(len(mask[0]), 1000)
dim3 = np.zeros((150,150,1000))
%timeit dim3[mask] = very_important_data # --> 114 µs ± 5.24 µs per loop
%timeit dim3 = np.zeros((150,150,1000)); dim3[mask] = very_important_data # --> 9.4 ms ± 585 µs per loop
是否有更有效的预初始化 dim3 矩阵的方法?还是在分配新值之前将矩阵设置为零的有效方法?
谢谢!
您可以使用 np.empty()
代替:
%timeit dim3 = np.zeros((150,150,1000)); dim3[mask] = very_important_data
%timeit dim3 = np.empty((150, 150, 1000)); dim3[mask] = very_important_data
输出(在 macOS 12.4 上):
5.3 ms ± 17.4 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
190 µs ± 382 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
注意:如果您设置所有元素,这只会给出相同的结果,请参阅文档:https://numpy.org/doc/stable/reference/generated/numpy.empty.html
要了解是否有更有效的方法,首先需要了解为什么当前代码很慢。
创建像 np.zeros
或 np.empty
这样的数组的 Numpy 方法(或任何创建像乘法、加法等临时数组的方法)从 CPython 分配器请求一个内存缓冲区,它转发它分配给 default libc 分配器(不同于 OS)或自定义分配器(如果有的话)。 np.zeros
请求一个带零的缓冲区 pre-filled 而 np.empty
只请求一个原始缓冲区。
默认分配器根据平台(主要是操作系统)表现不同。在 Windows 上,它向 OS 请求内存并系统地释放它用于大缓冲区,而 Mac 和 Linux 的默认内存分配器往往更保守:它们保持漂亮本地大块内存并尝试尽可能多地重用它们而不是将 space 释放到 OS.
此默认策略对性能和内存使用有重大影响。实际上,当从 Numpy 请求 zero-filled 内存缓冲区并且缓冲区从先前分配的 space 回收(尚未释放到 OS 时,分配器需要将所有值填充为 0 ).但是,当直接从 OS 请求 zero-filled 内存时,OS 可以 return 一个虚拟内存缓冲区,它将 延迟填充 仅当 first-touch 在特定 内存页 上执行时。这意味着对于大数组的分配可以快得多,但是用零填充数组的开销被延迟。最后,只要所有页都是read/written(即数组被完全读取或写入一些值),填充数组的开销就会被支付。实际上,这种惰性内存填充比缓冲区因 page-faults 而被分配器回收的代价更高。一些 OS 预填充内存块(可能在单独的线程中)以加速此类 zero-filled 缓冲区请求。因此,您应该非常注意对应用程序进行基准测试的方式。
在实践中,OS 请求的内存在主流平台上总是用零填充(默认情况下在 Windows、Linux 和 Mac 上),因为安全原因:一个进程之前分配、填充和释放的内存不能从另一个进程访问,因为内存块可能包含敏感信息(例如,您的浏览器可以在内存中存储密码,而您不希望使用 Numpy python脚本以便能够在没有任何权限的情况下读取它们)。这个补零一般在page-fault时间完成。因此,当从 OS 请求数组时,调用 np.empty
或 np.zeros
会得到相同的结果。但是,当数组被分配器回收时,np.empty
可以更快,并且(通常)没有 page-fault 开销需要支付(page-faults 每页完成一次,只要当你 运行 内存不足时,内存页不会存储在其他地方,比如交换中。
简而言之,没有办法(仅来自Python)加快创建数组的速度,只要您请求创建一个新数组并且您read/write所有目标数组。使用自定义系统分配器并没有多大帮助,因为无论如何都必须填充数组。如果您可以逐步支付开销,那么您需要使用手动内存映射。否则你可以预先分配一些缓冲区并自己回收它们。它可以更快,因为您可能不需要将它们完全填充为零,并且您不会支付页面错误的成本。天下没有免费的午餐。
相关帖子: