【问题标题】:Using referential-transparency to pre-compute values in haskell在 haskell 中使用引用透明度预先计算值
【发布时间】:2012-04-23 01:06:56
【问题描述】:

假设我们有一个这样的程序:

list = [1..10000000]

main = print $ sum list

我希望对其进行编译,以便可执行文件仅打印 50000005000000,而无需花费太多时间和精力。

基本上,任何肯定会被计算的表达式(也许严格性分析在这里会有所帮助)都可以在编译期间预先计算(即我们使用引用透明性来表示它不是真的计算值时很重要)。

简而言之:“必须计算”+引用透明=可以预先计算

这就像运行程序直到我们遇到取决于输入的东西(即程序的核心,所有输入通用的,将被预先计算)

目前是否有实现这一目标的现有机制(在 Haskell 或任何其他语言中)? [请不要指向 C++ 中的模板之类的东西,因为它首先没有引用透明性。]

如果不是,这个问题有多难? [伴随的技术(和理论)问题是什么?]

【问题讨论】:

    标签: haskell referential-transparency precompute


    【解决方案1】:

    听起来像是超级编译的工作!这个问题听起来像是对它的描述,关于非终止的讨论反映了超级编译器开发人员面临的问题。我在 GHC wiki 上看到有人正在为它开发生产超级编译器,但不知道结果如何。

    【讨论】:

    • 嘿,感谢您将“超级编译”一词添加到我的词汇表中 :)
    • 但我认为超级编译比我想要的更雄心勃勃;可能更多 en.wikipedia.org/wiki/Partial_evaluation where (Input_static) = NULL (但是当 Input_dynamic 对输出没有影响时;就好像你已经知道一切并且可以在编译时运行你的程序)
    • 哦,这是一个很棒的链接 - 一定会喜欢维基百科。根据这篇论文,超级编译和部分评估密切相关,请参阅research.microsoft.com/en-us/um/people/simonpj/papers/…。正如其他人所说,没有编译器会在编译时进行任意大的计算,不仅因为在一般情况下这是不可能的,而且因为编译器编写者不能代表你进行交易。
    【解决方案2】:

    这在一般情况下是不安全的。原因是 Haskell 表达式可能是纯的,但它们也可能无法终止。编译器应该总是终止,所以你能做的最好的就是“评估这个结果的 1000 步”。1 但是如果你确实添加了这样一个限制,如果你正在编译一个程序来运行在拥有 TB 级 RAM 的超级计算机集群上,编译器内存不足?

    您可以添加很多限制,但最终您会将优化降低为一种缓慢的常量折叠形式(尤其是对于大多数计算依赖于运行时用户输入的程序而言)。而且由于sum [1..10000000] 在这里需要半秒钟,所以它不太可能落入任何合理的限制范围内。

    当然,像这样的特定情况通常可以优化掉,而 GHC 经常会优化掉这样的复杂表达式。但是编译器不能只在编译时安全地评估任何表达式;它必须受到非常严格的限制,并且在这些限制下它会有多大帮助是有争议的。它是编译器,而不是解释器 :)

    1 这会大大减慢任何确实包含大量无限循环的程序的编译速度——由于 Haskell 是非严格的,因此比你可能认为)。或者,更常见的是,任何包含大量长时间运行计算的程序。

    【讨论】:

    • 我们可以限制时间和空间的使用
    • 我已经扩展了我的答案以解释为什么这可能没有帮助,并且可能会导致优化对这个示例没有帮助。
    • 嗯,时间和空间限制基本上是为了用户方便;但是对于/必须计算/的表达式,它们是否会导致大量的空间/时间爆炸并不重要,因为无论如何都会出现(这是您的选择:运行时或编译时)。
    • 对于无限循环,我们以ones = 1:ones定义的列表为例,这里只计算肯定需要的部分;例如,如果ones!!72 肯定会被计算,则只需在编译时找到它
    • 真实且解释得很好。尽管如此,我有时还是希望 GHC 在编译时减少表达式,通常是在处理来自文字转换的值构造时,例如fromString "long string literal in i18n module"。也许 GHC 已经完成了这项工作,但我不知道如何确定。
    【解决方案3】:

    进行编译时计算的通用答案是使用 Template Haskell。但是对于这个特定的用例,您可以使用 vector 包和 LLVM 后端,GHC 会优化掉这个总和。

    sorghum:~% cat >test.hs
    import Data.Vector.Unboxed as V
    main = print (V.sum $ V.enumFromN (1 :: Int) 10000000)
    sorghum:~% ghc --make -O2 -fllvm test.hs
    [1 of 1] Compiling Main             ( test.hs, test.o )
    Linking test ...
    sorghum:~% time ./test
    50000005000000
    ./test  0.00s user 0.00s system 0% cpu 0.002 total
    sorghum:~% objdump -d test | grep 2d7988896b40
      40653d:   48 bb 40 6b 89 88 79    movabs $0x2d7988896b40,%rbx
      406547:   49 be 40 6b 89 88 79    movabs $0x2d7988896b40,%r14
    

    (如果不是很明显,0x2d79888896b4050000005000000。)

    【讨论】:

    • 你能简单解释一下为什么 ghc 能够在这里优化向量而不是列表吗? (如果 llvm 是这种魔法的原因;我们可以从那里借用那种逻辑到 ghc 吗?)
    • 向量库包含特定于库的规则,用于指导编译器如何优化代码。
    • @DanielWagner : main = print (V.sum $ V.map (^2) $ V.enumFromN (1 :: Int) 10000000) 需要更多时间;如果我将其更改为 100000000 (10^8),则需要 10 倍的时间!!
    • @Karan 我已经看到您的评论,并且感觉您希望跟进,但不知道该说什么,这不会简单地重复我上面的回答。如果您进行更多计算,则即使通过最佳优化编译器也不太可能将其优化掉。对于这些情况,正如我在上面的回答中所说,通用解决方案是使用 Template Haskell。
    • @DanielWagner :感谢您的跟进 :) 。但我不想使用 Template Haskell,因为我认为(或认为)这是 GHC 应该能够自行优化的东西(但似乎我错了)。
    猜你喜欢
    • 1970-01-01
    • 2022-12-04
    • 2012-12-22
    • 2021-05-08
    • 2013-04-15
    • 2012-12-21
    • 2012-02-03
    • 1970-01-01
    • 2021-02-01
    相关资源
    最近更新 更多