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


    哈喽大家好,我是咸鱼

    当谈到编程效率和性能优化时,Python 常常被调侃为“慢如蜗牛”

    有趣的是,Python 代码在函数中运行往往比在全局范围内运行要快得多

    小伙伴们可能会有这个疑问:为什么在函数中运行的 Python 代码速度更快?

    今天这篇文章将会解答大家心中的疑惑

    原文链接:https://stackabuse.com/why-does-python-code-run-faster-in-a-function/

    译文

    要理解为什么 Python 代码在函数中运行得更快,我们需要首先了解 Python 是如何执行代码的

    我们知道,python 是一种解释型语言,它会逐行读取并执行代码

    当运行一个 python 程序的时候,首先将代码编译成字节码(一种更接近机器码的中间语言)然后 python 解释器执行字节码

    def hello_world():
        print("Hello, World!")
    
    import dis
    dis.dis(hello_world)
    
    #结果
      2           0 LOAD_GLOBAL              0 (print)
                  2 LOAD_CONST               1 ('Hello, World!')
                  4 CALL_FUNCTION            1
                  6 POP_TOP
                  8 LOAD_CONST               0 (None)
                 10 RETURN_VALUE
    

    由上所示,python 中的 dis 模块将函数 hello_world 分解为字节码

    需要注意的是,python 解释器是一个执行字节码的虚拟机,默认的 python 解释器是用 C 编写的,即 CPython

    还有其他的 python 解释器如 Jython(用 Java 编写),IronPython(用于 .net)和PyPy(用 Python 和 C 编写)

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

    我们来编写一个简单的例子:定义一个函数 my_function,函数内部包含一个 for 循环

    def my_function():
        for i in range(100000000):
            pass
    

    编译该函数的时候,字节码可能如下所示

      SETUP_LOOP              20 (to 23)
      LOAD_GLOBAL             0 (range)
      LOAD_CONST              3 (100000000)
      CALL_FUNCTION           1
      GET_ITER            
      FOR_ITER                6 (to 22)
      STORE_FAST              0 (i)
      JUMP_ABSOLUTE           13
      POP_BLOCK           
      LOAD_CONST              0 (None)
      RETURN_VALUE
    

    这里的关键指令是 STORE_FAST ,用于存储循环变量 i

    现在我们把这个 for 循环放在 python 脚本的顶层(全局范围内),然后再来看一下字节码

    for i in range(100000000):
    	pass
    
      SETUP_LOOP              20 (to 23)
      LOAD_NAME               0 (range)
      LOAD_CONST              3 (100000000)
      CALL_FUNCTION           1
      GET_ITER            
      FOR_ITER                6 (to 22)
      STORE_NAME              1 (i)
      JUMP_ABSOLUTE           13
      POP_BLOCK           
      LOAD_CONST              2 (None)
      RETURN_VALUE
    

    可以看到关键指令变成了 STORE_NAME,而不是 STORE_FAST

    字节码 STORE_FASTSTORE_NAME 快,因为在函数中,局部变量存储在固定长度的数组中,而不是存储在字典中。这个数组可以通过索引直接访问,使得变量检索非常快

    基本上,它只是一个指向列表的指针,并增加了 PyObject 的引用计数,这两个都是高效的操作

    另一方面,全局变量存储在一个字典。当访问全局变量时,Python 必须执行哈希表查找,这涉及计算哈希值,然后检索与之关联的值

    虽然经过优化,但仍然比基于索引的查找慢

    基准测试验证

    我们知道在 Python 中,代码执行的速度取决于代码执行的位置——在函数中还是在全局作用域中

    让我们用一个简单的基准测试的例子来比较一下

    首先定义一个求阶乘的函数

    def factorial(n):
        result = 1
        for i in range(1, n + 1):
            result *= i
        return result
    

    然后在全局范围内执行相同的代码

    n = 20
    result = 1
    for i in range(1, n + 1):
        result *= i
    

    为了对这两段代码进行基准测试,我们可以在 Python 中使用 timeit 模块,它提供了一种简单的方法来对少量 Python 代码进行计时

    import timeit
    
    # 函数
    def benchmark():
        start = timeit.default_timer()
    
        factorial(20)
    
        end = timeit.default_timer()
        print(end - start)
    
    benchmark()
    # Prints: 3.541994374245405e-06
    
    # 全局范围
    start = timeit.default_timer()
    
    n = 20
    result = 1
    for i in range(1, n + 1):
        result *= i
    
    end = timeit.default_timer()
    print(end - start) 
    # Pirnts: 5.375011824071407e-06
    

    可以看到,函数代码的执行速度比全局作用域代码要快

    需要注意的是,这两段代码最好不要放在同一脚本中,要分开单独运行

    这是因为 benchmark() 函数在执行时间上增加了一些开销,并且全局代码在内部进行了优化

    cProfile 分析

    python 提供了一个 cProfile 内置模块

    让我们用它来分析一个新例子:在局部和全局范围内计算平方和

    import cProfile
    
    def sum_of_squares():
        total = 0
        for i in range(1, 10000000):
            total += i * i
    
    i = None
    total = 0
    def sum_of_squares_g():
        global i
        global total
        for i in range(1, 10000000):
            total += i * i
        
    def profile(func):
        pr = cProfile.Profile()
        pr.enable()
    
        func()
    
        pr.disable()
        pr.print_stats()
    #
    # Profile function code
    #
    print("Function scope:")
    profile(sum_of_squares)
    
    #
    # Profile global scope code
    #
    print("Global scope:")
    profile(sum_of_squares_g)
    

    上面的例子中,可以认为sum_of_squares_g() 函数是全局的,因为它使用了两个全局变量, itotal

    从性能分析结果中,可以看到函数代码在执行时间方面比全局更有效

    Function scope:
             2 function calls in 0.903 seconds
    
       Ordered by: standard name
    
       ncalls  tottime  percall  cumtime  percall filename:lineno(function)
       1       0.903    0.903    0.903    0.903 profiler.py:3(sum_of_squares)
       1       0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
    
    
    Global scope:
             2 function calls in 1.358 seconds
    
       Ordered by: standard name
    
       ncalls  tottime  percall  cumtime  percall filename:lineno(function)
       1       1.358    1.358    1.358    1.358 profiler.py:10(sum_of_squares_g)
       1       0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
    

    如何优化 python 函数的性能

    前面我们知道,Python 代码在函数中运行往往比在全局范围内运行要快得多

    如果想要进一步提高 python 函数代码效率,不妨考虑一下使用局部变量而不是全局变量

    另一种方法是尽可能使用内置函数和库。Python 的内置函数是用 C 实现的,比 Python 快得多

    比如 NumPy 和 Pandas,也是用 C 或 C++ 实现的,它们比实现同样功能的 Python 代码速度更快

    又比如同样是实现数字求和的功能,python 内置的 sum 函数要比你自己编写函数速度更快

  • 相关阅读:
    Servlet开发-session和cookie理解案例-登录页面
    Rocky/GNU之Zabbix部署(1)
    CSS 清除浮动
    一个动态规划的简单例题
    How to Set Profiles for Spring
    k8s、数据存储
    防火墙内容安全笔记
    DDD领域驱动设计-实体
    Flutter 中的 IconTheme 小部件:全面指南
    7-16 城市间紧急救援 (综合最短路练习)
  • 原文地址:https://www.cnblogs.com/edisonfish/p/17715522.html