Python工作组提速CPython 5-10%
Python工作组提速CPython5-10%,关于PEP509,我们和Victor有很多对话,他给我一个网址,关于惊人的编译CPython性能的笔记[2]。他对我指出一个优化是LOAD/CALL_METHOD操作码,最初起源于PyPy的一个想法。
有一个补丁,实现这种优化,追踪在这里:[3]。我解释的这个问题有一些底层的细节,但在这个邮件中,我将介绍高层的设计。
每次访问对象的方法属性,就会创建绑定方法的对象。这是一个相当昂贵的操作,痛恨绑定方法的自由列表(所以通常避免内存分配)。这个想法是检测什么看起来像编译器调用的方法,为他发出一对专门的字节码。
所以我们将用LOAD_GLOBAL/LOAD_METHOD/CALL_METHOD替代LOAD_GLOBAL/LOAD_ATTR/CALL_FUNCTION。
LOAD_METHOD看起来是堆栈顶部的对象,检验该名称是否解析为方法或常规属性。如果它是一个方法,入栈未绑定的方法对象,该对象在堆栈了。如果它是一个属性,入栈解析后的属性和NULL。
CALL_METHOD在堆栈时,它知道如何调用正确的未绑定的方法(对象作为第一个参数),或者如何调用常规的可调用的。
这个想法确实使CPython快了大约2-4%。肯定不会使它更慢。可以肯定的,在CPython3.6至少实现这一优化。
目前,这个补丁只优化定位方法调用。有可能优化所有类型的调用,这将需要3个操作码(解释这个问题)。需要做一些认真的基准测试,看它是否真的需要。
ceval中的每个操作码缓存
-------------------------
阅读PEP509时,我在想ceval中如何使用dict->ma_version来加速全局查找。一个关键假设(正是这一点使JITs成为可能)是真实的程序不修改全局变量并重新绑定内置变量(通常),大多数代码路径操作相同类型的对象。
CPython中,所有的纯Python函数有代码对象。调用函数时,ceval在框架中执行代码对象。框架包含上下文信息,包含指向全局变量和内置字典的指针。观察重点是几乎所有的代码对象总是有相同的指针指向全局变量(模块中定义的)和内置变量。可变的全局变量或重新绑定内置变量,不是一个良好的编程实践
让我们看看这个函数:
defspam():
print(ham)
下面是它的操作码:
20LOAD_GLOBAL0(print)
3LOAD_GLOBAL1(ham)
6CALL_FUNCTION1(1positional,0keywordpair)
9POP_TOP
10LOAD_CONST0(None)
13RETURN_VALUE
我们想要优化的操作码是LOAD_GLOBAL,0和3。看看第一个,从内置变量载入‘print’函数。操作码知道下面的信息:
-它的偏移量(0),
-它的参数(0->'print'),
-它的类型(LOAD_GLOBAL).
这些信息绝不会改变。因此如果这个操作码能够解析‘print’名称(从全局变量或内置变量,可能是后者),保存指针到某个地方,globals->ma_version和builtins->ma_version,在第二次调用,只需载入此缓存信息,检验没有改变的全局变量和内置变量字典,将缓存引用放入堆栈。这将节省做两个字典查找。
我们也可以优化LOAD_METHOD。有很高的机会,每次执行代码对象‘obj.method()’中的‘obj’是相同类型。如果有操作码缓存,LOAD_METHOD可能缓存指针到解析的未绑定方法,一个指针指向obj.__class__,和tp_version_tagofobj.__class__。只需要检验缓存对象类型是否相同(它没有改变),obj.__dict__没有覆盖‘method’。长话短说,这个缓存实际上加速了C类型实现的方法调用。list.append变得非常快,因为list没有__dict__,因此检验开销很低(用缓存)。
一个简明的方法来实现这个缓存很简单,但是消耗很多内存,那将是浪费,由于我们仅仅需要LOAD_GLOBAL和LOAD_METHOD操作码的缓存。所以我们必须创新缓存的设计。下面是我提出的:
1.向代码对象添加一些字段。
2.ceval将计算每个代码对象执行多少次。
3.代码对象执行超过900次时,我们把它标记为“热门”。我们也创建一个‘unsignedchar’数组“MAPPING”,长度设置为匹配代码对象的长度。我们有一个一对一的映射,在操作码和映射数组之间。
4.其次100次调用,代码对象是“热门”,LOAD_GLOBAL和LOAD_METHOD做“MAPPING[opcode_offset()]++”。
5.代码对象1024次调用后,ceval循环将遍历映射,计算所有执行超过50次的操作码。
6.创建缓存“CACHE”结构体的数组(这是更新后的code.h文件的链接:[6])。更新操作码位置和缓存位置之间的MAPPING映射。现在代码对象是“优化的”。
7.代码对象是“优化的”,LOAD_METHOD和LOAD_GLOBAL对于快速路径使用CACHE数组。
8.缓存未命中时,例如,内置变量/全局变量/obj.__dict__变化了,‘CACHE’操作码标记的实体去最佳化,它将永远不会再尝试使用缓存。
这里有一个链接,跟踪第一个版本补丁:[5]。我写的补丁在github仓库:[4]。
总结
-------
关于这个算法的很多地方,我们可以改进/调整。应该配置代码对象更长,或者它们执行占用的时间。它们首次缓存未命中时,可能不应该去掉优化操作码。也许我们可以提出更好的数据结构。也需要配置内存,看这个缓存需要多少。
有一点我是肯定的,我们可以用相对低的内存影响得到CPython5-10%的加速。我认为这是值得探索的!
如果你对这些类型的优化感兴趣,请帮助进行代码审查,想法,分析和基准。后者尤其重要,我从未想象,提出一个良好的宏观基准是多么困难。
我也要感谢我的公司MagicStack(magic.io)赞助这些工作。
Python培训,Python学习,就选光环大数据Python培训机构!
大数据培训、人工智能培训、Python培训、大数据培训机构、大数据培训班、数据分析培训、大数据可视化培训,就选光环大数据!光环大数据,聘请专业的大数据领域知名讲师,确保教学的整体质量与教学水准。讲师团及时掌握时代潮流技术,将前沿技能融入教学中,确保学生所学知识顺应时代所需。通过深入浅出、通俗易懂的教学方式,指导学生更快的掌握技能知识,成就上万个高薪就业学子。 更多问题咨询,欢迎点击------>>>>在线客服!