为什么 Python 代码在函数中运行速度更快?

2024-12-27 08:47:00
admin
原创
126
摘要:问题描述:def main(): for i in xrange(10**8): pass main() 这段 Python 代码运行于(注:计时是用 Linux 中 BASH 中的时间函数完成的。)real 0m1.841s user 0m1.828s sys 0...

问题描述:

def main():
    for i in xrange(10**8):
        pass
main()

这段 Python 代码运行于(注:计时是用 Linux 中 BASH 中的时间函数完成的。)

real    0m1.841s
user    0m1.828s
sys     0m0.012s

但是,如果 for 循环没有放在函数中,

for i in xrange(10**8):
    pass

然后它会运行更长的时间:

real    0m4.543s
user    0m4.524s
sys     0m0.012s

这是为什么呢?


解决方案 1:

在函数内部,字节码是:

  2           0 SETUP_LOOP              20 (to 23)
              3 LOAD_GLOBAL              0 (xrange)
              6 LOAD_CONST               3 (100000000)
              9 CALL_FUNCTION            1
             12 GET_ITER            
        >>   13 FOR_ITER                 6 (to 22)
             16 STORE_FAST               0 (i)

  3          19 JUMP_ABSOLUTE           13
        >>   22 POP_BLOCK           
        >>   23 LOAD_CONST               0 (None)
             26 RETURN_VALUE        

在顶层,字节码是:

  1           0 SETUP_LOOP              20 (to 23)
              3 LOAD_NAME                0 (xrange)
              6 LOAD_CONST               3 (100000000)
              9 CALL_FUNCTION            1
             12 GET_ITER            
        >>   13 FOR_ITER                 6 (to 22)
             16 STORE_NAME               1 (i)

  2          19 JUMP_ABSOLUTE           13
        >>   22 POP_BLOCK           
        >>   23 LOAD_CONST               2 (None)
             26 RETURN_VALUE        

不同之处在于STORE_FAST比 更快 (!) STORE_NAME。这是因为在函数中i是本地的,但在顶层它是全局的。

要检查字节码,请使用dis模块。我能够直接反汇编该函数,但要反汇编顶层代码,我必须使用compile内置模块。

解决方案 2:

你可能会问为什么存储局部变量比存储全局变量更快。这是一个 CPython 实现细节。

请记住,CPython 被编译为字节码,由解释器运行。当函数被编译时,局部变量存储在一个固定大小的数组(而不是dict)中,变量名被分配给索引。这是可能的,因为您无法动态地将局部变量添加到函数中。然后检索局部变量实际上是在列表中查找指针,并在其上增加引用计数,PyObject这是微不足道的。

将其与全局查找 ( ) 进行对比,后者是涉及哈希等的LOAD_GLOBAL真正搜索。顺便说一句,这就是为什么您需要指定是否希望它是全局的:如果您曾经在作用域内分配变量,则编译器将发出s 以进行访问,除非您告诉它不要这样做。dict`global i`STORE_FAST

顺便说一句,全局查找仍然非常优化。属性查找foo.bar真的慢!

这是局部变量效率的小例子。

解决方案 3:

除了本地/全局变量存储时间之外,操作码预测使得函数更快。

正如其他答案所解释的那样,该函数STORE_FAST在循环中使用操作码。这是该函数循环的字节码:

    >>   13 FOR_ITER                 6 (to 22)   # get next value from iterator
         16 STORE_FAST               0 (x)       # set local variable
         19 JUMP_ABSOLUTE           13           # back to FOR_ITER

通常,当程序运行时,Python 会逐个执行每个操作码,跟踪堆栈并在执行每个操作码后对堆栈帧执行其他检查。操作码预测意味着在某些情况下 Python 能够直接跳转到下一个操作码,从而避免一些开销。

在这种情况下,每次 Python 看到FOR_ITER(循环顶部),它都会“预测”这STORE_FAST是它必须执行的下一个操作码。然后 Python 会查看下一个操作码,如果预测正确,它会直接跳转到STORE_FAST。这可以将两个操作码压缩为一个操作码。

另一方面,STORE_NAME操作码在全局级别循环中使用。Python在看到此操作码时不会做出类似的预测。相反,它必须返回到求值循环的顶部,这对循环执行的速度有明显的影响。

