我一直致力于优化 NumPy 数组中元素之间差异的计算。我一直在使用 Numba 来提高性能,但当数组大小超过 1 MB 时,我会出现 100 微秒的跳跃。我认为这是由于我的 CPU 的 Ryzen 7950X 1 MB L1 缓存大小所致。
这是示例代码:
@jit(nopython=True)
def extract_difference_1(random_array):
shape0, shape1 = random_array.shape
difference_arr = np.empty((shape0, shape1), dtype=np.float64)
for i in range(shape0):
difference_arr[i] = random_array[i,0] - random_array[i,1], random_array[i,1] - random_array[i,2], random_array[i,2] - random_array[i,3], random_array[i,3] - random_array[i,4], random_array[i,4] - random_array[i,5], random_array[i,5] - random_array[i,6], random_array[i,6] - random_array[i,0]
return difference_arr
@jit(nopython=True)
def extract_difference_2(random_array):
shape0, shape1 = random_array.shape
split_index = shape0 // 2
part_1 = extract_difference_1(random_array[:split_index])
part_2 = extract_difference_1(random_array[split_index:])
return part_1 , part_2
x_list = [18500, 18700, 18900]
y = 7
for x in x_list:
random_array = np.random.rand(x, y)
print(f"\nFor (x,y) = ({x}, {y}), random_array size is {array_size_string(random_array)}:\n")
for func in [extract_difference_1, extract_difference_2]:
func(random_array) # compile the function
timing_result = %timeit -q -o func(random_array)
print(f"{func.__name__}:\t {timing_result_message(timing_result)}")
计时结果为:
For (x,y) = (18500, 7), random_array size is 0.988 MB, 1011.72 KB:
extract_difference_1: 32.4 µs ± 832 ns, b: 31.5 µs, w: 34.3 µs, (l: 7, r: 10000),
extract_difference_2: 33.8 µs ± 279 ns, b: 33.5 µs, w: 34.3 µs, (l: 7, r: 10000),
For (x,y) = (18700, 7), random_array size is 0.999 MB, 1022.66 KB:
extract_difference_1: 184 µs ± 2.15 µs, b: 181 µs, w: 188 µs, (l: 7, r: 10000),
extract_difference_2: 34.4 µs ± 51.2 ns, b: 34.3 µs, w: 34.5 µs, (l: 7, r: 10000),
For (x,y) = (18900, 7), random_array size is 1.009 MB, 1033.59 KB:
extract_difference_1: 201 µs ± 3.3 µs, b: 196 µs, w: 205 µs, (l: 7, r: 10000),
extract_difference_2: 34.5 µs ± 75.2 ns, b: 34.4 µs, w: 34.6 µs, (l: 7, r: 10000),
将生成的 Difference_arr 分成两个即可,但我更喜欢结果是单个数组。特别是稍后,我将把 y 增加到 10、50、100、1000,将 x 增加到 20000。当将分割数组part_1和part_2合并到difference_arr时,我发现它比extract_difference_1慢。我认为速度变慢是由于 extract_difference_1 大于 1 MB,导致 L1 缓存未被使用。
有没有办法在使用 Python、Numba 或任何其他包将结果保持为单个数组的同时保持性能?或者有没有一种方法可以让我重新组合这些数组,而不会因为结果数组超出一级缓存大小而导致性能损失?
TL;DR:性能问题不是由您的CPU缓存引起。它来自目标平台上的分配器的行为,当然是Windows。
我认为这是由于我的 CPU 的 Ryzen 7950X 1 MB L1 缓存大小所致。
首先,AMD Ryzen 7950X CPU是Zen4 CPU。该架构具有 32 KiB 而不是 1 MiB 的 L1D 缓存。话虽如此,此架构上的 L2 缓存为 1 MiB。
虽然缓存大小假设乍一看是一个诱人的想法。它有两个主要问题:
首先,两个函数读取和写入的数据量相同。数组分为两部分的事实并没有改变这一事实。因此,如果第一个函数由于 L2 容量而发生缓存未命中,那么其他函数也应该出现这种情况。关于内存访问,两个函数之间唯一的主要区别是访问的顺序,无论如何都不会对性能产生重大影响(因为数组足够大,因此可以缓解延迟问题)。
此外,Zen4 上的 L2 缓存并不比 L3 慢很多。事实上,它的速度不应慢两倍以上,而实验结果显示执行时间增加了 5 倍以上。
我可以在 Windows 上的 Cascade Lake CPU(二级缓存也为 1 MiB)上重现这一点。这是结果:
For (x,y) = (18500, 7), random_array size is 0.988006591796875:
extract_difference_1: 68.6 µs ± 3.63 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
extract_difference_2: 70.8 µs ± 5.2 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
For (x,y) = (18700, 7), random_array size is 0.998687744140625:
extract_difference_1: 342 µs ± 8.31 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
extract_difference_2: 69.7 µs ± 2.67 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
For (x,y) = (18900, 7), random_array size is 1.009368896484375:
extract_difference_1: 386 µs ± 7.34 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
extract_difference_2: 67 µs ± 4.51 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
将得到的 Difference_arr 分成两部分就可以了
这两个函数之间的主要区别在于,一个函数执行 2 个小分配,而不是 1 个大分配。这就提出了一个新的假设:分配时机能解释这个问题吗?
我们可以根据之前的文章轻松回答这个问题:为什么使用 np.empty 进行分配而不是 O(1)。我们可以看到 0.76 MiB (np.empty(10**5)
) 的分配与下一个大于 1 MiB 的分配之间存在很大的性能差距。以下是目标答案提供的结果:
np.empty(10**5) # 620 ns ± 2.83 ns per loop (on 7 runs, 1000000 loops each)
np.empty(10**6) # 9.61 µs ± 34.2 ns per loop (on 7 runs, 100000 loops each)
更准确地说,这是我当前机器上的新基准:
%timeit -n 10_000 np.empty(1000*1024, np.uint8)
793 ns ± 18.8 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
%timeit -n 10_000 np.empty(1024*1024, np.uint8)
6.6 µs ± 173 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
我们可以看到差距接近 1 MiB。请注意,1000 KiB 和 1024 之间的计时不是很稳定(表明结果取决于隐藏的低级参数——可能是打包/对齐问题)。这种 Numpy 分配行为是 AFAIK 特定于 Windows 的,而 AFAIR 在 Linux 上不可见(可能会看到差距,但不是那么大,并且不在相同的阈值)。
链接答案中提供了解释:
内核调用成本高昂(大页面也可能发挥作用)。
有没有办法在保持性能的同时让Python的结果成为单个数组是的。您可以
预分配输出数组内存,这样就不必支付昂贵的分配开销。另一种解决方案是使用另一个分配器(例如 jemalloc)。
这是预分配内存的修改代码:
@nb.jit(nopython=True)
def extract_difference_1(random_array, scratchMem):
shape0, shape1 = random_array.shape
difference_arr = scratchMem[:shape0*shape1].reshape((shape0, shape1))#np.empty((shape0, shape1), dtype=np.float64)
for i in range(shape0):
difference_arr[i] = random_array[i,0] - random_array[i,1], random_array[i,1] - random_array[i,2], random_array[i,2] - random_array[i,3], random_array[i,3] - random_array[i,4], random_array[i,4] - random_array[i,5], random_array[i,5] - random_array[i,6], random_array[i,6] - random_array[i,0]
return difference_arr
@nb.jit(nopython=True)
def extract_difference_2(random_array, scratchMem):
shape0, shape1 = random_array.shape
split_index = shape0 // 2
part_1 = extract_difference_1(random_array[:split_index], np.empty((split_index, shape1)))
part_2 = extract_difference_1(random_array[split_index:], np.empty((split_index, shape1)))
return part_1 , part_2
x_list = [18500, 18700, 18900]
y = 7
scratchMem = np.empty(16*1024*1024)
for x in x_list:
random_array = np.random.rand(x, y)
print(f"\nFor (x,y) = ({x}, {y}), random_array size is {x*y*8/1024/1024}:\n")
for func in [extract_difference_1, extract_difference_2]:
func(random_array, scratchMem) # compile the function
timing_result = %timeit -q -o func(random_array, scratchMem)
print(f"{func.__name__}:\t {timing_result}")
结果如下:
For (x,y) = (18500, 7), random_array size is 0.988006591796875:
extract_difference_1: 65.1 µs ± 2.48 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
extract_difference_2: 71 µs ± 2.36 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
For (x,y) = (18700, 7), random_array size is 0.998687744140625:
extract_difference_1: 69.3 µs ± 4.05 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
extract_difference_2: 68.3 µs ± 3.06 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
For (x,y) = (18900, 7), random_array size is 1.009368896484375:
extract_difference_1: 68.5 µs ± 1.98 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
extract_difference_2: 68.7 µs ± 3.14 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)