![](/img/trans.png)
[英]Why is this code getting faster when I'm using way more threads than my CPU has cores?
[英]Why is replicateM (length xs) m way more efficient than sequenceA (fmap (const m) xs)?
我提交的兩個編程問題的不同之處僅在於一個表達式(其中anchors
是一個非空列表,而(getIntegrals n)
是一個狀態單子):
提交 1 . replicateM (length anchors - 1) (getIntegrals n)
提交 2 。 sequenceA $ const (getIntegrals n) <$> tail anchors
我猜這兩個表達式的等價性在編譯時應該很容易看出。 然而,相比之下, sequenceA
更慢,更重要的是,它占用 > 10 倍的內存:
代碼 | 時間 | 記憶 |
---|---|---|
復制一 | 732 毫秒 | 22200 KB |
序列一 | 1435 毫秒 | 262100 KB |
(第二個條目出現“測試 4 時超出內存限制”錯誤,因此情況可能更糟)。
為什么會這樣?
很難預測哪些優化是自動的,哪些不是!
編輯:按照建議,粘貼下面的提交 1代碼。 在這個交互式問題中,“服務器”有一個大小為n
的隱藏樹。 我們的代碼的工作是找出那棵樹,用最少數量的表單查詢? k
? k
。 粗略地說,服務器對? k
? k
是樹的
鄰接
距離矩陣中節點k
對應的行。 我們對k
選擇是:最初是1
,然后是從getAnchors
獲得的一堆節點。
{-# LANGUAGE Safe #-}
{-# OPTIONS_GHC -O2 #-}
import Data.Maybe
import qualified Data.ByteString.Lazy.Char8 as B8
import qualified Data.ByteString.Builder as Bu
import Data.Functor.Identity
import Control.Monad.Trans.State
import Control.Monad
import Control.Applicative
import Data.ByteString.Builder.Extra (flush)
import System.IO
type St = StateT [B8.ByteString] Identity
solve :: St Bu.Builder
solve = do
n <- getIntegral
ds <- getIntegrals n -- get the first row of adjacency matrix
let
anchors = getAnchors ds
readFirst = if head anchors==1 then return ds else getIntegrals n
readRest = replicateM (length anchors - 1) (getIntegrals n) -- get some other rows too
adjss <- liftA2 (:) readFirst readRest
let
adj1ss = [map snd $ filter ((1==).fst) (zip adjs [1..]) | adjs <- adjss]
s0 = Bu.string7
snl = Bu.string7 "\n" <> flush
i0 = Bu.intDec
printEdge src dst = i0 src <> s0 " " <> i0 dst <> snl
printAdj (src,dsts) = mconcat [printEdge src dst | dst<-dsts]
printAdjs = mconcat $ printAdj <$> zip anchors adj1ss
ask k = s0 "? " <> i0 k <> snl
askRest = mconcat $ ask <$> (dropWhile (==1) anchors)
return $ ask 1 <> askRest <> s0 "!" <> snl <> printAdjs
getAnchors :: [Int]->[Int]
getAnchors xs = reverse $ go (zip xs [1..]) [] [] where
go [] odds evens = if length odds < length evens then odds else evens
go ((k,i):rest) odds evens
| even k = go rest odds (i: evens)
| odd k = go rest (i: odds) evens
getByteString :: St B8.ByteString
getByteString = state getNext where
getNext [] = (B8.take 0 (B8.pack "."),[])
getNext (w:ws) = (w,ws)
getIntegral :: Num t => St t
getIntegral = convertToNum <$> getByteString where
convertToNum x = fromIntegral $ fromMaybe 0 $ liftA fst $ B8.readInteger x
getIntegrals :: Num t => Int -> St [t]
getIntegrals n = replicateM n getIntegral
main :: IO ()
main = do
hSetBuffering stdout NoBuffering
bytestrings <- B8.words <$> B8.getContents
B8.putStr $ Bu.toLazyByteString $ evalState solve bytestrings
這里的問題與內聯有關。 我不完全理解它,但這是我的理解。
首先,我們發現將replicateM
的定義復制並粘貼到Submission 1 會產生與Submission 2 ( submit ) 相同的糟糕性能。 然而,如果我們用INLINABLE
pragma 替換replicateM
的NOINLINE
pragma,事情NOINLINE
起作用(提交)。
在INLINABLE
對編譯replicateM
是從不同的INLINE
編譯,后者導致比前者更聯處理。 具體來說,如果我們在同一個文件中定義了replicateM
,那么Haskells heuristic for inlining 決定內聯,但是使用replicateM
from base 在這種情況下,即使存在INLINABLE
pragma,它也決定不進行內聯。
另一方面, sequenceA
和traverse
都有導致內聯的INLINE
編譯指示。 從上面的實驗中得到一個提示,我們可以定義一個不可內聯的sequenceA
A,這確實使解決方案 2 起作用(提交)。
{-# NOINLINE sequenceA' #-}
sequenceA' :: [St x] -> St [x]
sequenceA' = sequenceA
以下是我的一些非常嚴重的猜測。
但是內聯是如何引起問題的呢? 好吧,讓我們看看以下兩個核心轉儲之間的區別
在這里,我們兩次查看對應的內容,第一個實例是內聯部分,第二個實例是對replicateM 的實際調用。
readRest = replicateM (length anchors - 1) (getIntegrals n)
現在有趣的一點是,在內聯代碼中,黃色突出顯示的行在replicateM
每個循環中運行,而在非內聯部分,它們在傳遞給replicateM
的lambda 抽象之外計算一次。
但他們在做什么? 核心中有多個變量叫ds
,但是這個是指這個:
這又對應於
solve = do
n <- getIntegral
所以我認為正在發生的事情是,它不是運行getIntegral
一次並保存結果,而是保存它的起始狀態,並在每次循環時以該狀態重新運行。 確實將這一行更改為以下內容(需要 BangPatterns 語言擴展)修復所有版本(提交)。
solve = do
!n <- getIntegral
我仍然不確定,但這是我最好的猜測。
這里有兩個核心轉儲供參考: Inline , Noinline
嗯,是的,但我覺得這里的潛在問題是你使用了惰性 IO 和惰性狀態。 使用嚴格狀態轉換器 Haskell 可能會想出不保留舊狀態(我不知道,只是猜測),但是我們不能在這里使用嚴格狀態,因為您依賴於惰性 IO,即獲取所有輸入一開始使用getContents
並懶惰地強制它,同時確保在強制太多之前提供輸出。 相反,逐行顯式讀取輸入會更安全。 StateT [ByteString]
替換為IO
或更花哨的東西,例如Conduit
或Pipe
。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.