Performance 为什么在Haskell中(常量)表达式在编译时不求值?

Performance 为什么在Haskell中(常量)表达式在编译时不求值?,performance,haskell,optimization,compile-time,compile-time-constant,Performance,Haskell,Optimization,Compile Time,Compile Time Constant,我目前正在学习Haskell,有一件事让我感到困惑: 当我构建一个复杂的表达式(其计算将花费一些时间)并且这个表达式是常量(这意味着它只构建已知的硬编码值)时,表达式不会在编译时求值 我来自C/C++背景,已经习惯了这种优化 在Haskell/GHC中,不执行此类优化(默认)的原因是什么?如果有的话,有什么好处 数据树a= 空树 |节点a(树a)(树a) 导出(显示、读取、均衡) elementToTree::a->Tree a elementToTree x=节点x空树空树 树插入::(Ord

我目前正在学习Haskell,有一件事让我感到困惑:

当我构建一个复杂的表达式(其计算将花费一些时间)并且这个表达式是常量(这意味着它只构建已知的硬编码值)时,表达式不会在编译时求值

我来自C/C++背景,已经习惯了这种优化

在Haskell/GHC中,不执行此类优化(默认)的原因是什么?如果有的话,有什么好处

数据树a=
空树
|节点a(树a)(树a)
导出(显示、读取、均衡)
elementToTree::a->Tree a
elementToTree x=节点x空树空树
树插入::(Ord a)=>a->a树->a树
树插入x空树=元素到树x
树插入x(节点a左-右)
|x==a=节点x左右
|xa=节点a左侧(树插入x右侧)
treeFromList::(Ord a)=>[a]->树a
treeFromList[]=EmptyTree
treeFromList(x:xs)=treeInsert x(treeFromList xs)
treeElem::(Ord a)=>a->Tree a->Bool
treeElem x EmptyTree=False
树元素x(节点a左-右)
|x==a=True
|xa=treeElem x右
main=do
let tree=treeFromList[0..90000]
putStrLn$show(树3树)
由于这将始终打印
True
,因此我希望编译后的程序打印并退出
几乎是立即进行。

在这种情况下,GHC无法确定计算是否会完成。这不是懒惰与严格的问题,而是停滞不前的问题。对您来说,说
treeFromlist[0..90000]
是一个可以在编译时计算的常量似乎很简单,但编译器是如何知道这一点的?编译器可以轻松地将
[0..90000]
优化为常数,但您甚至不会注意到这一变化。

您可能会喜欢。编译器可以尝试这样做,但这可能是危险的,因为任何类型的常量都可以做一些有趣的事情,比如循环。至少有两种解决方案:一种是超级编译,目前还不能作为任何编译器的一部分使用,但您可以尝试各种研究人员提供的原型;更实用的方法是使用Template Haskell,这是GHC的机制,允许程序员要求在编译时运行一些代码。

您所说的过程称为超级编译,它比您想象的要困难得多。它实际上是计算科学中一个活跃的研究课题!有一些人正试图为Haskell创建这样一个超级编译器(可能基于GHC,我的记忆很模糊),但GHC(至今)中并未包含该功能,因为维护人员希望缩短编译时间。你提到C++作为一种语言来实现这一点——C++也恰好有坏的编译时间!
Haskell的替代方法是使用模板Haskell手动执行此优化,该模板是Haskell编译时评估的宏系统。

请记住Haskell有延迟评估。这适用于表达式是否为“常量”。是的,我知道。但是坚持这种(在本例中不必要的)惰性有什么好处?@ChrisJester-Young如果编译器能够保证表达式是根据严格性分析计算的呢?@ChrisJester-Young嗯,Haskell有“非严格语义”。在惰性计算允许终止的程序上不允许失败终止,但它实际上不必使用惰性计算,而且它甚至不必对所有“常量”使用相同的计算策略。好吧,假设计算永远不会完成,为什么不让编译器挂起而不是运行时?或者换句话说:是否有任何有用的计算不会停止?(除了与IO相关的东西)@DanielOertwig我不想让我的编译器挂起运行我的程序。是的,有一些有用的计算要么不会停止,需要很长时间才能停止,要么只会占用字节码中的大量空间。如果我有一棵树,但我想要的不仅仅是检查它是否有一个元素呢?编译器是否应该计算一个可能占用千兆字节硬盘空间的非常大的树,这样它就不必在运行时进行计算?@Danieloretwig这尤其有趣,因为在Haskell中,经常会有不断的未完成计算。例如,每当使用
repeat
时。或者<代码> [1…] /代码> @ BHKLILR认为:如果使用超级编译——编译器编译,则必须有一个输入,这将导致程序的挂起。难道你不想让编译器挂起,而不是在你的版本中出现一个可能未被注意到的bug吗?在编译时计算一棵大树有什么问题?如果它是大的,那么它就是大的,唯一可以保存的就是在运行时构建它。还要记住,像这样的超级编译只能在编译时可用的数据上完成,所以数据量是有限制的。@DanielOertwig,但我们又回到了暂停问题上。对于像
[1..]
这样明显的情况,仅仅添加“异常”是无法逃脱的。相反,对于每个常量表达式,您必须证明它不是无限的,即计算停止。当然,这会(大幅)增加编译时间,但我看不出有什么问题:一旦生成的二进制文件运行多次,您就已经节省了一些时间。作为一个旁注:编译C++需要时间,但大多数时候它不是编译代码的问题,而不是排序哪些代码必须编译。像臃肿的头文件和糟糕的(=递归)生成文件这样的事情会增加编译时间,而不是(大多数)编译时计算。@DanielOertwig“一旦生成的二进制文件运行了不止一次,您就已经为自己节省了一些时间”——这是在一个有点可疑的假设下运行的,即超级编译