python-面向运行时性能优化
一: python运行机制
1> 语言运行过程
1. 高级语言分类及特点
A: 分类
高级语言-程序的执行方式 分类:编译型语言、解释型语言;
分类 | 含义 |
---|---|
编译型语言 | 将所有源代码一次性转换成二进制指令,也就是生成一个可执行程序(如Windows 下的 .exe),比如C语言、C++、Golang、Pascal(Delphi)、汇编等; |
解释型语言 | 一边执行一边转换,需要哪些源代码就转换哪些源代码,不生成可执行程序,比如 Python、JavaScript、PHP、Shell、MATLAB 等 |
半编译半解释型语言 | 既用了解释器也用编译器,如 Java 和 C## 。源代码需要先转换成一种中间文件(字节码文件),然后再将中间文件拿到虚拟机中执行 |
使用的转换工具称为 :编译器
编译器的类型:前端编译器、后端编译器
编译器分类 | 含义 |
---|---|
前端编译器 | 编译器的分析阶段也称为前端,它将程序划分为基本的组成部分,检查代码的语法、语义和语法,然后生成中间代码。分析阶段包括词法分析、语义分析和语法分析 |
后端编译器 | 编译器的合成阶段也称为后端,优化中间代码,生成目标代码。合成阶段包括代码优化器和代码生成器 |
B: 特点
编译型语言 | 特点 |
---|---|
1 | 编译型语言经过编译器转成可执行程序后,可执行程序里面包含的就是机器码。只要拥有可执行程序,可以随时运行,不用再重新编译,即 一次编译,无限次运行; |
2 | 编译型语言可以脱离开发环境运行;运行可执行程序时,不再需要源代码和编译器 |
3 | 编译型语言一般不能跨平台,即不能在不同的操作系统之间随意切换 |
解释型语言 | 特点 |
---|---|
1 | 解释型语言,每次执行程序都需要一边转换一边执行 |
2 | 无法脱离开发环境,所以解释型语言的执行效率天生就低于编译型语言,甚至存在数量级的差距 |
3 | 解释型语言 能跨平台,即实现“一次编写,到处运行” |
C: 对比
类型 | 原理 | 优点 | 缺点 |
---|---|---|---|
编译型语言 | 通过专门的编译器,将所有源代码一次性转换成特定平台(Windows、Linux 等)执行的机器码(以可执行文件的形式存在) | 编译一次后,脱离了编译器也可以运行,并且运行效率高 | 可移植性差,不够灵活 |
解释型语言 | 由专门的解释器,根据需要将部分源代码临时转换成特定平台的机器码 | 跨平台性好,通过不同的解释器,将相同的源代码解释成不同平台下的机器码 | 一边执行一边转换,效率很低 |
2. python 运行过程
理解:
注释 | 理解 |
---|---|
运行过程 | C、C++等语言都是:先经过预处理、编译、汇编、链接、最终生成机器代码(可执行文件)。而python每次运行,多了中间的两步(编译、解释) |
.pyc文件存在的目的 | .pyc是PyCodeObject的一种永久保存方式,对可能重用的模块不用再重新解释,提高程序运行速度 |
2> python 内存管理机制
python的自动垃圾回收机制,在Python中创建对象时无须手动释放。这对开发者非常友好,让开发者无须关注低层内存管理;
1. 垃圾回收算法
在python中,垃圾回收算法以引用计数为主,标记-清除和分代收集两种机制为辅;
A: 引用计数
原理 | 理解 |
---|---|
1 | 每个对象有一个整型的引用计数属性。用于记录对象被引用的次数 |
2 | 例如对象A,如果有一个对象引用了A,则A的引用计数+1 |
3 | 当引用删除时,A的引用计数-1 |
4 | 当A的引用计数为0时,即表示对象A不可能再被使用,直接回收 |
demo
Python中,可以通过sys模块的getrefcount函数获取指定对象的引用计数器的值,
import sys
# People类里有两个成员方法(一个有参数,一个无参数)、一个静态方法
class People:
def __init__(self):
self.__age = 20
def name(self, firstName, lastName):
return firstName + ' ' + lastName
def age(self):
return self.__age
@staticmethod
def class_name():
return People.__name__
print("创建对象 0 + 1 =", sys.getrefcount(People()))
P = People()
print("创建对象 1+ 1 =", sys.getrefcount(P))
A = P
a = A
print("创建对象 2+ 2 =", sys.getrefcount(a))
print("创建对象 2+ 2 =", sys.getrefcount(P))
print("创建对象 2+ 2 =", sys.getrefcount(A))
a = None
print("创建对象 4 -1 =", sys.getrefcount(P))
print("创建对象 4 -1 =", sys.getrefcount(A))
del A
print("创建对象 3 -1 =", sys.getrefcount(P))
list_P = [P, P, P]
print("P 三次加入列表被调用;创建对象 2 + 3 =", sys.getrefcount(P))
========================
创建对象 0 + 1 = 1
创建对象 1+ 1 = 2
创建对象 2+ 2 = 4
创建对象 2+ 2 = 4
创建对象 2+ 2 = 4
创建对象 4 -1 = 3
创建对象 4 -1 = 3
创建对象 3 -1 = 2
P 三次加入列表被调用;创建对象 2 + 3 = 5
优点&缺点
引用计数 | 含义 |
---|---|
优点 | 1, 高效、逻辑简单,只需根据规则对计数器做加减法;2,实时性。一旦对象的计数器为零,就说明对象永远不可能再被用到,无须等待特定时机,直接释放内存。 |
缺点 | 1, 需要为对象分配引用计数空间,增大了内存消耗;2, 当需要释放的对象比较大时,如字典对象,需要对引用的所有对象循环嵌套调用,可能耗时比较长;3, 循环引用。这是引用计数的致命伤,引用计数对此是无解的,因此必须要使用其它的垃圾回收算法对其进行补充 |
B: 标记清除
标记-清除算法:主要用于潜在的循环引用问题;
序列 | 算法步骤 |
---|---|
1 | 标记阶段。将所有的对象看成图的节点,根据对象的引用关系构造图结构。从图的根节点遍历所有的对象,所有访问到的对象被打上标记,表明对象是“可达”的; |
2 | 清除阶段。遍历所有对象,如果发现某个对象没有标记为“可达”,则就回收; |
demo
class A():
def __init__(self):
self.obj = None
def func():
a = A()
b = A()
c = A()
d = A()
a.obj = b
b.obj = a
return [c, d]
e = func()
上面代码中,a和b相互引用,e引用了c和d。整个引用关系如下图所示:
如果采用引用计数器算法,那么a和b两个对象将无法被回收。而采用标记清除法,从根节点(即e对象)开始遍历,c、d、e三个对象都会被标记为可达,而a和b无法被标记。因此a和b会被回收;
标记清除流程
标记清除缺点:执行效率低,且会造成空间碎片问题;
C: 分代回收
分代回收原理:在执行垃圾回收过程中,程序会被暂停,即stop-the-world;
为了减少程序的暂停时间,采用分代回收(Generational Collection)降低垃圾收集耗时
解决标记清除扫描遍历开销大,效率低的问题;
将对象分为初生代、中生代、老生代,设置阈值来扫描遍历的类型,以空间换取时间;
分代回收法则
序列 | 含义 |
---|---|
1 | 接大部分的对象生命周期短,大部分对象都是朝生夕灭 |
2 | 经历越多次数的垃圾收集且活下来的对象,说明该对象越不可能是垃圾,应该越少去收集 |
分代回收理解
序列 | 含义 |
---|---|
1 | 对象刚创建时为G0。 |
2 | 如果在一轮GC扫描中存活下来,则移至G1,处于G1的对象被扫描次数会减少。 |
3 | 如果再次在扫描中活下来,则进入G2,处于G1的对象被扫描次数将会更少 |
触发GC时机
当某世代中分配的对象数量与被释放的对象之差达到某个阈值的时,将触发对该代的扫描。当某世代触发扫描时,比该世代年轻的世代也会触发扫描;
import gc
threshold = gc.get_threshold()
print("各世代的阈值:", threshold)
# 设置各世代阈值
gc.set_threshold(800, 20, 20)
D: 内存池
python 内存池机制:为了避免频繁的申请和释放内存,python的内置数据类型,数值、字符串,查看python源码可以看到数值缓存范围为 -5 ~ 257;对于 -5 ~ 257 范围内的数值,创建之后python会把其加入到缓存池中,当再次使用时,则直接从缓存池中返回,而不需要重新申请内存,如果超出了这个范围的数值,则每次都需要申请内存;这样能够减少内存碎片,提升效率;
python内置对象(int,dict,list,str等)都有独立的私有内存池,对象之间的内存池不共享;
内存池分类
分类 | 理解 |
---|---|
大内存 | 以256Bytes 为界限,大于256Bytes 属于大内存,大内存使用Malloc进行分配, |
小内存 | 以256Bytes 为界限,小于256Bytes 属于小内存,小内存使用内存池进行分配; |
内存池分别level
python 的对象管理主要位于Level+1 —> Level+3 层;从内存池申请的内存对象被删除后的内存会被归还到内存池,避免了多次频繁释放,能有效减少内存碎片的产生;
level | 理解 |
---|---|
Level+1 | 大于256Bytes 属于大内存,使用Malloc进行分配 |
Level+2 | 小于256Bytes 属于小内存,由python 对象分配器(内存池)分配; |
Level+3 | python内置对象(int,dict,list,str等)都有独立的私有内存池,对象之间的内存池不共享; |
E: 缓存池
python 缓存池作用:
作用 | 理解 |
---|---|
1 | 提升运行效率,对常用对象缓存; |
2 | 小整数对象池,intern,常量池和free_list |
3> python 常用解释器
分类 |
---|