Warning: file_get_contents(/data/phpspider/zhask/data//catemap/8/http/4.json): failed to open stream: No such file or directory in /data/phpspider/zhask/libs/function.php on line 167

Warning: Invalid argument supplied for foreach() in /data/phpspider/zhask/libs/tag.function.php on line 1116

Notice: Undefined index: in /data/phpspider/zhask/libs/function.php on line 180

Warning: array_chunk() expects parameter 1 to be array, null given in /data/phpspider/zhask/libs/function.php on line 181
Python中的解释与动态分派惩罚_Python_Dynamic Programming_Cython_Performance Testing_Dispatch - Fatal编程技术网

Python中的解释与动态分派惩罚

Python中的解释与动态分派惩罚,python,dynamic-programming,cython,performance-testing,dispatch,Python,Dynamic Programming,Cython,Performance Testing,Dispatch,我看了布兰登·罗德斯(Brandon Rhodes)关于赛顿(Cython)的演讲——“EXE的日子就要到了” Brandon在09:30提到,对于一段特定的短代码,跳过解释可以提供40%的加速,而跳过分配和分派可以提供574%的加速(10:10) 我的问题是——对于一段特定的代码,这是如何衡量的?是否需要手动提取底层c命令,然后让运行时运行它们 这是一个非常有趣的观察结果,但是如何重新创建实验呢?让我们看看这个python函数: def py_fun(i,N,step): res=0

我看了布兰登·罗德斯(Brandon Rhodes)关于赛顿(Cython)的演讲——“EXE的日子就要到了”

Brandon在09:30提到,对于一段特定的短代码,跳过解释可以提供40%的加速,而跳过分配和分派可以提供574%的加速(10:10)

我的问题是——对于一段特定的代码,这是如何衡量的?是否需要手动提取底层c命令,然后让运行时运行它们


这是一个非常有趣的观察结果,但是如何重新创建实验呢?

让我们看看这个python函数:

def py_fun(i,N,step):
     res=0.0
     while i<N:
        res+=i
        i+=step
     return res
解释器将运行结果字节码并对其进行解释。但是,我们可以通过使用cython对完全相同的代码进行/cythonizing来切断解释器:

%load_ext Cython
%%cython
def cy_fun(i,N,step):
     res=0.0
     while i<N:
        res+=i
        i+=step
     return res
当我们查看生成的c代码时,我们看到在剥离样板代码后,直接调用正确的函数,而无需解释/调用
ceval

static PyObject *__pyx_pf_4test_cy_fun(CYTHON_UNUSED PyObject *__pyx_self, PyObject *__pyx_v_i, PyObject *__pyx_v_N, PyObject *__pyx_v_step) {
  ...
  while (1) {
    __pyx_t_1 = PyObject_RichCompare(__pyx_v_i, __pyx_v_N, Py_LT); 
    ...
    __pyx_t_2 = __Pyx_PyObject_IsTrue(__pyx_t_1);
    ...
    if (!__pyx_t_2) break;
    ...
    __pyx_t_1 = PyNumber_InPlaceAdd(__pyx_v_res, __pyx_v_i);
    ...
    __pyx_t_1 = PyNumber_InPlaceAdd(__pyx_v_i, __pyx_v_step); 
  }
  ...
  return __pyx_r;
}
但是,这个cython函数处理python对象,而不是c风格的浮点,因此在函数
PyNumber\u inpeaceadd
中,有必要弄清楚这些对象(整数、浮点等)到底是什么,并将此调用分派给执行此任务的正确函数

在cython的帮助下,我们还可以消除此调度的需要,并直接调用浮点乘法:

 %%cython
 def c_fun(double i,double N, double step):
      cdef double res=0.0
      while i<N:
         res+=i
         i+=step
      return res
结果是:

In [15]: %timeit c_fun(0.0,1.0e5,1.0)
10000 loops, best of 3: 148 µs per loop
现在,与没有解释器但有调度的版本相比,这一速度提高了近100

实际上,可以说,分派+分配是这里的瓶颈(因为消除它会导致几乎100倍的速度提高)是一个谬误:口译员负责50%以上的运行时间(15毫秒),分派和分配“仅”10毫秒


然而,对于性能来说,除了“解释器”和动态调度之外,还有更多的问题:Float是不可变的,因此每次它更改时,都必须在垃圾收集器中创建并注册/注销一个新对象

我们可以引入可变浮点数,这些浮点数可以就地更改,不需要注册/注销:

%%cython
cdef class MutableFloat: 
 cdef double x      
 def __cinit__(self, x):
    self.x=x         
 def __iadd__(self, MutableFloat other):
    self.x=self.x+other.x
    return self
 def __lt__(MutableFloat self,  MutableFloat other):
    return self.x<other.x
 def __gt__(MutableFloat self, MutableFloat other):
    return self.x>other.x
 def __repr__(self):
    return str(self.x)
因此,在有解释器的版本中,注册/取消注册浮动(我不确定是否有其他东西在起作用)需要7毫秒(约20%),而在没有解释器的版本中,需要超过33%

现在看来:

  • 40%(13/30)的时间由口译员使用
  • 高达33%的时间用于动态调度
  • 多达20%的时间用于创建/删除临时对象
  • 算术运算大约1%

另一个问题是数据的局部性,这对于内存带宽限制的问题来说变得很明显:现代缓存对于一个接一个连续的内存地址线性处理的数据非常有效。这适用于在
std::vector
(或
array.array
)上循环,但不适用于在python列表上循环,因为此列表包含可以指向内存中任何位置的指针

考虑以下python脚本:

#list.py
N=int(1e7)
lst=[0]*int(N)
for i in range(N):
  lst[i]=i
print(sum(lst)) 

它们都创建
1e7
整数,第一个版本是Python整数,第二个版本是低位c-int,它们连续放置在内存中

有趣的是,这些脚本会产生多少缓存未命中(D):

valgrind --tool=cachegrind python list.py 
...
D1  misses:        33,964,276  (   27,473,138 rd   +     6,491,138 wr)

valgrind --tool=cachegrind python bytearray.py 
...
D1  misses:         4,796,626  (    2,140,357 rd   +     2,656,269 wr)

这意味着python整数的缓存未命中率增加了8倍。部分原因是,python整数需要超过8个字节(可能是32个字节,即因子4)的内存,(可能不是100%确定,因为相邻整数是在彼此之后创建的,所以可能性很高,它们被存储在内存中的某个位置,需要进一步研究)一些原因是,它们在内存中没有对齐,就像bytearray的c-整数一样。下面是本次演讲的源代码:代码似乎与cython/nuitka有关,我没有看到Brandon的初始语句的任何代码片段。您是否尝试过使用cython编译python代码片段?我通常希望使用cython编译纯python应用程序的速度能提高约50%,因为它消除了Brandon在演讲中讨论的解释器开销。这似乎是正确的:)为了完整性,您可以添加已编译cython文件的位置吗?@Jay我猜在此期间,您已经发现临时文件在
~/.ipython/cython
@Jay顺便说一句,从“intepreter 40%的速度,调度60倍的速度”得出调度是瓶颈的结论是一个谬误:它们都需要10毫秒左右的时间,所以两者都同样“差”或“好”。看看我更新的答案,还有一些你可能会感兴趣的实验。
def py_fun(i,N,step,acc):
        while i<N:
             acc+=i
             i+=step
        return acc

