Python 中的解释与动态调度惩罚

2024-02-16

我观看了 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

并使用 ipython-magic 来计时:

In [11]: %timeit py_fun(0.0,1.0e5,1.0)
10 loops, best of 3: 25.4 ms per loop

解释器将运行生成的字节码并解释它。但是,我们可以通过使用 cython for/cythonizing 相同的代码来删除解释器:

%load_ext Cython
%%cython
def cy_fun(i,N,step):
     res=0.0
     while i<N:
        res+=i
        i+=step
     return res

我们的速度提高了 50%:

In [13]: %timeit cy_fun(0.0,1.0e5,1.0)
100 loops, best of 3: 10.9 ms per loop

当我们查看生成的 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_InPlaceAdd有必要弄清楚这些对象(整数,浮点数,其他什么?)到底是什么,并将此调用分派给可以完成这项工作的正确函数。

在 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

在这个版本中,i, N, step and res是 c 风格的双精度数,不再是 python 对象。所以不再需要像这样调用调度函数PyNumber_InPlaceAdd但我们可以直接调用+- 运算符double:

static PyObject *__pyx_pf_4test_c_fun(CYTHON_UNUSED PyObject *__pyx_self, double __pyx_v_i, double __pyx_v_N, double __pyx_v_step) {
  ...
  __pyx_v_res = 0.0;  
  ... 
  while (1) {
    __pyx_t_1 = ((__pyx_v_i < __pyx_v_N) != 0);
    if (!__pyx_t_1) break;
    __pyx_v_res = (__pyx_v_res + __pyx_v_i);
    __pyx_v_i = (__pyx_v_i + __pyx_v_step);
  }
  ...
  return __pyx_r;
}

结果是:

In [15]: %timeit c_fun(0.0,1.0e5,1.0)
10000 loops, best of 3: 148 µs per loop

现在,与没有解释器但有调度的版本相比,速度提高了近 100。

实际上,说调度+分配是这里的瓶颈(因为消除它导致速度几乎提高了 100 倍)是一个谬论:解释器负责超过 50% 的运行时间(15 毫秒),并且调度和分配“仅”10ms。


然而,对于性能来说,除了“解释器”和动态分派之外,还有更多问题: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)

时间(现在我使用不同的机器,所以时间有点不同):

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)

不要忘记重新初始化i因为它是可变的!结果

            immutable       mutable
 py_fun       30ms           23ms
 cy_fun       17ms           11ms

因此,在带有解释器的版本中,注册/取消注册浮点数最多需要 7 毫秒(大约 20%)(我不确定是否有其他东西在起作用),而在没有解释器的版本中,需要超过 33% 的时间。

现在看起来:

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

另一个问题是数据的局部性,这对于内存带宽限制问题来说变得很明显:如果数据在一个又一个连续的内存地址上线性处理,那么现代缓存可以很好地工作。这对于循环来说是正确的std::vector<> (or array.array),但不适用于循环 python 列表,因为该列表由可以指向内存中任何位置的指针组成。

考虑以下 python 脚本:

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

and

#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))

他们都创造1e7整数,第一个版本的Python整数和第二个版本的低级c整数,它们连续放置在内存中。

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

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

versus

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% 确定,因为相邻整数是在彼此之后创建的,所以机会很高) ,它们被存储在内存中的某个地方,需要进一步调查)一些是因为它们在内存中没有对齐,就像 c 整数的情况一样bytearray.

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Python 中的解释与动态调度惩罚 的相关文章

