簡體   English   中英

為什么將數據重構為 newtype 會加速我的 haskell 程序?

[英]Why does refactoring data to newtype speed up my haskell program?

我有一個程序,它遍歷一個表達式樹,它對概率分布進行代數,采樣或計算結果分布。

我有兩種計算分布的實現:一種( computeDistribution )可以很好地與 monad 轉換器重用,另一種( simpleDistribution )我手動將所有內容具體化。 我不想手動具體化所有內容,因為那將是采樣和計算代碼之間的代碼重復。

我也有兩個數據表示:

type Measure a = [(a, Rational)]
-- data Distribution a = Distribution (Measure a) deriving Show
newtype Distribution a = Distribution (Measure a) deriving Show

當我將data版本與可重用代碼一起使用時,計算 20d2 ( ghc -O3 program.hs; time ./program 20 > /dev/null ) 的分布大約需要一秒鍾,這似乎太長 選擇更高的n值,后果自負。

當我使用手動具體化的代碼,或者我在任一實現中使用newtype表示時,計算 20d2 ( time ./program 20 s > /dev/null ) 眨眼之間。

為什么?

我怎樣才能找出原因?

我對 Haskell 如何執行的知識幾乎為零。 我收集了一個與程序基本相同形狀的 thunk 圖,但這就是我所知道的。

我認為使用newtypeDistribution的表示與Measure的表示相同,即它只是一個列表,而對於data版本,每個Distribution有點像一個單字段記錄,除了指向包含列表的指針,所以data版本必須執行更多分配。 這是真的? 如果屬實,這足以解釋性能差異嗎?

我是使用 monad 變壓器堆棧的新手。 考慮simpleDistribution中的LetUniform案例——它們是否與基於walkTree的實現相同? 我該怎么說?

這是我的程序。 請注意, Uniform n對應於滾動 n 面骰子(以防一元性令人驚訝)。

更新:根據評論,我通過刪除不影響性能差距的所有內容來簡化我的程序。 我做了兩個語義上的改變:概率現在是非規范化的,而且都是不穩定的和錯誤的,簡化步驟已經消失了。 但是我的程序的基本形狀仍然存在。 (請參閱非簡化程序的問題編輯歷史。)

更新 2 :我做了進一步的簡化,將Distribution減少到 list monad 並稍作改動,刪除了與概率有關的所有內容,並縮短了名稱。 使用data但不是newtype時,我仍然觀察到很大的性能差異。

import Control.Monad (liftM2)
import Control.Monad.Trans (lift)
import Control.Monad.Reader (ReaderT, runReaderT)
import System.Environment (getArgs)
import Text.Read (readMaybe)

main = do
  args <- getArgs
  let dieCount = case map readMaybe args of Just n : _ -> n; _ -> 10
  let f = if ["s"] == (take 1 $ drop 1 $ args) then fast else slow
  print $ f dieCount

fast, slow :: Int -> P Integer
fast n = walkTree n
slow n = walkTree n `runReaderT` ()

walkTree 0 = uniform
walkTree n = liftM2 (+) (walkTree 0) (walkTree $ n - 1)

data P a = P [a] deriving Show
-- newtype P a = P [a] deriving Show

class Monad m => MonadP m where uniform :: m Integer
instance MonadP P where uniform = P [1, 1]
instance MonadP p => MonadP (ReaderT env p) where uniform = lift uniform

instance Functor P where fmap f (P pxs) = P $ fmap f pxs

instance Applicative P where
  pure x = P [x]
  (P pfs) <*> (P pxs) = P $ pfs <*> pxs

instance Monad P where
  (P pxs) >>= f = P $ do
    x <- pxs
    case f x of P fxs -> fxs

我怎樣才能找出原因?

一般來說,這很難。

做到這一點的極端方法是查看核心代碼(您可以通過使用-ddump-simpl運行 GHC 來生成這些代碼)。 這很快就會變得復雜,它基本上是一門全新的語言來學習。 您的程序已經足夠大,以至於我無法從核心轉儲中學到很多東西。

找出原因的另一種方法是繼續使用 GHC 並提出問題並學習 GHC 優化,直到您認識到某些模式。

為什么?

簡而言之,我相信這是由於列表融合。

注意:我不確定這個答案是否正確,並且需要比我現在願意投入更多的時間/工作來驗證。 也就是說,它符合證據。

首先,我們可以通過在O0中運行(即沒有優化)來檢查您看到的這種減速是否是真正基本的與 GHC 優化觸發的結果。 在這種模式下,兩種Distribution表示都會產生大致相同(非常長)的運行時間。 這讓我相信,問題本質上不是數據表示,而是由新類型版本觸發的newtype ,而不是data版本。

當 GHC 在-O1或更高版本中運行時,它會使用某些重寫規則來將不同的折疊和列表映射融合在一起,這樣它就不需要分配中間值。 (請參閱https://markkarpov.com/tutorial/ghc-optimization-and-fusion.html#fusion以獲取有關此概念的體面教程以及https://stackoverflow.com/a/38910170/14802384 ,其中還有一個鏈接到包含所有重寫規則的要點base 。)由於computeDistribution基本上只是一堆列表操作(本質上都是折疊),因此這些操作有可能被觸發。

關鍵是使用Distributionnewtype表示,newtype 包裝器在編譯期間被擦除,並且允許列表操作融合。 但是,使用data表示,不會刪除包裝器,並且不會觸發重寫規則。

因此,我將提出一個未經證實的主張:如果您希望您的data表示與新類型一樣newtype ,您將需要設置類似於列表折疊但適用於Distribution類型的重寫規則。 這可能涉及編寫您自己的特殊折疊函數,然后重寫您的 Functor/Applicative/Monad 實例以使用它們。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM