文章目录
- 一、NumExpr 原理
- 内部支持的数据类型
- 演示示例
NumPy 虽然通过底层高度优化过的计算库可以实现接近C的高效计算,但在计算复杂且计算量庞大的时候多少还是有些慢。Numexpr 库是一个非常简单易用的 Numpy性能提升工具,很大程度上解决了性能的问题。
一、NumExpr 原理
NumExpr的运算机制是怎么样的呢?
通常,表达式是使用 Python 编译函数编译的,提取变量并构建解析树结构。然后,这个树被编译成一个字节码程序,该程序使用所谓的“向量寄存器”(每个4096个元素宽)来描述基于元素的操作流。提高速度的关键是Numexpr一次处理元素块的能力。
它跳过了Numpy使用临时数组的做法,因为临时数组会浪费内存,而且对于大型数组,甚至无法装入缓存内存中。
另外,虚拟机完全是用C编写的,这使得它比本机Python更快。它也是多线程的,允许在合适的硬件上更快地并行化操作。
NumExpr支持在表达式中使用大量的数学运算符,但不支持条件运算符,如 if 或 else。
你也可以通过设置环境变量 NUMEXPR_MAX_THREAD 来控制你想要生成的线程的数量,以便用大型数组进行并行操作。NUMEXPR_MAX_THREAD 最好是CPU的线程数,以便获得更好的加速。
import os
os.environ['NUMEXPR_MAX_THREADS'] = '16'
os.environ['NUMEXPR_NUM_THREADS'] = '8'
import numexpr as
内部支持的数据类型
NumExpr 仅在内部使用以下类型进行操作:
- 8位布尔值(布尔)
- 32位有符号整数(int或int32)
- 64位有符号整数(long或int64)
- 32位单精度浮点数(float或float32)
- 64位双精度浮点数(double或float64)
- 2x64位双精度复数(complex或complex128)
- 原始字节串(str)
另外,NumExpr条件下的类型比Python严格一些。例如,布尔值的唯一有效常量是 True和False,并且它们永远不会自动转换为整数。
NumExpr 支持以下列出的一组运算符:
- 逻辑运算符:
&, |, ~
- 比较运算符:
<, <=, ==, !=, >=, >
- 一元算术运算符:
-
- 二进制算术运算符:
+, -, *, /, **, %, <<, >>
接下来是当前支持的集合:
-
where(bool, number1, number2): number
–如果布尔条件为真,则为number1,否则为number2。 -
{sin,cos,tan}(float|complex): float|complex
–三角正弦,余弦或正切。 -
{arcsin,arccos,arctan}(float|complex): float|complex
–三角反正弦,余弦或正切。 -
arctan2(float1, float2): float
– float1 / float2的三角反正切。 -
{sinh,cosh,tanh}(float|complex): float|complex
–双曲正弦,余弦或正切。 -
{arcsinh,arccosh,arctanh}(float|complex): float|complex
–双曲反正弦,余弦或正切。 -
{log,log10,log1p}(float|complex): float|complex
–自然,以10为底和log(1 + x)对数。 -
{exp,expm1}(float|complex): float|complex
–指数和指数减一。 -
sqrt(float|complex): float|complex
- 平方根。 -
abs(float|complex): float|complex
- 绝对值。 -
conj(complex): complex
–共轭值。 -
{real,imag}(complex): float
–复数的实部或虚部。 -
complex(float, float): complex
–由实部和虚部组成。 -
contains(str, str): bool
–对于op1
包含的每个字符串,返回Trueop2
。
查看文章:
https://numexpr.readthedocs.io/projects/NumExpr3/en/latest/https://pypi.org/project/numexpr/
安装:pip install numexpr
演示示例
简单运算
>>> import numpy as np
>>> import numexpr as ne
>>> a = np.arange(1e6) # Choose large arrays for better speedups
>>> b = np.arange(1e6)
>>> ne.evaluate("a + 1") # a simple expression
array([ 1.00000000e+00, 2.00000000e+00, 3.00000000e+00, ...,
9.99998000e+05, 9.99999000e+05, 1.00000000e+06])
>>> ne.evaluate('a*b-4.1*a > 2.5*b') # a more complex one
array([False, False, False, ..., True, True, True], dtype=bool)
>>> ne.evaluate("sin(a) + arcsinh(a/b)") # you can also use functions
array([ NaN, 1.72284457, 1.79067101, ..., 1.09567006,
0.17523598, -0.09597844])
>>> s = np.array([b'abba', b'abbb', b'abbcdef'])
>>> ne.evaluate("b'abba' == s") # string arrays are supported too
array([ True, False, False], dtype=bool)
多数组复杂运算
让我们更进一步,在一个复杂的有理函数表达式中加入更多的数组。假设,我们想计算下面涉及5个Numpy数组的值,每个数组都有100万个随机数(从正态分布抽取):
我们创建一个形状(1000000,5)的Numpy数组,并从中提取5个向量(1000000,1)用于有理函数:
a = np.random.normal(size=(1000000,5))
a1,a2,a3,a4,a5 = a[:,0],a[:,1],a[:,2],a[:,3],a[:,4]
%%timeit -n100 -r10
c = (a1**2+2*a2+(3/a3))/(np.sqrt(a4**2+a5**2))
47 ms ± 220 µs per loop (mean ± std. dev. of 10 runs, 100 loops each)
%%timeit -n100 -r10
ne.evaluate("(a1**2+2*a2+(3/a3))/(sqrt(a4**2+a5**2))")
3.96 ms ± 218 µs per loop (mean ± std. dev. of 10 runs, 100 loops each)
巨大的速度提升!直接从47ms下降到4ms。实际上,这是一个趋势,你会观察到:表达式变得越复杂,涉及的数组越多,使用Numexpr的速度提升就越快!
逻辑表达式 / bool过滤
我们并不局限于简单的算术表达式。Numpy数组最有用的特征之一是直接在包含逻辑运算符(如>或<)的表达式中使用它们来创建布尔过滤器或掩码。
我们可以用NumExpr做同样的操作,并加快过滤过程。我们检测欧氏距离测量涉及的4个向量是否大于某个阈值:
x1 = np.random.random(1000000)
x2 = np.random.random(1000000)
y1 = np.random.random(1000000)
y2 = np.random.random(1000000)
%%timeit -n100 -r10
c = np.sqrt((x1-x2)**2+(y1-y2)**2) > 0.5
23.2 ms ± 143 µs per loop (mean ± std. dev. of 10 runs, 100 loops each)
%%timeit -n100 -r10
c = ne.evaluate("sqrt((x1-x2)**2+(y1-y2)**2) > 0.5")
1.86 ms ± 112 µs per loop (mean ± std. dev. of 10 runs, 100 loops each)
%%timeit -n100 -r10
c = ne.evaluate("2*a+3*b > 3.5",optimization='moderate')
763 µs ± 85.4 µs per loop (mean ± std. dev. of 10 runs, 100 loops each)
在数据科学、机器学习pipeline中,这种过滤操作经常出现,你可以使用NumExpr表达式有策略地替换Numpy计算,这样可以节省很多计算时间。
复数
NumExpor也可以很好地处理复数,Python和Numpy本身就支持复数。这里有一个例子:
a = np.random.random(1000000)
b = np.random.random(1000000)
cplx = a + b*1j
%%timeit -n100 -r10
c = np.log10(cplx)
55.9 ms ± 159 µs per loop (mean ± std. dev. of 10 runs, 100 loops each)
%%timeit -n100 -r10
c = ne.evaluate("log10(cplx)")
9.9 ms ± 117 µs per loop (mean ± std. dev. of 10 runs, 100 loops each)