为什么 numpy.view(bool) 使 numpy.logical_and 明显更快?
Why does numpy.view(bool) makes numpy.logical_and significantly faster?
将 uint8
的 numpy.ndarray
传递给 numpy.logical_and
时,如果我将 numpy.view(bool)
应用于其输入,它运行速度会明显加快。
a = np.random.randint(0, 255, 1000 * 1000 * 100, dtype=np.uint8)
b = np.random.randint(0, 255, 1000 * 1000 * 100, dtype=np.uint8)
%timeit np.logical_and(a, b)
126 ms ± 1.17 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
%timeit np.logical_and(a.view(bool), b.view(bool))
20.9 ms ± 110 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
有人可以解释为什么会这样吗?
此外,为什么 numpy.logical_and
不会自动将 view(bool)
应用于 uint8
的数组? (有什么情况不应该使用view(bool)
?)
编辑:
这似乎是 Windows 环境的问题。
我刚刚在官方 python docker 容器(debian)中尝试了同样的事情,发现它们之间没有区别。
我的环境:
- OS: Windows 10 Pro 21H2
- CPU:AMD 锐龙 9 5900X
- Python:Python 3.10.2(tags/v3.10.2:a58ebcc,2022 年 1 月 17 日,14:12:15)[MSC v.1929 64 位 (AMD64) ] 在 win32 上
- numpy: 1.22.2
这是当前 Numpy 实现的性能问题。我还可以在 Windows 上重现此问题(使用带有 Numpy 1.20.3 的 Intel Skylake Xeon 处理器)。 np.logical_and(a, b)
基于 慢条件跳转 执行 very-inefficient 标量汇编代码 而 np.logical_and(a.view(bool), b.view(bool))
执行 relatively-fast SIMD指令.
目前,Numpy 使用 specific implementation 作为 bool
类型。关于所使用的编译器,如果用于构建 Numpy 的编译器无法自动矢量化代码,Windows 显然是这种情况,general-purpose 的实现速度可能会明显变慢(并解释为什么情况并非如此其他平台,因为编译器可能不完全相同)。 Numpy 代码可以针对非bool
类型进行改进。请注意,Numpy 的矢量化是一项正在进行的工作,我们计划尽快对此进行优化。
更深入的分析
这里是np.logical_and(a, b)
执行的汇编代码:
Block 24:
cmp byte ptr [r8], 0x0 ; Read a[i]
jz <Block 27> ; Jump to block 27 if a[i]!=0
Block 25:
cmp byte ptr [r9], 0x0 ; Read b[i]
jz <Block 27> ; Jump to block 27 if b[i]!=0
Block 26:
mov al, 0x1 ; al = 1
jmp <Block 28> ; Skip the next instruction
Block 27:
xor al, al ; al = 0
Block 28:
mov byte ptr [rdx], al ; result[i] = al
inc r8 ; i += 1
inc rdx
inc r9
sub rcx, 0x1
jnz <Block 24> ; Loop again while i<a.shape[0]
如您所见,循环使用多个 data-dependent 条件跳转 来写入每个 a
和 b
读取的项目。这是非常低效的,因为 采取的分支不能由具有随机值的处理器 predicted。结果,处理器 停顿几个周期 (在现代 x86 处理器上通常约为 10 个周期)。
这里是np.logical_and(a.view(bool), b.view(bool))
执行的汇编代码:
Block 15:
movdqu xmm1, xmmword ptr [r10] ; xmm1 = a[i:i+16]
movdqu xmm0, xmmword ptr [rbx+r10*1] ; xmm0 = b[i:i+16]
lea r10, ptr [r10+0x10] ; i += 16
pcmpeqb xmm1, xmm2 ; \
pandn xmm1, xmm0 ; | Complex sequence to just do:
pcmpeqb xmm1, xmm2 ; | xmm1 &= xmm0
pandn xmm1, xmm3 ; /
movdqu xmmword ptr [r14+r10*1-0x10], xmm1 ; result[i:i+16] = xmm1
sub rcx, 0x1
jnz <Block 15> ; Loop again while i!=a.shape[0]//16
此代码使用称为 SSE 的 SIMD 指令 集,它能够在 128 位宽的寄存器上工作。没有条件跳跃。此代码的效率要高得多,因为它每次迭代一次对 16 个项目进行操作,并且每次迭代应该更快。
请注意,最后一个代码也不是最佳的,因为大多数现代 x86 处理器(如您的 AMD 处理器)支持 256 位 AVX-2 指令集(快两倍)。此外,编译器生成一个低效的 SIMD 指令序列来执行可以优化的 logical-and。编译器似乎假定布尔值可以是 0 或 1 的不同值。也就是说,输入数组太大而无法放入 CPU 缓存,因此代码 受吞吐量限制你的 RAM 而不是第一个。这就是 SIMD-friendly 代码没有显着加快的原因。对于处理器上小于 1 MiB 的数组,这两个版本之间的差异肯定要大得多(就像几乎所有其他现代处理器一样)。
将 uint8
的 numpy.ndarray
传递给 numpy.logical_and
时,如果我将 numpy.view(bool)
应用于其输入,它运行速度会明显加快。
a = np.random.randint(0, 255, 1000 * 1000 * 100, dtype=np.uint8)
b = np.random.randint(0, 255, 1000 * 1000 * 100, dtype=np.uint8)
%timeit np.logical_and(a, b)
126 ms ± 1.17 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
%timeit np.logical_and(a.view(bool), b.view(bool))
20.9 ms ± 110 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
有人可以解释为什么会这样吗?
此外,为什么 numpy.logical_and
不会自动将 view(bool)
应用于 uint8
的数组? (有什么情况不应该使用view(bool)
?)
编辑:
这似乎是 Windows 环境的问题。 我刚刚在官方 python docker 容器(debian)中尝试了同样的事情,发现它们之间没有区别。
我的环境:
- OS: Windows 10 Pro 21H2
- CPU:AMD 锐龙 9 5900X
- Python:Python 3.10.2(tags/v3.10.2:a58ebcc,2022 年 1 月 17 日,14:12:15)[MSC v.1929 64 位 (AMD64) ] 在 win32 上
- numpy: 1.22.2
这是当前 Numpy 实现的性能问题。我还可以在 Windows 上重现此问题(使用带有 Numpy 1.20.3 的 Intel Skylake Xeon 处理器)。 np.logical_and(a, b)
基于 慢条件跳转 执行 very-inefficient 标量汇编代码 而 np.logical_and(a.view(bool), b.view(bool))
执行 relatively-fast SIMD指令.
目前,Numpy 使用 specific implementation 作为 bool
类型。关于所使用的编译器,如果用于构建 Numpy 的编译器无法自动矢量化代码,Windows 显然是这种情况,general-purpose 的实现速度可能会明显变慢(并解释为什么情况并非如此其他平台,因为编译器可能不完全相同)。 Numpy 代码可以针对非bool
类型进行改进。请注意,Numpy 的矢量化是一项正在进行的工作,我们计划尽快对此进行优化。
更深入的分析
这里是np.logical_and(a, b)
执行的汇编代码:
Block 24:
cmp byte ptr [r8], 0x0 ; Read a[i]
jz <Block 27> ; Jump to block 27 if a[i]!=0
Block 25:
cmp byte ptr [r9], 0x0 ; Read b[i]
jz <Block 27> ; Jump to block 27 if b[i]!=0
Block 26:
mov al, 0x1 ; al = 1
jmp <Block 28> ; Skip the next instruction
Block 27:
xor al, al ; al = 0
Block 28:
mov byte ptr [rdx], al ; result[i] = al
inc r8 ; i += 1
inc rdx
inc r9
sub rcx, 0x1
jnz <Block 24> ; Loop again while i<a.shape[0]
如您所见,循环使用多个 data-dependent 条件跳转 来写入每个 a
和 b
读取的项目。这是非常低效的,因为 采取的分支不能由具有随机值的处理器 predicted。结果,处理器 停顿几个周期 (在现代 x86 处理器上通常约为 10 个周期)。
这里是np.logical_and(a.view(bool), b.view(bool))
执行的汇编代码:
Block 15:
movdqu xmm1, xmmword ptr [r10] ; xmm1 = a[i:i+16]
movdqu xmm0, xmmword ptr [rbx+r10*1] ; xmm0 = b[i:i+16]
lea r10, ptr [r10+0x10] ; i += 16
pcmpeqb xmm1, xmm2 ; \
pandn xmm1, xmm0 ; | Complex sequence to just do:
pcmpeqb xmm1, xmm2 ; | xmm1 &= xmm0
pandn xmm1, xmm3 ; /
movdqu xmmword ptr [r14+r10*1-0x10], xmm1 ; result[i:i+16] = xmm1
sub rcx, 0x1
jnz <Block 15> ; Loop again while i!=a.shape[0]//16
此代码使用称为 SSE 的 SIMD 指令 集,它能够在 128 位宽的寄存器上工作。没有条件跳跃。此代码的效率要高得多,因为它每次迭代一次对 16 个项目进行操作,并且每次迭代应该更快。
请注意,最后一个代码也不是最佳的,因为大多数现代 x86 处理器(如您的 AMD 处理器)支持 256 位 AVX-2 指令集(快两倍)。此外,编译器生成一个低效的 SIMD 指令序列来执行可以优化的 logical-and。编译器似乎假定布尔值可以是 0 或 1 的不同值。也就是说,输入数组太大而无法放入 CPU 缓存,因此代码 受吞吐量限制你的 RAM 而不是第一个。这就是 SIMD-friendly 代码没有显着加快的原因。对于处理器上小于 1 MiB 的数组,这两个版本之间的差异肯定要大得多(就像几乎所有其他现代处理器一样)。