深度解析Python性能优化与GIL的那些事20240918

深度解析Python性能优化与GIL的那些事

在Python的世界中,性能优化一直是开发者关注的焦点之一。本文将深入探讨Python的GIL(全局解释器锁),了解其对多线程的影响,以及如何利用各种工具和方法进行性能分析与优化。

引言

Python以其简单易用的特性赢得了众多开发者的青睐。然而,在性能和多线程方面,Python却常常被诟病。究其原因,GIL扮演了关键角色。那么,GIL究竟是什么?它如何影响我们的程序性能?又该如何优化?本文将为您一一揭晓。

什么是GIL?

GIL是CPython本身自带的机制吗?

是的,GIL是CPython解释器的内置机制,并非人为额外添加的。GIL(Global Interpreter Lock,全局解释器锁)是CPython为保证线程安全而引入的一种锁机制。它确保在任何时候,都只有一个线程在执行Python的字节码。

GIL的由来
  • 设计初衷:早期的CPython解释器采用引用计数来管理内存,而引用计数的增减操作需要是线程安全的。为了避免在对象的引用计数上加锁(这会导致性能下降),CPython选择了更为简单的方式,即引入GIL。
  • 内存管理的考虑:由于CPython的内存管理和垃圾回收机制并不是线程安全的,因此需要一种机制来防止多个线程同时执行字节码,导致内存访问冲突和数据不一致。

GIL的作用

  • 线程同步:GIL使得CPython解释器在同一时刻只执行一个线程的字节码,防止了多线程同时访问和修改对象,保证了解释器级别的线程安全。
  • 影响多核利用:由于GIL的存在,即使在多核CPU上,CPython的多线程程序也无法实现真正的并行执行,限制了CPU密集型程序的性能。

深入理解字节码操作

在Python中,源代码会被编译成字节码,然后由解释器执行。理解字节码操作有助于我们深入了解Python的执行过程,以及GIL对线程执行的影响。

什么是字节码?

字节码是Python代码被编译后的中间表示形式,是一种与平台无关的二进制指令集。Python的虚拟机(解释器)逐条读取并执行这些字节码指令。

使用dis模块分析字节码

import disdef example_function(a):a += 1return adis.dis(example_function)

输出:

  2           0 LOAD_FAST                0 (a)2 LOAD_CONST               1 (1)4 INPLACE_ADD6 STORE_FAST               0 (a)3           8 LOAD_FAST                0 (a)10 RETURN_VALUE

解释:

  • LOAD_FAST 0 (a):将变量a加载到栈顶。
  • LOAD_CONST 1 (1):将常量1加载到栈顶。
  • INPLACE_ADD:对栈顶的两个值执行就地加法,并将结果放回栈顶。
  • STORE_FAST 0 (a):将栈顶的值存储回变量a
  • LOAD_FAST 0 (a):再次将变量a加载到栈顶,以准备返回。
  • RETURN_VALUE:返回栈顶的值。

从字节码可以看出,a += 1并非一个原子操作,而是由多条指令组成。这意味着在执行这些指令的过程中,可能发生线程切换,导致线程安全问题。

GIL对多线程的影响

CPU密集型任务

对于需要大量计算的CPU密集型任务,由于GIL的存在,同一时间只能有一个线程执行Python字节码,导致无法充分利用多核CPU的优势。

I/O密集型任务

对于I/O密集型任务(如文件读写、网络请求),Python在进行I/O操作时会释放GIL,允许其他线程执行。因此,GIL对I/O密集型任务的影响较小,可以通过多线程提高程序的并发性能。

为什么有了GIL,还要关注线程安全?

**GIL并不能保证我们编写的代码都是线程安全的。**虽然GIL确保了同一时刻只有一个线程执行Python字节码,但在执行多条字节码指令的过程中,可能发生线程切换,导致数据竞争。

原子性操作的定义

原子操作是指在执行过程中不可被中断的操作,要么全部执行完毕,要么完全不执行。对于Python的一些简单操作,可能对应单个字节码指令,是原子的。但更多的操作是由多条字节码指令组成的,可能在指令之间被其他线程打断。