为了提供有关此优化的更多技术细节,这里引用了ceval.c文件(Python 虚拟机的“引擎”)中的一段话:

有些操作码往往成对出现,因此当第一个操作码运行时,就可以预测第二个操作码。例如,
GET_ITER后面通常跟着FOR_ITER。 并且FOR_ITER后面通常跟着STORE_FASTUNPACK_SEQUENCE

验证预测需要对寄存器变量和常量进行一次高速测试。如果配对良好,那么处理器自己的内部分支预测就很有可能成功,从而几乎零开销地转换到下​​一个操作码。成功的预测可以节省一次通过 eval 循环的行程,包括其两个不可预测的分支,即测试HAS_ARG和 switch-case。与处理器的内部分支预测相结合,成功的PREDICT预测可以使两个操作码运行起来,就像它们是合并了主体的单个新操作码一样。

我们可以在操作码的源代码中看到FOR_ITER预测的确切位置STORE_FAST

case FOR_ITER:                         // the FOR_ITER opcode case
    v = TOP();
    x = (*v->ob_type->tp_iternext)(v); // x is the next value from iterator
    if (x != NULL) {                     
        PUSH(x);                       // put x on top of the stack
        PREDICT(STORE_FAST);           // predict STORE_FAST will follow - success!
        PREDICT(UNPACK_SEQUENCE);      // this and everything below is skipped
        continue;
    }
    // error-checking and more code for when the iterator ends normally                                     

PREDICT函数扩展为if (*next_instr == op) goto PRED_##op,即我们直接跳转到预测操作码的开头。在本例中,我们跳转到此处:

PREDICTED_WITH_ARG(STORE_FAST);
case STORE_FAST:
    v = POP();                     // pop x back off the stack
    SETLOCAL(oparg, v);            // set it as the new local variable
    goto fast_next_opcode;

现在已设置局部变量,并且下一个操作码已准备好执行。Python 继续遍历可迭代对象,直到到达末尾,每次都成功做出预测。

Python 维基页面有更多有关 CPython 虚拟机如何工作的信息。

相关推荐
  政府信创国产化的10大政策解读一、信创国产化的背景与意义信创国产化,即信息技术应用创新国产化,是当前中国信息技术领域的一个重要发展方向。其核心在于通过自主研发和创新,实现信息技术应用的自主可控,减少对外部技术的依赖,并规避潜在的技术制裁和风险。随着全球信息技术竞争的加剧,以及某些国家对中国在科技领域的打压,信创国产化显...
工程项目管理   1565  
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   1354  
  信创国产芯片作为信息技术创新的核心领域,对于推动国家自主可控生态建设具有至关重要的意义。在全球科技竞争日益激烈的背景下,实现信息技术的自主可控,摆脱对国外技术的依赖,已成为保障国家信息安全和产业可持续发展的关键。国产芯片作为信创产业的基石,其发展水平直接影响着整个信创生态的构建与完善。通过不断提升国产芯片的技术实力、产...
国产信创系统   21  
  信创生态建设旨在实现信息技术领域的自主创新和安全可控,涵盖了从硬件到软件的全产业链。随着数字化转型的加速,信创生态建设的重要性日益凸显,它不仅关乎国家的信息安全,更是推动产业升级和经济高质量发展的关键力量。然而,在推进信创生态建设的过程中,面临着诸多复杂且严峻的挑战,需要深入剖析并寻找切实可行的解决方案。技术创新难题技...
信创操作系统   27  
  信创产业作为国家信息技术创新发展的重要领域,对于保障国家信息安全、推动产业升级具有关键意义。而国产芯片作为信创产业的核心基石,其研发进展备受关注。在信创国产芯片的研发征程中,面临着诸多复杂且艰巨的难点,这些难点犹如一道道关卡,阻碍着国产芯片的快速发展。然而,科研人员和相关企业并未退缩,积极探索并提出了一系列切实可行的解...
国产化替代产品目录   28  
热门文章
项目管理软件有哪些?
云禅道AD
禅道项目管理软件

云端的项目管理软件

尊享禅道项目软件收费版功能

无需维护,随时随地协同办公

内置subversion和git源码管理

每天备份,随时转为私有部署

免费试用