首页 文章

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

提问于
浏览
753
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

为什么是这样?

3 回答

  • 459

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

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

    将此与全局查找( LOAD_GLOBAL )进行对比,这是一个涉及哈希等的真正的 dict 搜索 . 顺便说一句,这就是为什么你需要指定 global i ,如果你想要它是全局的:如果你曾经分配给一个范围内的变量,编译器将发出 STORE_FAST s进行访问,除非你告诉它不要 .

    顺便说一下,全局查找仍然相当优化 . 属性查找 foo.bar 真的很慢!

    这是关于局部变量效率的小illustration .

  • 635

    除了本地/全局变量存储时间之外, opcode prediction 使功能更快 .

    正如其他答案所解释的那样,该函数在循环中使用 STORE_FAST 操作码 . 这里's the bytecode for the function'循环:

    >>   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 (循环的顶部)时,它将"predict" STORE_FAST 是它必须执行的下一个操作码 . 然后Python会查看下一个操作码,如果预测正确,它会直接跳到 STORE_FAST . 这具有将两个操作码压缩成单个操作码的效果 .

    另一方面, STORE_NAME 操作码在全局级别的循环中使用 . 当它看到这个操作码时,Python会做 not 做类似的预测 . 相反,它必须回到评估循环的顶部,这对循环执行的速度有明显的影响 .

    要提供有关此优化的更多技术细节,请参阅ceval.c文件(Python的虚拟机的"engine"):

    某些操作码倾向于成对出现,因此可以在第一个代码运行时预测第二个代码 . 例如,GET_ITER通常后跟FOR_ITER . FOR_ITER通常后跟STORE_FAST或UNPACK_SEQUENCE . 验证预测会花费一个寄存器变量对常量的高速测试 . 如果配对良好,那么处理器自己的内部分支预测很有可能成功,导致几乎零开销转换到下一个操作码 . 成功的预测可以节省通过eval-loop的行程,包括其两个不可预测的分支,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 wiki page提供了有关CPython虚拟机如何工作的更多信息 .

  • 30

    在函数内部,字节码是

    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_FASTSTORE_NAME更快(!) . 这是因为在函数中, i 是本地的,但在顶层它是全局的 .

    要检查字节码,请使用dis module . 我能够直接反汇编函数,但是要反汇编顶层代码我必须使用compile builtin .

相关问题