在 numpy 内存映射上随机切片的效率
Efficiency of random slicing on a numpy memory map
我有一个 20GB、100k x 100k 'float16' 二维数组作为数据文件。我按如下方式将其加载到内存中:
fp_read = np.memmap(filename, dtype='float16', mode='r', shape=(100000, 100000))
然后我尝试从中读取切片。我需要拍摄的垂直切片实际上是随机的,但性能很差,还是我做错了什么?
分析:
我对比过其他形式的横截面切片,虽然不知道为什么会好很多:
%timeit fp_read[:,17000:17005] # slice 5 consecutive cols
1.64 µs ± 16.4 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
%timeit fp_read[:,11000:11050:10]
1.67 µs ± 21 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
%timeit fp_read[:,5000:6000:200]
1.66 µs ± 27.3 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
%timeit fp_read[:,0:100000:20000] # slice 5 disperse cols
1.69 µs ± 14.7 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
%timeit fp_read[:,[1,1001,27009,81008,99100]] # slice 5 rand cols
32.4 ms ± 10.9 ms per loop (mean ± std. dev. of 7 runs, 100 loops each)
a = np.arange(100000); b = np.array([1,1001,27009,81008,99100])
%timeit fp_read[np.ix_(a,b)]
18 ms ± 142 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
即使这些 timeit 函数也无法准确捕获性能下降,因为:
import time
a = np.arange(100000)
cols = np.arange(100000)
np.random.shuffle(cols)
cols = np.sort(cols[:5])
t = time.time()
arr = fp_read[np.ix_(a,cols)]
print('Actually took: {} seconds'.format(time.time() - t))
Actually took: 24.5 seconds
与:
相比
t = time.time()
arr = fp_read[:,0:100000:20000]
print('Actually took: {} seconds'.format(time.time() - t))
Actually took 0.00024 seconds
性能差异可以通过 "basic slicing and indexing" 与 "advanced indexing"、see these docs 中的一个关键差异来解释。这里的重点是
Advanced indexing always returns a copy of the data (contrast with basic slicing that returns a view).
从fp_read[:,5000:6000:200]
与fp_read[:,5000:6000:200].copy()
的比较中可以看出副本的伤害程度。
虽然制作数组副本总是比制作新视图慢,但对于 memmap 来说尤其糟糕:
- 从磁盘读取相对较慢。需要从磁盘读取数据以制作(内存中的)副本,而 view 根本不需要读取任何数据!只是使用内存缓冲区的新偏移量和步长(步幅)参数创建了一个新的 ndarray 对象。
- 数据的内存布局是行优先顺序(相对于列优先,请参阅 wikipedia)。对于访问随机列,这意味着必须为每个数据值从磁盘读取一个扇区。将其与 contiguous 访问进行比较,其中每 256 个值只读取一个扇区(假设 float16 和 512 字节扇区)。使用内存映射 io 这种效果更糟,因为数据是以 4kB 的块(内存页)读取的,所以 8 x 512 字节扇区。
现在我们也可以理解为什么 timeit 结果并不真正具有代表性了:文件的那个特定部分被 OS 缓存在内存中。
我有一个 20GB、100k x 100k 'float16' 二维数组作为数据文件。我按如下方式将其加载到内存中:
fp_read = np.memmap(filename, dtype='float16', mode='r', shape=(100000, 100000))
然后我尝试从中读取切片。我需要拍摄的垂直切片实际上是随机的,但性能很差,还是我做错了什么?
分析:
我对比过其他形式的横截面切片,虽然不知道为什么会好很多:
%timeit fp_read[:,17000:17005] # slice 5 consecutive cols
1.64 µs ± 16.4 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
%timeit fp_read[:,11000:11050:10]
1.67 µs ± 21 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
%timeit fp_read[:,5000:6000:200]
1.66 µs ± 27.3 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
%timeit fp_read[:,0:100000:20000] # slice 5 disperse cols
1.69 µs ± 14.7 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
%timeit fp_read[:,[1,1001,27009,81008,99100]] # slice 5 rand cols
32.4 ms ± 10.9 ms per loop (mean ± std. dev. of 7 runs, 100 loops each)
a = np.arange(100000); b = np.array([1,1001,27009,81008,99100])
%timeit fp_read[np.ix_(a,b)]
18 ms ± 142 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
即使这些 timeit 函数也无法准确捕获性能下降,因为:
import time
a = np.arange(100000)
cols = np.arange(100000)
np.random.shuffle(cols)
cols = np.sort(cols[:5])
t = time.time()
arr = fp_read[np.ix_(a,cols)]
print('Actually took: {} seconds'.format(time.time() - t))
Actually took: 24.5 seconds
与:
相比t = time.time()
arr = fp_read[:,0:100000:20000]
print('Actually took: {} seconds'.format(time.time() - t))
Actually took 0.00024 seconds
性能差异可以通过 "basic slicing and indexing" 与 "advanced indexing"、see these docs 中的一个关键差异来解释。这里的重点是
Advanced indexing always returns a copy of the data (contrast with basic slicing that returns a view).
从fp_read[:,5000:6000:200]
与fp_read[:,5000:6000:200].copy()
的比较中可以看出副本的伤害程度。
虽然制作数组副本总是比制作新视图慢,但对于 memmap 来说尤其糟糕:
- 从磁盘读取相对较慢。需要从磁盘读取数据以制作(内存中的)副本,而 view 根本不需要读取任何数据!只是使用内存缓冲区的新偏移量和步长(步幅)参数创建了一个新的 ndarray 对象。
- 数据的内存布局是行优先顺序(相对于列优先,请参阅 wikipedia)。对于访问随机列,这意味着必须为每个数据值从磁盘读取一个扇区。将其与 contiguous 访问进行比较,其中每 256 个值只读取一个扇区(假设 float16 和 512 字节扇区)。使用内存映射 io 这种效果更糟,因为数据是以 4kB 的块(内存页)读取的,所以 8 x 512 字节扇区。
现在我们也可以理解为什么 timeit 结果并不真正具有代表性了:文件的那个特定部分被 OS 缓存在内存中。