示例:线程不安全的操作

import threadingn = [0]def increment():n[0] += 1threads = []for _ in range(10000):t = threading.Thread(target=increment)threads.append(t)t.start()for t in threads:t.join()print(n[0])

预期结果:n[0]应该等于10000。

**实际结果:**可能小于10000,例如9998。

原因分析:

n[0] += 1并非原子操作,而是由以下步骤组成:

  1. 读取n[0]的值(LOAD)。
  2. 将其与1相加(ADD)。
  3. 将结果写回n[0](STORE)。

在执行这三个步骤的过程中,可能发生线程切换。例如:

  • 线程A读取了n[0]的值为100
  • 线程A计算100 + 1 = 101
  • 线程切换到线程B
  • 线程B读取了n[0]的值(仍为100)。
  • 线程B计算100 + 1 = 101
  • 线程A将结果101写回n[0]
  • 线程B将结果101写回n[0]

结果,n[0]只增加了一次,导致计数丢失。

使用dis模块分析操作

import disdef increment():n[0] += 1dis.dis(increment)

输出的字节码:

  2           0 LOAD_GLOBAL              0 (n)2 LOAD_CONST               1 (0)4 DUP_TOP_TWO6 BINARY_SUBSCR8 LOAD_CONST               2 (1)10 INPLACE_ADD12 ROT_THREE14 STORE_SUBSCR16 LOAD_CONST               0 (None)18 RETURN_VALUE

解释:

  • 该操作并非单一的原子操作,而是由多条字节码指令组成。
  • 在执行过程中,可能在任意字节码指令之间发生线程切换。

解决方法:使用锁确保线程安全

import threadingn = [0]
lock = threading.Lock()def increment():with lock:n[0] += 1threads = []for _ in range(10000):t = threading.Thread(target=increment)threads.append(t)t.start()for t in threads:t.join()print(n[0])

通过在操作前获取锁,可以确保整个操作的原子性,防止线程切换导致的数据竞争。

如何规避GIL的影响

区分任务类型

  • CPU密集型任务:使用multiprocessing模块创建多进程,充分利用多核CPU。
  • I/O密集型任务:使用多线程或协程,如asyncio,提高程序的并发性能。

多进程示例

from multiprocessing import Pooldef cpu_bound_task(n):# 计算密集型任务return sum(i * i for i in range(n))if __name__ == '__main__':with Pool() as pool:results = pool.map(cpu_bound_task, [1000000] * 10)

协程示例

import asyncioasync def io_bound_task():# I/O密集型任务await asyncio.sleep(1)async def main():tasks = [io_bound_task() for _ in range(100)]await asyncio.gather(*tasks)asyncio.run(main())

如何分析程序性能

二八定律

根据二八定律,程序中80%的性能问题源自20%的代码。因此,找出性能瓶颈是优化的关键。

使用Profiling工具

  • 内置工具profilecProfile
  • 第三方工具line_profilerpyflame

示例:使用cProfile

import cProfiledef main():# 主函数passif __name__ == '__main__':cProfile.run('main()')

假设我们有以下脚本pycls_3_5_gil.py

import cProfiledef main():pass  # 主函数逻辑if __name__ == '__main__':cProfile.run('main()')

运行结果:

D:\Python38-64\python.exe D:/git_new_src/KidsTutorAndEfficiencyScripts/interview_python/pycls_3_5_gil.py4 function calls in 0.000 secondsOrdered by: standard namencalls  tottime  percall  cumtime  percall filename:lineno(function)1    0.000    0.000    0.000    0.000 <string>:1(<module>)1    0.000    0.000    0.000    0.000 pycls_3_5_gil.py:97(main)1    0.000    0.000    0.000    0.000 {built-in method builtins.exec}1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}进程已结束,退出代码0

解释:

  • ncalls:函数被调用的次数。
  • tottime:函数自身的运行时间,不包括调用子函数的时间。
  • percalltottime除以调用次数,即平均每次调用的时间。
  • cumtime:函数运行的总时间,包括所有子函数的运行时间。
  • percallcumtime除以调用次数,即平均每次调用的总时间。
  • filename:lineno(function):函数所在的文件、行号和名称。

