编译器的时间复杂度


54

我对编译器的时间复杂度感兴趣。显然,这是一个非常复杂的问题,因为要考虑许多编译器,编译器选项和变量。具体来说,我对LLVM感兴趣,但是对人们有什么想法或开始研究的地方感兴趣。一个相当谷歌似乎没有什么亮点。

我的猜测是有些优化步骤是指数级的,但对实际时间影响很小。例如,基于数字的指数是函数的参数。

从我的头顶上,我会说生成AST树将是线性的。IR生成将需要在不断增长的表中查找值时单步执行树,因此或。代码生成和链接将是类似的操作类型。因此,如果我们删除不实际增长的变量的指数,我的猜测将是。O(n2)O(nlogn)O(n2)

我可能完全错了。有人对此有任何想法吗?


7
当声明任何东西是“指数”,“线性”,或时,请务必小心。至少对我来说,如何衡量输入完全不是显而易见的(指数表示什么?代表什么?)O(n2)O(nlogn)n
Juho 2014年

2
当您说LLVM时,您的意思是Clang吗?LLVM是一个大项目,有多个不同的编译器子项目,因此有点模棱两可。
Nate CK 2014年

5
对于C#,至少在最坏情况下它是指数级的(您可以在C#中编码NP完全SAT问题)。这不仅是优化,还需要选择正确的函数重载。对于C ++之类的语言,由于模板的完整性,将是无法确定的。
CodesInChaos

2
@赞恩我不明白你的意思。模板实例化发生在编译过程中。您可以采用强制编译器解决该问题以产生正确输出的方式,将难题编码到模板中。您可以将编译器视为图灵完整模板编程语言的解释器。
CodesInChaos 2014年

3
当您将多个重载与lambda表达式结合使用时,C#重载解析非常棘手。您可以使用这种方式对布尔公式进行编码,从而确定是否存在适用的重载需要NP完全3SAT问题。要实际编译问题,编译器必须实际找到该公式的解决方案,这可能甚至更难。埃里克·利珀特(Eric Lippert)在他的博客文章Lambda Expressions vs. Anonymous Methods,第五部分
CodesInChaos 2014年

Answers:


50

回答您的问题的最好的书可能是:Cooper和Torczon,“工程编译器”,2003年。如果您可以访问大学图书馆,则应该可以借阅一本。

在像llvm或gcc这样的生产编译器中,设计人员会尽一切努力使所有算法保持在,其中是输入的大小。对于“优化”阶段的某些分析,这意味着您需要使用启发式方法,而不是生成真正的最佳代码。nO(n2)n

词法分析器是一个有限状态机,因此输入大小为(以字符为单位),并生成令牌流,该令牌流将传递给解析器。O n O(n)O(n)

对于使用多种语言的许多编译器,解析器为LALR(1),因此在输入令牌数量的时间中处理令牌流。在解析期间,您通常必须跟踪符号表,但是对于许多语言而言,可以使用一堆哈希表(“字典”)来处理。每个字典访问都是,但是您有时可能不得不走栈来查找符号。堆栈的深度为,其中为合并范围的嵌套深度。(因此,在类似C的语言中,您内部有多少层花括号。)O 1 O s O(n)O(1)O(s)s

然后,通常将解析树“展平”为控制流程图。控制流程图的节点可能是3地址指令(类似于RISC汇编语言),并且控制流程图的大小通常在解析树的大小上是线性的。

然后通常采用一系列冗余消除步骤(常见子表达式消除,循环不变代码运动,恒定传播等)。(这通常被称为“优化”,尽管结果几乎没有什么优化,真正的目标是在我们对编译器施加的时间和空间限制内尽可能地提高代码。)每个冗余消除步骤将通常需要证明有关控制流程图的某些事实。这些证明通常使用数据流分析来完成。大多数数据流分析的设计目的是使它们收敛于流程图上的传递,其中是(大致而言)循环嵌套深度,而流程图上的传递花费时间d O n nO(d)dO(n)其中是3地址指令的数量。n

对于更复杂的优化,您可能需要进行更复杂的分析。此时,您开始遇到权衡。您希望您的分析算法所花费的时间少于O(n2)时间花费在整个程序流程图的大小上,但这意味着您需要在没有可能证明昂贵的信息(程序改进转换)的情况下进行操作。别名分析就是一个典型的例子,对于某些存储器写对,您想证明这两个写永远不能针对相同的存储器位置。(您可能要进行别名分析,以查看是否可以将一条指令移到另一条指令上。)但是,要获取有关别名的准确信息,您可能需要分析程序中所有可能的控制路径,这与分支数成指数关系在程序中(因此控制流图中的节点数呈指数形式。)

接下来,进入寄存器分配。寄存器分配可以表述为图着色问题,并且用最少数量的颜色为图着色是众所周知的NP-Hard。因此,大多数编译器将某种贪婪启发式方法与寄存器溢出结合使用,目的是在合理的时间范围内尽可能减少寄存器溢出的次数。

最后,您开始进行代码生成。通常,在基本块是一组具有单个入口和单个出口的线性连接的控制流程图节点的时间,代码生成通常在最大的基本块中进行。可以将其重新表示为一个图形覆盖问题,其中您要覆盖的图形是基本块中这组3地址指令的依赖图,并且您试图覆盖代表可用机器的一组图形说明。这个问题在最大的基本块的大小上是指数级的(原则上,它可以与整个程序的大小相同),因此通常使用启发式方法来完成此任务,在这种方法中,可能覆盖的子集很小检查。


