为什么Haskell/GHC可执行文件的文件大小如此之大?

19

可能重复:
Small Haskell program compiled with GHC into huge binary

最近我注意到Haskell可执行文件非常大。以下所有内容都是在Linux上使用GHC 7.4.1和-O2编译的。

  1. Hello World (main = putStrLn "Hello World!") 超过了800 KiB。运行strip能将文件大小缩减至500 KiB;即使在编译时添加了-dynamic,它也没有太多帮助,最终的可执行文件会被削减至约400 KiB。

  2. 编译一个非常原始的使用Parsec的示例,得到的文件大小是1.7 MiB。

-- File: test.hs
import qualified Text.ParserCombinators.Parsec as P
import Data.Either (either)

-- Parses a string of type "x y" to the tuple (x,y).
testParser :: P.Parser (Char, Char)
testParser = do
    a <- P.anyChar
    P.char ' '
    b <- P.anyChar
    return (a, b)

-- Parse, print result.
str = "1 2"
main = print $ either (error . show) id . P.parse    testParser "" $ str
-- Output: ('1','2')

Parsec可能是一个较大的库,但我只使用它的一个小子集,实际上由上面生成的优化核心代码比可执行文件要小得多:

$ ghc -O2 -ddump-simpl -fforce-recomp test.hs | wc -c
49190 (bytes)

因此,我的最初假设是错误的,实际上程序中并没有大量的Parsec。

为什么可执行文件大小如此之大?有什么可以做的(除了动态链接)?


@DanielWagner 另一个问题肯定是相关的,但即使使用那里描述的技术,Hello World 仍然很大。另外:为什么小核心代码,在编译时变得如此庞大,而它应该包含整个程序? - David
2
有一个相当大的运行时系统。 - augustss
2
@David:除非全部被内联,否则核心不包含整个程序。因此它将链接Parsec,除非您使用-split-objs构建(请参见相关答案),否则它将不得不链接所有内容。 - hammar
作为参考,您的原始示例在我的系统上生成了29 KiB的“大型”可执行文件。ghc -O2 -dynamic test.hs && strip test && du -b test => 28712字节。 GHC版本为7.4.2,x86_64 Linux系统。 - David Unric
2个回答

14
为了有效地减小Glasgow Haskell Compiler生成的可执行文件的大小,您需要专注于以下几点:
  • 使用动态链接,并使用传递给ghc的-dynamic选项,以便通过利用共享(动态)库来避免将模块代码捆绑到最终可执行文件中。系统中需要存在这些 GHC 库的共享版本!
  • 从最终可执行文件中删除调试信息(例如使用GNU's binutils的strip工具)
  • 删除未使用模块的导入(不要期望在动态链接时获得收益)
简单的 hello world 示例的最终大小为9 KiB,Parsec 测试约为28 KiB(两者都是64位Linux可执行文件),我认为对于这样一种高级语言实现而言,这个大小相当小且可以接受。

如果我使用“-dynamic”链接,Hello World只有9 KiB。在Parsec的情况下,我在安装动态版本时遇到了问题(“cabal install parsec --enable-shared --reinstall”会导致cabal抱怨我没有“mtl-2.1.1”软件包的“dyn libraries”,但这会带来另一个问题。无论如何,谢谢。 - David

5
我理解的是,如果您从X包中使用单个函数,则会静态链接整个包。我认为GHC并不是逐个函数链接的。 (除非您使用“分离对象”技巧,这往往会使链接器失控)。
但是,如果您正在动态链接,那么应该会解决这个问题。所以我不确定在这里该建议什么...
(我非常确定我看到过一篇博客文章,当动态链接首次出现时,演示了编译成2KB二进制的Hello World。显然我现在找不到这篇博客文章...grr.)
还考虑跨模块优化。如果您正在编写Parsec解析器,那么GHC可能会内联所有解析器定义,并将其简化为最有效的代码。果然,您几行Haskell代码产生了50KB的Core。将它编译为机器码后,应该会变成37倍大吗?我不知道。您可以尝试查看下一步产生的STG和Cmm代码。(抱歉,我不记得头脑中的编译器标志...)

这实际上并不是完全正确的。这取决于系统。在大多数使用静态链接的系统中,GHC 使用“分离对象”,因此您会得到每个函数一个对象。 - Don Stewart
@DonStewart 但是您需要在cabal配置中启用split-objs才能使用分割对象构建cabal安装的库,不是吗? - Daniel Fischer

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接