在这个简单的示例中,我们可以看到main函数被调用了一次,运行时间几乎为零。这是因为main函数中并没有实际的逻辑。如果在main函数中添加实际的代码,那么cProfile将提供更详细的性能数据,帮助我们定位性能瓶颈。

更复杂的示例

假设我们在main函数中添加一些逻辑:

def main():total = 0for i in range(100000):total += iprint(total)

再次运行cProfile,将得到类似如下的输出:

         100004 function calls in 0.012 secondsOrdered by: standard namencalls  tottime  percall  cumtime  percall filename:lineno(function)1    0.005    0.005    0.012    0.012 <string>:1(<module>)1    0.007    0.007    0.007    0.007 pycls_3_5_gil.py:97(main)...

现在,我们可以看到main函数的运行时间,以及循环内部的性能消耗。

火焰图分析

火焰图是一种可视化工具,用于展示程序在运行期间的CPU或内存消耗情况。通过火焰图,我们可以直观地看到函数调用的层次结构和性能消耗。

使用pyflame生成火焰图

pyflame是Uber开源的一个性能分析工具,可以为Python程序生成火焰图。

步骤:

  1. 安装pyflameflamegraph工具:

    • pyflame需要在Linux系统上编译安装,具体请参考pyflame的GitHub页面。
    • flamegraph是一个Perl脚本,用于生成火焰图,下载地址:FlameGraph。
  2. 运行程序并收集数据:

    pyflame -o profile.txt -t python your_script.py
    

    这将生成一个包含采样数据的profile.txt文件。

  3. 生成火焰图:

    cat profile.txt | ./flamegraph.pl > flamegraph.svg
    

    这将生成一个可视化的火焰图文件flamegraph.svg

解释火焰图
  • 水平轴(X轴):表示调用栈的快照,宽度表示该函数被调用的频率或消耗的时间。
  • 垂直轴(Y轴):表示调用栈的深度,越高表示调用关系越深。
  • 每个矩形块:表示一个函数调用,块的宽度与其耗时成正比。

通过火焰图,我们可以:

  • 直观地找到耗时最多的函数或代码路径。
  • 分析调用关系,了解性能瓶颈所在。
  • 优化关键路径,提升程序性能。
简单示例

假设我们有以下脚本performance_test.py

import timedef func_a():time.sleep(0.1)def func_b():time.sleep(0.2)def main():for _ in range(5):func_a()func_b()if __name__ == '__main__':main()

生成火焰图:

  1. 运行采样:

    pyflame -o profile.txt -t python performance_test.py
    
  2. 生成火焰图:

    cat profile.txt | ./flamegraph.pl > flamegraph.svg
    

分析火焰图:

  • func_b的矩形块比func_a宽,表示func_b消耗的时间更多。
  • 总体来看,程序的大部分时间消耗在time.sleep函数中。

通过火焰图,我们可以直观地看到程序的性能分布,进而进行有针对性的优化。

Python Web服务性能优化

语言并非瓶颈

在Web应用中,性能瓶颈往往不在于语言本身,而在于数据库、网络I/O等环节。

优化策略

  1. 数据结构和算法优化:选择合适的数据结构,优化算法,提高代码效率。
  2. 数据库优化
    • 建立合理的索引。
    • 消除慢查询。
    • 使用批量操作,减少数据库I/O。
    • 引入NoSQL数据库,满足特定需求。
  3. 网络I/O优化
    • 使用批量请求。
    • 采用Pipeline技术,减少网络往返次数。
  4. 缓存机制
    • 使用Redis或Memcached等内存数据库,缓存热点数据。
  5. 异步框架和库
    • 使用asyncio构建异步I/O。
    • 采用celery进行任务异步处理。
  6. 并发工具
    • 利用gevent实现协程。
    • 在I/O密集型任务中使用多线程。

结论

GIL是CPython解释器的内置机制,旨在简化内存管理,保证解释器级别的线程安全。然而,它也限制了多线程的并发性能。通过深入理解GIL的工作原理,了解Python字节码的执行过程,我们可以在编写多线程程序时,注意线程安全问题,使用合适的同步机制。

