理解Numpy中的向量化和Numexpr的向量化表达式多线程的区别

9 投票
2 回答
5172 浏览
提问于 2025-04-18 11:38

我对NumPy的一个概念有点困惑,听说它在进行数组运算时是“向量化”的:这是不是意味着它克服了Python的全局解释器锁(GIL),因为NumPy的一部分是用C语言实现的?那么,Numexpr又是怎么工作的呢?如果我没理解错的话,它是通过一种优化的即时编译(JIT)来运行代码,并且支持多线程,从而克服了Python的GIL。

而且,所谓的“真正的”向量化难道更像是多进程而不是多线程吗?

2 个回答

0

Numexpr 是一个很棒的工具,特别适合做一些计算,比如数组的乘法和简化操作,而且它可以直接使用 numpy 的内存映射(memmap)作为输入。在像 (ij,jk->i) 这样的操作中,使用 numexpr 可以用一行代码搞定,而用 numpy 则需要分成几步来做,比如 (ij,jk -> ik -> i)。你可以在这里找到更多信息:numpy - python - way to do fast matrix multiplication and reduction while working in memmaps and CPU - Stack Overflow

8

NumPy在某些情况下可能会使用一个库,这个库通过多个进程来处理任务,从而把负担分散到多个核心上。不过,这主要取决于这个库本身,和NumPy中的Python代码关系不大。所以,没错,NumPy和其他库如果不是用Python写的,可以绕过这些限制。甚至还有一些库提供了GPU加速的功能。

NumExpr也是用类似的方法来绕过GIL(全局解释器锁)。在他们的官网上写着:

此外,numexpr在其内部虚拟机中直接实现了多线程计算的支持,这个虚拟机是用C语言写的。这使得可以绕过Python中的GIL。

不过,NumPy和NumExpr之间有一些根本的区别。NumPy专注于为数组操作提供一个良好的Python接口,而NumExpr的范围要窄得多,并且有自己的一种语言。当NumPy执行计算 c = 3*a + 4*b 时,操作数是数组,这个过程中会创建两个临时数组(3*a4*b)。而NumExpr可能会优化这个计算,使得乘法和加法是逐个元素进行的,而不需要使用任何中间结果。

这就导致了NumPy的一些有趣现象。以下测试是在一个4核8线程的i7处理器上进行的,时间测量使用了iPython的 %timeit

import numpy as np
import numexpr as ne

def addtest_np(a, b): a + b
def addtest_ne(a, b): ne.evaluate("a+b")

def addtest_np_inplace(a, b): a += b
def addtest_ne_inplace(a, b): ne.evaluate("a+b", out=a)

def addtest_np_constant(a): a + 3
def addtest_ne_constant(a): ne.evaluate("a+3")

def addtest_np_constant_inplace(a): a += 3
def addtest_ne_constant_inplace(a): ne.evaluate("a+3", out=a)

a_small = np.random.random((100,10))
b_small = np.random.random((100,10))

a_large = np.random.random((100000, 1000))
b_large = np.random.random((100000, 1000))

# results: (time given is in nanoseconds per element with small/large array)
# np: NumPy
# ne8: NumExpr with 8 threads
# ne1: NumExpr with 1 thread
#
# a+b:
#  np: 2.25 / 4.01
#  ne8: 22.6 / 3.22
#  ne1: 22.6 / 4.21
# a += b:
#  np: 1.5 / 1.26 
#  ne8: 36.8 / 1.18
#  ne1: 36.8 / 1.48

# a+3:
#  np: 4.8 / 3.62
#  ne8: 10.9 / 3.09
#  ne1: 20.2 / 4.04
# a += 3:
#  np: 3.6 / 0.79
#  ne8: 34.9 / 0.81
#  ne1: 34.4 / 1.06

当然,使用的时间测量方法并不是非常准确,但有一些普遍的趋势:

  • NumPy使用的时钟周期更少(np < ne1)
  • 在处理非常大的数组时,使用并行处理会有一点帮助(10-20%)
  • NumExpr在处理小数组时要慢得多
  • NumPy在就地操作方面表现非常强大

NumPy并没有让简单的算术操作并行化,但从上面的结果可以看出,这其实并不重要。速度主要受限于内存带宽,而不是处理能力。

如果我们做一些更复杂的事情,情况就会有所不同。

np.sin(a_large)               # 19.4 ns/element
ne.evaluate("sin(a_large)")   # 5.5 ns/element

速度不再受限于内存带宽。为了确认这是否真的是因为线程的原因(而不是NumExpr有时使用一些快速库的原因):

ne.set_num_threads(1)
ne.evaluate("sin(a_large)")    # 34.3 ns/element

在这里,并行处理真的能帮助很多。

NumPy在处理更复杂的线性操作时,比如矩阵求逆,可能会使用并行处理。这些操作NumExpr不支持,所以没有什么有意义的比较。实际的速度取决于所使用的库(BLAS/Atlas/LAPACK)。另外,在执行复杂操作如FFT时,性能也依赖于库。(据我所知,NumPy/SciPy还没有支持fftw。)

总的来说,似乎在某些情况下NumExpr非常快且有用。而在其他情况下,NumPy则是最快的。如果你有大数组和逐元素操作,NumExpr表现得非常强大。不过需要注意的是,使用 multiprocessing 或类似的方式,通常可以很容易地将一些并行处理(甚至是将计算分散到多台计算机上)整合到代码中。


关于“多进程”和“多线程”的问题有点棘手,因为这些术语有点模糊。在Python中,“线程”是在同一个GIL下运行的,但如果我们谈论操作系统的线程和进程,二者之间可能没有区别。例如,在Linux中,这两者是没有区别的。

撰写回答