4
三等!顺便说一句,编译器试图解决的许多问题(例如寄存器分配)都是NP难题,但是其他问题在形式上还不确定。例如,假设您有一个调用p()和一个调用q()。如果p是纯函数,则只要p()不无限循环,就可以安全地对调用进行重新排序。要证明这一点,需要解决停顿问题。与NP难题一样,编译器编写者可以在可行的情况下尽一切努力来逼近解决方案。
别名2014年

4
哦,还有一件事:今天使用的某些类型系统在理论上非常复杂。Hindley-Milner类型推断是DEXPTIME-complete所熟知的,类似ML的语言必须正确实现它。但是,实际上,运行时间是线性的,因为a)现实情况中程序永远不会出现病理情况,b)现实世界中的程序员倾向于输入类型注释,即使只是为了获得更好的错误消息。
别名2014年

1
好的答案,似乎唯一缺少的是解释的简单部分,用简单的术语阐明:可以在O(n)中完成程序的编译。像任何现代编译器一样,在编译之前优化程序是一项几乎没有限制的任务。实际花费的时间不受任务的任何固有限制,而是受编译器在人们厌倦等待之前的某个时候完成的实际需要所控制。这始终是一种妥协。
aaaaaaaaaaaaaa 2014年

@Pseudonym,很多情况下编译器必须解决停止问题(或非常讨厌的NP硬问题),这是标准赋予编译器编写者假设没有发生未定义行为的余地的原因之一(例如无限循环等) )。
vonbrand 2014年

15

实际上,某些语言(例如C ++,Lisp和D)在编译时是图灵完备的,因此通常无法确定对其进行编译。对于C ++,这是因为递归模板实例化。对于Lisp和D,您几乎可以在编译时执行任何代码,因此,如果需要,可以将编译器置于无限循环中。


3
Haskell(带有扩展名)和Scala的类型系统也是图灵完备的,这意味着类型检查可能会花费无数的时间。现在,Scala在顶部还具有图灵完备的宏。
约尔格W¯¯米塔格

5

根据我在C#编译器上的实际经验,我可以说,对于某些程序,输出二进制文件的大小相对于输入源的大小呈指数增长(这实际上是C#规范所要求的,无法减少),因此时间复杂度高也必须至少是指数的。

众所周知,C#中的常规重载解决方案任务是NP难的(并且实际的实现复杂度至少是指数级的)。

在C#源代码中处理XML文档注释还需要在编译时评估任意XPath 1.0表达式,这也是指数形式的AFAIK。


是什么使C#二进制文件以这种方式爆炸?对我来说,这听起来像是一个语言错误……
vonbrand 2014年

1
这是在元数据中编码通用类型的方式。class X<A,B,C,D,E> { class Y : X<Y,Y,Y,Y,Y> { Y.Y.Y.Y.Y.Y.Y.Y.Y y; } }
弗拉基米尔·雷谢尼科夫2014年

-2

用现实的代码库(例如一组开源项目)对其进行度量。如果将结果绘制为(codeSize,finishTime),则可以绘制这些图。如果您的数据f(x)= y为O(n),那么在数据开始变大之后绘制g = f(x)/ x应该会给您一条直线。

绘制f(x)/ x,f(x)/ lg(x),f(x)/(x * lg(x)),f(x)/(x * x)等减小到零,无限制增大或变平。这个想法对于从空数据库开始测量插入时间(例如:长期寻找“性能泄漏”)的情况非常有用。


1
对运行时间的经验测量不会建立计算复杂性。首先,计算复杂度最通常用最坏情况下的运行时间来表示。其次,即使您想衡量某种平均情况,也需要确定输入在这种意义上是“平均”的。
David Richerby 2014年

好吧,这只是一个估计。但是具有大量真实数据的简单经验测试(每次提交一堆git repos)可能会击败一个谨慎的模型。无论如何,如果一个函数确实是O(n ^ 3)并绘制f(n)/(n n n),则您应该得到一条噪声线,其斜率大约为零。如果仅绘制O(n ^ 3)/(n * n),您会看到它呈线性上升。如果您高估了这一点,并且看线迅速降为零,这是很明显的。
罗布

1
否。例如,quicksort 在大多数输入数据上以时间运行,但在最坏的情况下(通常,在已排序的输入上),某些实现的运行时间为)。但是,如果仅绘制运行时间,则遇到情况的可能性要大于的情况。Θ n 2Θ n log n Θ n 2Θ(nlogn)Θ(n2)Θ(nlogn)Θ(n2)
David Richerby 2014年

我同意,如果您担心从攻击者那里获得拒绝服务的担心,这将给您提供不好的输入,并进行一些实时的关键输入解析,这就是您需要知道的。衡量编译时间的实际功能将非常嘈杂,而我们关心的情况将在实际的代码存储库中。
罗布

1
否。问题询问问题的时间复杂性。通常将其解释为最坏情况下的运行时间,而不是仓库中代码的运行时间。您建议的测试可以合理地预期编译器执行给定代码段的时间,这是一件好事,很有用。但是他们几乎没有告诉您问题的计算复杂性。
David Richerby 2014年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.