同时,结合任务类型选择合适的并发模型,利用多进程、协程等方式规避GIL的影响,以及使用各种性能分析工具(如cProfile、火焰图)对程序进行分析,我们可以有效地优化Python程序的性能,提升应用的效率和响应速度。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/145948.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

erlang学习:Linux常用命令2

目录操作命令 对目录进行基本操作 相关cd切换目录之类的就直接省去了&#xff0c;以下操作中都会用到 查看当前目录下的所有目录和文件 ls 列表查看当前目录下的所有目录和文件&#xff08;列表查看&#xff0c;显示更多信息&#xff09; ls -l 或 ll 在当前目录下创建一个…

高性能并发计数器的比较

参考文档&#xff1a;https://baijiahao.baidu.com/s?id1742540809477784106&wfrspider&forpc 一、常用的并发计数方法 1、synchronized synchronized早期是一个重量级锁&#xff0c;因为线程竞争锁会引起操作系统用户态和内核态切换&#xff0c;浪费资源&#xff…

Element Plus中button按钮相关大全

一、基本用法 使用 type、plain、round 和 circle 来定义按钮的样式。 样式代码如下&#xff1a; <template><div class"mb-4"><el-button>Default</el-button><el-button type"primary">Primary</el-button><el…

C语言常见字符串函数模拟实现一

strlen模拟实现 重点&#xff1a;1.字符串已经\0作为结束标志&#xff0c;strlen返回的是字符串\0前面出现的字符个数&#xff08;不包含\0&#xff09; 2.参数指向的字符串必须要以\0结束。 3.注意函数的返回值是size_t&#xff0c;是无符号的&#xff0c;加减是无法对比的。…

卡西欧相机SD卡格式化后数据恢复指南

在数字摄影时代&#xff0c;卡西欧相机以其卓越的性能和便携性成为了众多摄影爱好者的首选。然而&#xff0c;随着拍摄量的增加&#xff0c;SD卡中的数据管理变得尤为重要。不幸的是&#xff0c;有时我们可能会因为操作失误或系统故障而将SD卡格式化&#xff0c;导致珍贵的照片…

数据类型转换中存在的问题分析

隐式类型转换&#xff08;implicit type conversion&#xff09; 隐式类型转换&#xff08;implicit type conversion&#xff09;包括整型提升&#xff08;integer promotion&#xff09;和标准算数转换&#xff08;usual arithmetic conversions&#xff09; 遵循较大范围优…

堡垒机(Bastion Host)概述

Bastion Host 堡垒机 一、什么是堡垒机&#xff1f; A bastion host is a computer specially designed to mitigate cyberattacks and manage access rights to an internal network. 堡垒机Bastion Host是一种专门设计用于缓解网络攻击并管理内部网络访问权限的计算机。 在…

肖扬新书《微权力下的项目管理》读书笔记2

一个核心思想&#xff1a;“借力” 合格的项目经理是不热衷于培养人的。项目经理的工作场景和职能经理的工作场景往往有很 大不同。职能经理的工作方式通常适用于常态化工作&#xff0c;要有足够的时间去培养人&#xff0c;先把人培 养起来&#xff0c;然后再干事&#xff0c;可…

加油卡APP定制搭建,让加油更便捷!

在汽车时代中&#xff0c;汽车的数量不断增加&#xff0c;加油已经成为了大众生活中不可缺少的一部分。同时&#xff0c;加油卡的出现也为大众的汽车加油提供了更多的优惠方式&#xff0c;为大众节省经济开支&#xff0c;为车主带来便利&#xff1b;同时加油卡的发展也提高了加…

2024年华为杯研赛(E题)数学建模竞赛解题思路|完整代码论文集合

我是Tina表姐&#xff0c;毕业于中国人民大学&#xff0c;对数学建模的热爱让我在这一领域深耕多年。我的建模思路已经帮助了百余位学习者和参赛者在数学建模的道路上取得了显著的进步和成就。现在&#xff0c;我将这份宝贵的经验和知识凝练成一份全面的解题思路与代码论文集合…