%timeit py_fun(1.0, 5e5,1.0,0.0)
30.2 ms ± 1.12 ms per loop (mean ± std. dev. of 7 runs, 10 loops each 
%timeit cy_fun(1.0, 5e5,1.0,0.0)
16.9 ms ± 612 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
%timeit i,N,step,acc=MutableFloat(1.0),MutableFloat(5e5),MutableFloat(1
    ...: .0),MutableFloat(0.0); py_fun(i,N,step,acc)
23 ms ± 254 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
%timeit i,N,step,acc=MutableFloat(1.0),MutableFloat(5e5),MutableFloat(1
...: .0),MutableFloat(0.0); cy_fun(i,N,step,acc)
11 ms ± 66.2 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
            immutable       mutable
 py_fun       30ms           23ms
 cy_fun       17ms           11ms
#list.py
N=int(1e7)
lst=[0]*int(N)
for i in range(N):
  lst[i]=i
print(sum(lst)) 
#byte
N=int(1e7)
b=bytearray(8*N)
m=memoryview(b).cast('L') #reinterpret as an array of unsigned longs
for i in range(N):
  m[i]=i
print(sum(m))
valgrind --tool=cachegrind python list.py 
...
D1  misses:        33,964,276  (   27,473,138 rd   +     6,491,138 wr)
valgrind --tool=cachegrind python bytearray.py 
...
D1  misses:         4,796,626  (    2,140,357 rd   +     2,656,269 wr)