用Python不断折叠

2021-01-29 12:51:44

每种编程语言都旨在在其利基市场中表现出色,而要实现卓越的性能,则需要大量的编译器级优化。一种著名的优化技术是“常量折叠”(Constant Folding),在编译期间,引擎会尝试识别常量表达式,对其进行求值,然后用此新求值替换表达式,从而使运行时更精简。

在本文中,我们深入研究了什么是常量折叠,了解了它在Python世界中的应用范围,最后遍历了Python的源代码CPython,并了解了Python如何真正优雅地实现了它。

在“常量折叠”中,引擎在编译时查找并评估常量表达式,而不是在运行时对其进行计算,从而使运行时更加精简和快速。

当编译器遇到一个常量表达式时,如上所述,它将对表达式求值并将其替换为求值。通常用“抽象语法树”中的评估值替换该表达式,但是实现完全取决于该语言。因此上述表达式可以有效地执行为

在Python中,我们可以使用Disassembler模块获取CPython字节码,从而使我们可以很好地了解如何执行事情。当我们使用dis模块反汇编上述常量表达式时,我们得到以下字节码

import dis dis.dis(" day_sec = 24 * 60 * 60")0 LOAD_CONST 0(86400)2 STORE_NAME 0(day_sec)4 LOAD_CONST 1(无)6 RETURN_VALUE

我们看到该字节码没有执行两个LOAD之后的二进制乘法运算,而是执行了一个已经赋值为86400的LOAD_CONST。这表明CPython解释器在解析和构建Abstract Syntax Tree时折叠了常量表达式, 24 * 60 * 60并将其替换为评估值86400。

Python尝试折叠存在的每个单个常量表达式,但在某些情况下,即使该表达式是常量,但Python选择不对其进行折叠。例如,Python不会折叠x = 4 ** 64,而会折叠x = 2 ** 64。

除了算术表达式,Python还折叠涉及Strings和Tuples的表达式,其中常量字符串表达式直到长度4096被折叠。

a ="-" * 4096#折叠a ="-" * 4097#未折叠a ="-" * 4096#未折叠

现在,我们将重点转移到内部,并确切地找到CPython在哪里以及如何实现Constant Folding。可以在文件ast_opt.c中找到所有AST优化,包括恒定折叠。始于此的基本函数是astfold_expr,它折叠Python源中具有的所有表达式。该函数以递归方式遍历AST,并尝试折叠每个常量表达式,如下面的代码片段所示。

astfold_expr在折叠手头的表达式之前,先尝试折叠其子表达式(操作数),然后将折叠委托给相应的特定表达式折叠函数。特定于操作的折叠函数对表达式求值并返回求值的常数,然后将其放入AST中。

例如,每当astfold_expr遇到二进制运算时,在使用fold_binop评估手边的表达式之前,它都会递归折叠两个子操作数(表达式)。函数fold_binop返回评估的常量值,如下面的代码片段所示。

fold_binop函数通过检查手边的运算符的种类,然后在其上调用相应的评估函数来折叠二进制运算。例如,如果手头的操作是加法运算,则要评估最终值,它将在其左侧和右侧操作数上调用PyNumber_Add。

CPython不会调用特殊的逻辑来处理某些模式或类型以有效地折叠常量表达式,而是调用相同的通用代码。 例如,它在折叠时调用相同的常规PyNumber_Add函数,以执行常规的加法操作。 因此,CPython通过确保其代码和求值过程以通用代码本身可以处理常量表达式求值的方式来构造,从而消除了编写特殊函数来处理常量折叠的需要。