如何远程访问局域网内的电脑?局域网内远程桌面怎么实现?揭秘4种干货技巧

想象一下&#xff0c;你正在办公室A&#xff0c;而你想访问办公室B里的某台电脑&#xff0c;却不想起身到另一楼层甚至是另一个房间。 如何不动身就能控制局域网内的另一台电脑呢&#xff1f; 这并不是科幻&#xff0c;而是完全可以通过远程桌面技术来实现。 今天&#xff0…

学习Java(一)类和对象

package demo.ceshi;public class Puppy {private int age;private String name;//构造器public Puppy( String name){this.name name;System.out.println("公主的名字叫&#xff1a;"name);}//设置age的值public void setAge(int age){this.age age;System.out.pr…

智慧仓储-AI销量预测

1、预测系统技术选型 基础层&#xff1a; Hbase、ClickHouse、Hdfs 用来做数据存储 框架层&#xff1a; 以 Spark RDD、Spark SQL、Hive 为主&#xff0c; MapReduce 程序占一小部分&#xff0c;是原先遗留下来的&#xff0c;目前正逐步替换成 Spark RDD。 选择 Spark 除了对…

rsyslogd 内存占用很高解决方案

在Kubernetes&#xff08;K8S&#xff09;集群中&#xff0c;监控日志是非常重要的&#xff0c;而rsyslogd是Linux系统中用于处理系统和应用程序日志的守护进程。有时候rsyslogd可能会占用较高的内存&#xff0c;这时候我们就需要对其进行优化和调整。 阿里云虚拟服务器&…

创客中国AIGC专题赛冠军天鹜科技:AI蛋白质设计引领者

“落霞与孤鹜齐飞,秋水共长天一色——这句出自《滕王阁序》的诗句,是我作为江西人熟记于心的佳句。它描绘的天地壮丽景色常浮现于我的脑海,正是这种豁达与壮观,启发我们将公司命名为‘天鹜科技’,我们希望将源自自然的蛋白质与现代科技的创新精神相结合,打造蛋白质设计与应用的…

16_Python的迭代器

在Python中&#xff0c;迭代是一个非常重要的概念。迭代通常指的是按照某种顺序逐个访问容器中的元素的行为。如使用for循环遍历取值的过程。 可迭代对象&#xff08;Iterable&#xff09; 可迭代对象是任何可以返回一个迭代器的对象。简单来说&#xff0c;它是可以逐一返回其…

机器学习模型中特征贡献度分析:预测贡献与错误贡献

在机器学习领域&#xff0c;特征重要性分析是一种广泛应用的模型解释工具。但是特征重要性并不等同于特征质量。本文将探讨特征重要性与特征有效性之间的关系&#xff0c;并引入两个关键概念&#xff1a;预测贡献度和错误贡献度。 核心概念 预测贡献度&#xff1a;衡量特征在…

【C++】—— stack queue deque

【C】—— stack & queue & deque 1 stack 与 queue 的函数接口2 适配器2.1 发现问题2.2 什么是适配器 3 stack 与 queue的模拟实现3.1 栈的基础框架3.2 栈的模拟实现3.3 队列的模拟实现 4 模板的按需实例化5 deque 的简单介绍5.1 vector 与list对比5.1.1 vector5.1.2 …

C++函数重载完成日期类相关计算

本文内容如下&#xff1a; 1.创建类以及函数的声明2.日期加减天数1.月份天数2.函数实现 3.日期比较大小4.日期减日期1.日期的前置和后置加加2.日期减日期的实现 5.内置类型的cout和cin本文代码如下&#xff1a; 要完成日期类的相关计算要创建自定义的类型&#xff0c;然后用函数…

获取IPV6地址的参考网站|nginx解析IPV6|linux服务器获取IPV6的方法

获取IPV6地址的参考网站 网址1 https://v6.ident.me/ 网址2 https://ifconfig.co/ 网址3 https://ifconfig.me/ IPV6检测站点推荐 网址1 http://ipv6-test.ch/ linux服务器获取IPV6的方法 以centos7为例 curl -6 ifconfig.mecurl -6 https://v6.ident.mecurl -6 https:…