linker aquamacs - 用GHC編譯成的小Haskell程序變成了巨大的二進製文件




static-linking glfw (3)

即使是普通的小型Haskell程序也會變成巨大的可執行文件。

我寫了一個小程序,它被編譯(使用GHC)到大小為7 MB的二進製文件!

什麼會導致一個小的Haskell程序被編譯成巨大的二進製文件?

如果有的話,我能做些什麼來減少這種情況?


Answers

Haskell默認使用靜態鏈接。 這是,整個綁定到OpenGL被複製到您的程序。 由於它們相當大,你的程序會被不必要地誇大。 您可以通過使用動態鏈接解決此問題,但默認情況下它未啟用。


讓我們看看發生了什麼,試試吧

  $ du -hs A
  13M   A

  $ file A
  A: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), 
     dynamically linked (uses shared libs), for GNU/Linux 2.6.27, not stripped

  $ ldd A
    linux-vdso.so.1 =>  (0x00007fff1b9ff000)
    libXrandr.so.2 => /usr/lib/libXrandr.so.2 (0x00007fb21f418000)
    libX11.so.6 => /usr/lib/libX11.so.6 (0x00007fb21f0d9000)
    libGLU.so.1 => /usr/lib/libGLU.so.1 (0x00007fb21ee6d000)
    libGL.so.1 => /usr/lib/libGL.so.1 (0x00007fb21ebf4000)
    libgmp.so.10 => /usr/lib/libgmp.so.10 (0x00007fb21e988000)
    libm.so.6 => /lib/libm.so.6 (0x00007fb21e706000)
    ...      

您從ldd輸出中看到GHC已生成動態鏈接的可執行文件,但只有C庫被動態鏈接 ! 所有的Haskell庫都是逐字拷貝的。

另外:因為這是一個圖形密集型應用程序,我一定會用ghc -O2編譯

有兩件事你可以做。

剝離符號

簡單的解決方案:去掉二進製文件:

$ strip A
$ du -hs A
5.8M    A

條帶丟棄來自目標文件的符號。 它們通常只用於調試。

動態鏈接的Haskell庫

最近,GHC已經獲得了對C和Haskell庫的動態鏈接的支持。 大多數發行版現在分發構建的GHC版本,以支持Haskell庫的動態鏈接。 共享的Haskell庫可以在許多Haskell程序之間共享,而不必每次都將它們複製到可執行文件中。

在撰寫本文時,支持Linux和Windows。

為了讓Haskell庫動態鏈接,你需要使用-dynamic來編譯它們,如下所示:

 $ ghc -O2 --make -dynamic A.hs

另外,任何你想共享的庫都應該使用--enabled-shared來構建:

 $ cabal install opengl --enable-shared --reinstall     
 $ cabal install glfw   --enable-shared --reinstall

最終你會得到一個更小的可執行文件,它具有動態解析C和Haskell依賴關係的能力。

$ ghc -O2 -dynamic A.hs                         
[1 of 4] Compiling S3DM.V3          ( S3DM/V3.hs, S3DM/V3.o )
[2 of 4] Compiling S3DM.M3          ( S3DM/M3.hs, S3DM/M3.o )
[3 of 4] Compiling S3DM.X4          ( S3DM/X4.hs, S3DM/X4.o )
[4 of 4] Compiling Main             ( A.hs, A.o )
Linking A...

而且,瞧!

$ du -hs A
124K    A

你可以剝下更小的:

$ strip A
$ du -hs A
84K A

一個eensy weensy可執行文件,由許多動態鏈接的C和Haskell組件構建而成:

$ ldd A
    libHSOpenGL-2.4.0.1-ghc7.0.3.so => ...
    libHSTensor-1.0.0.1-ghc7.0.3.so => ...
    libHSStateVar-1.0.0.0-ghc7.0.3.so =>...
    libHSObjectName-1.0.0.0-ghc7.0.3.so => ...
    libHSGLURaw-1.1.0.0-ghc7.0.3.so => ...
    libHSOpenGLRaw-1.1.0.1-ghc7.0.3.so => ...
    libHSbase-4.3.1.0-ghc7.0.3.so => ...
    libHSinteger-gmp-0.2.0.3-ghc7.0.3.so => ...
    libHSghc-prim-0.2.0.0-ghc7.0.3.so => ...
    libHSrts-ghc7.0.3.so => ...
    libm.so.6 => /lib/libm.so.6 (0x00007ffa4ffd6000)
    librt.so.1 => /lib/librt.so.1 (0x00007ffa4fdce000)
    libdl.so.2 => /lib/libdl.so.2 (0x00007ffa4fbca000)
    libHSffi-ghc7.0.3.so => ...

最後一點:即使在僅具有靜態鏈接的系統上,也可以使用-split-objs為每個頂級函數獲取一個.o文件,這可以進一步減少靜態鏈接庫的大小。 它需要使用-split-objs來構建GHC,而某些系統會忘記這樣做。


作為抽象的調查員/ Iteratees是由Oleg Kiselyov發明的。 它們提供了一種干淨的IO方式,可以預測(低)資源需求。 目前的Enumerators包非常接近Oleg的原創作品。

為Yesod Web框架創建了管道。 我的理解是它們的設計速度非常快。 該圖書館的早期版本非常有狀態。

管道專注於優雅。 它們只有一種類型而不是幾種,形成monad(變換器)和類別實例,並且在設計中非常“功能”。

如果你喜歡分類解釋: Pipe類型只是免費的monad而不是以下不敬虔的簡單仿函數

data PipeF a b m r = M (m r) | Await (a -> r) | Yield b r
instance Monad m => Functor (PipeF a b m) where
   fmap f (M mr) = M $ liftM mr
   fmap f (Await g) = Await $ f . g
   fmap f (Yield b p) = Yield b (f p)
--Giving:
newtype Pipe a b m r = Pipe {unPipe :: Free (PipeF a b m) r}
  deriving (Functor, Applicative, Monad)

--and
instance MonadTrans (Pipe a b) where
   lift = Pipe . inj . M

在實際的管道定義中,這些是烘焙的,但這個定義的簡單性是驚人的。 管道在操作下形成一個類別(<+<) :: Monad m => Pipe cdmr -> Pipe abmr -> Pipe admr ,它接受第一個管道yields任何內容並將其提供給等待的第二個管道。

看起來Conduits正在變得更像Pipe (使用CPS而不是狀態,並切換到單一類型),而Pipes正在獲得對更好的錯誤處理的支持,並且可能為發電機和消費者返回單獨的類型。

這個地區正在迅速發展。 我一直在使用具有這些功能的管道庫的實驗變體進行攻擊,並且知道其他人也是如此(請參閱Hackage上的Guarded Pipes包),但懷疑Gabriel(Pipes的作者)會在我之前解決它們做。

我的建議:如果您使用的是Yesod,請使用Conduits。 如果您想要一個成熟的庫,請使用Enumerator。 如果您主要關心優雅,請使用Pipe。





haskell linker ghc static-linking glfw