Python工作组提速CPython 5-10%

编辑:光环大数据 来源: 互联网 时间: 2017-10-26 18:11 阅读:

  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培训、大数据培训机构、大数据培训班、数据分析培训、大数据可视化培训,就选光环大数据!光环大数据,聘请专业的大数据领域知名讲师,确保教学的整体质量与教学水准。讲师团及时掌握时代潮流技术,将前沿技能融入教学中,确保学生所学知识顺应时代所需。通过深入浅出、通俗易懂的教学方式,指导学生更快的掌握技能知识,成就上万个高薪就业学子。 更多问题咨询,欢迎点击------>>>>在线客服

你可能也喜欢这些

在线客服咨询

领取资料

X
立即免费领取

请准确填写您的信息

点击领取
#第三方统计代码(模版变量) '); })();
'); })();