随机推荐

  • 泛型类中的静态方法?

    在Java中 我想要这样的东西 class Clazz
  • ServerManager CommitChanges 进行更改时略有延迟

    我对 ServerManager 类 来自 Microsoft Web Administration 程序集 有一个小问题 我希望你们中的一些人可以帮助我 基本上 我需要在站点内创建一个新应用程序 使用 IIS 7 5 并将用户重定向到同一
  • 使用 git-svn:拉取、合并还是变基?

    我一直在与 git git svn 学习曲线作斗争 昨晚 作为学习曲线的一部分 我做了一些非常非常糟糕的事情 我已经纠正了它 但我希望以我的方式理解错误 我有一个 svn 存储库 我从中克隆了主干和分支 我忽略了标签 因为我们不处理这些标签
  • tinyMCE模糊事件

    你好 当用户在tinyMCE文本区域中完成书写并单击外部某处 onBlur 时 我想做一些事情 到目前为止我已经尝试过 id topic text parent live blur function alert asd I saw id t
  • 为什么在 C++ 中我们需要使用 `int main` 而不是 `void main`? [复制]

    这个问题在这里已经有答案了 为什么我们需要使用int main并不是void main in C 简短的回答是因为 C 标准要求main 回来int 您可能知道 返回值main 运行时库使用函数作为进程的退出代码 Unix 和 Win32
  • 如何从切片中删除最后一个元素?

    我见过有人说只需通过附加旧切片来创建一个新切片 slc append slc item slc item 1 但是如果你想删除切片中的最后一个元素怎么办 如果您尝试更换i 最后一个元素 与i 1 它返回越界错误 因为没有i 1 您可以使用l
  • Keras“pickle_safe”:Python 中的“pickle 安全”或“不可 picklable”是什么意思?

    Keras fit generator 有一个参数pickle safe默认为False 如果有的话 训练可以跑得更快ispickle safe 并相应地将标志设置为True 根据Kera 的文档 https keras io models
  • JMock- java.lang.NoSuchMethodError: org.hamcrest.Matcher.describeMismatch()

    我知道解决方案是以某种方式确保 Junit 在 hamcrest 之后加载 我有一个 intellij 项目 在其中设置了一个外部库 其中包含 JUnit 和 JMock 以及 hamcrest 我怎样才能确保这个错误不会出现 junit
  • 迭代 Numpy 矩阵行以每行应用一个函数?

    我希望能够迭代矩阵以将函数应用于每一行 我该如何为 Numpy 矩阵做到这一点 您可以使用numpy apply along axis 假设你的数组是二维的 你可以像下面这样使用它 import numpy as np myarray np
  • 零长度数组

    我正在重构一些旧代码 并发现一些包含零长度数组的结构 如下 当然 警告被 pragma 抑制 但我无法通过包含此类结构的 新 结构创建 错误 2233 数组 byData 用作指针 但为什么不使用指针呢 或者长度为1的数组 当然 没有添加任
  • Ruby class_eval 方法

    我想弄清楚如何动态创建方法 class MyClass def initialize dynamic methods arr Array new dynamic methods arr each m self class class eva
  • Kotlin 中的普通类和数据类有什么区别?

    我尝试解决任务 6 DataClass 科特林公案 https github com vicboma1 Kotlin Koans named arguments 当我在代码中使用普通类时 测试用例失败 这是我的数据类代码 data clas
  • CMakeExternalProject_Add() 和 FindPackage()

    是否有正确的方法来查找图书馆 通过FindPackage 是用ExternalProject Add 问题是 CMake 无法在 CMake 时找到该库 因为外部库是在编译时构建的 我知道在超级构建中构建库和项目时可以组合这两个 CMake
  • 为什么投票机不开源? [关闭]

    Closed 这个问题是基于意见的 help closed questions 目前不接受答案 嗯 这只是与编程相关 但我想今天是选举日 对吧 是否有一个很好的理由说明为什么它们不开源 不一定是开源的 因为任何人都可以做出贡献 但开源是因为
  • 我可以在 Javascript 中识别(图形输入板)笔压吗?

    有没有办法使用 javascript 来识别笔压 最好我不想使用 Flash 并尝试将其作为纯 JS 完成 编辑 好吧 我意识到 Wacom 平板电脑有可能实现这一点 因为它们附带的软件可以与其 javascript api 配合使用 从而
  • 使用 pandas 忽略来自 openpyxl 的 UserWarning

    我有大量必须加载的 xlsm 文件 每个 Excel 文件有 6 个工作表 因此 我使用 pandas 打开每个 Excel 文件 for excel file in files list with pd ExcelFile excel f
  • .Net 中字符串(或任何其他对象)的内存使用情况

    我写了这个小测试程序 using System namespace GCMemTest class Program static void Main string args System GC Collect System Diagnost
  • WordPress 预览_帖子_链接

    我试图在 WordPress 上发布时更改默认的 预览帖子 按钮 因为该网站安装了被黑客入侵的 WordPress 并且帖子预览不在应有的位置 我找到了钩子preview post link现在我只是想弄清楚如何制作一个小插件来解决这个问题
  • 更改构造函数原型时出现的问题

    我目前正在阅读 Stoyan Stefanov 的书 面向对象的 JavaScript 我偶然发现了一个有趣的问题 这是代码 var shape type shape getType function return this type fu
  • Python 中的解释与动态调度惩罚

    我观看了 Brandon Rhodes 关于 Cython 的演讲 EXE 的日子即将到来 Brandon 在 09 30 提到 对于特定的一小段代码 跳过解释可以带来 40 的加速 而跳过分配和调度则可以带来 574 的加速 10 10