【问题标题】:Inlining Python Function内联 Python 函数
【发布时间】:2018-10-12 23:38:26
【问题描述】:

在 C 程序中,内联函数是一种相当直观的优化。如果内联函数的主体足够小,则最终保存到函数的跳转和堆栈帧的创建,并将返回值存储在函数结果将被存储的任何地方,跳转到内联函数的“主体”的末尾" 而不是长跳转到返回指针。

我有兴趣在 Python 中做同样的事情,将两个 Python 函数转换为另一个有效的 Python 函数,其中第一个被“内联”到第二个。一个理想的解决方案可能如下所示:

def g(x):
    return x ** 2

def f(y):
    return g(y + 3)

# ... Becomes ...

def inlined_f(y):
    return (y + 3) ** 2

显然,在像 Python 这样动态的语言中,自动执行此操作并非易事。我想出的最好的通用解决方案是使用dict 来捕获传递给函数的参数,将函数体包装在一个迭代for 循环中,使用break 跳转到函数的末尾, 并将参数的使用替换为参数字典中的索引。结果如下所示:

def inlined_f(y):
    _g = dict(x=y + 3)
    for ____ in [None]:
        _g['return'] = _g['x'] ** 2
        break
    _g_return = _g.get('return', None)
    del _g
    return _g_return

我不在乎它难看,但我确实在乎它不支持从循环内返回。例如:

def g(x):
    for i in range(x + 1):
        if i == x:
            return i ** 2
    print("Woops, you shouldn't get here")

def inlined_f(y):
    _g = dict(x=y + 3)
    for ____ in [None]:
        for _g['i'] in range(_g['x'] + 1):
            if _g['i'] == _g['x']:
                _g['return'] _g['i'] ** 2
                break  # <-- Doesn't exit function, just innermost loop
        print("Woops, you shouldn't get here")
    _g_return = _g.get('return', None)
    del _g
    return _g_return

我可以采取什么方法来避免需要使用break 来“跳”出内联函数的主体?我也愿意接受一种整体更好、更通用的方法来将一个 Python 函数内联到另一个函数中。

作为参考,我在 AST(抽象语法树)级别工作,因此使用已解析的 Python 代码;显然,在文字值之外,我不知道在执行这种转换时任何东西会有什么值或类型。生成的内联函数的行为必须与原始函数相同,并且必须支持调用函数时通常可用的所有功能。这在 Python 中是否可行?


编辑:我应该澄清一下,因为我使用了“优化”标签,我实际上对性能提升并不感兴趣。生成的代码不需要更快,它只是不能调用内联函数,同时仍然表现相同。您可以假设这两个函数的源代码都可以作为有效的 Python 使用。

【问题讨论】:

  • 感谢您提出有趣的问题,但我很好奇您对这种转换的预期应用。
  • 在字节码级别进行这种转换可能更有意义。 (不过,如果您正在将这种程度的努力投入到微优化中,那么使用 Cython 之类的东西可能会给您带来更多收益。)
  • 嗯.. 这似乎不像内联的正常定义(即 alpha 转换 + 用代码替换调用)。你似乎在做某种急切的评估..?
  • @NPE 特别是I'm writing a library,其目标是执行动态句法更改以支持其他代码生成库,例如numba.jittangent。因此,为什么我需要结果是一个有效的 Python 函数。如果我可以动态生成不使用某些代码特性的等效 Python 函数,那么我不需要依赖那些库来支持那些代码特性(例如函数调用)。
  • @scnerd:Numba 目前是doesn't support exception handling,但它确实支持函数调用,所以使用异常处理来转换函数调用听起来没什么用。

标签: python inline abstract-syntax-tree compiler-optimization


【解决方案1】:

return 最接近的模拟可能是引发Exception,它可以从嵌套循环中弹出到“内联函数”的顶部。

class ReturnException(Exception):
    pass


g = dict(x=y + 3)
try:
    for j in some_loop:
        for _g['i'] in range(_g['x'] + 1):
            if _g['i'] == _g['x']:
                raise ReturnException(_g['i'] ** 2)
except ReturnException as e:
    _g['return'] = e.message
else:
    _g['return'] = None

我不知道有多少开销与异常相关,或者这是否比简单地调用函数更快。

【讨论】:

  • 哦,我喜欢这种方法。它需要确保异常类型在函数体中可用,但我可以很容易地做到这一点。如果其他人有建议,我将给出更长的时间,但我很乐意接受这个答案。
  • @scnerd:内联函数内的except 块会出现问题。
  • @user2357112 是的,如果它们是裸露的 except:except BaseException: 块。我可以从BaseException 继承以最小化风险,但这当然不是万无一失的。不过,在正确的编码实践下,这种异常方法比我原来的 for/break 方法更安全、更通用
【解决方案2】:

在源代码级别我看到的唯一合理的方法,简化了:

  • 将源代码解析为某个 AST(或仅使用 the built-in AST)。
  • 复制代表函数主体的子树。
  • 重命名子树中的变量,例如通过添加一个唯一的前缀。
  • 在调用站点,使用函数的新变量名将所有传递的参数替换为赋值。
  • 删除调用并用你准备好的函数体替换它。
  • 将 AST 序列化回源代码。

真正的问题是什么:

  • 生成器函数;只是不要内联它们。
  • 从需要运行finally 部分的try/finally 下返回。可能很难正确重写;恕我直言,最好不要加衬里。
  • 从需要运行__exit__ 部分的上下文管理器下返回。虽然并非不可能,但重写保留语义也很棘手;最好不要内联。
  • 中间函数返回,尤其是在多个循环构造中。您可能需要用一个额外的变量替换它们并将其线程化到每个while 语句的每个条件中,并可能在for 语句中添加一个条件中断。同样,并非不可能,但最好不要内联。

【讨论】:

  • @thebjorn:确实!虽然尾递归可以用循环代替:)
  • 当然,您可以将其全部转换为 CPS(连续传递样式);-) -- 但无论您尝试什么,您都会很快遇到 Python 对简单不友好的事实内联的形式。 PyPy 内联器甚至不会尝试在源代码级别进行内联,而这些人已经为此工作了一段时间。
  • @thebjorn:Afaict Python 是一种中间形式:«具体来说,我正在编写一个库,其目标是执行动态句法更改以支持其他代码生成库»。我认为简单的内联形式(例如,内联一个函数,其中一个返回作为最后一条语句)是可行的。有了更高级的东西,复杂性(以及出现错误的可能性)就会滚雪球。
  • 对于递归函数,到目前为止我一直在跟踪递归深度,所以你实际上有_g_0_g_1等,并且递归限制可以明确设置上限(并且应该是一个非常低的上限)。很确定生成器函数不可能在语法上内联,所以我采用了“你最好知道你在做什么”的方法,并将对生成器函数 f(x) 的调用处理为等同于 list(f(x)),这使得它可以内联.
  • finally 和上下文管理器__exit__ 的情况下,它们应该可以正常运行,因为return-exception 会从它们上面级联,不是吗?这两种范式的重点之一就是能够适应被排除在外的过去,对吧?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-07-21
  • 2020-06-02
  • 1970-01-01
  • 2011-09-20
  • 1970-01-01
相关资源
最近更新 更多