Olá, Habr.
O outro dia Siemarglme convidou para traduzir um artigo curioso sobre a vitória sobre o Unix wccom a ajuda de Haskell. Obviamente, não o traduzirei e por várias razões:
- o autor tirou da versão single-threaded de maneira alguma tudo, e a versão single-threaded foi muito mais lenta
wc, - nesse artigo, para vencer, era necessário usar multithreading (que por si só é um pouco de trapaça e vitória, e não sobre o senso comum, e não sobre
wc), - para isso, o autor teve que se aprofundar
trichomônadas e monoides - não, esta é uma excelente ilustração dos encantos do pensamento monoidal, mas IMHO é um pouco demais para essa tarefa, especialmente porque por causa disso - o código acabou sendo muito volumoso,
- e, de fato, competir com
wc, que tem um monte de opções e recursos, percebendo que é um análogo muito de brinquedo, em geral é de alguma forma estranho e até um pouco bobo.
No entanto, fazer coisas estranhas é uma coisa boa, então hoje tentaremos corrigir o primeiro dos pontos acima e melhorar o resultado de Chris (o nome do autor do artigo original).
Novamente, como descobrimos na última vez, não sei escrever código C, então também não escrevo, e como concorrente da implementação Haskell, eu (como Chris) tenho um wcsistema GNU Coreutils. Esses caras certamente sabem escrever em C, esse código não tem uma década e cuidaram da performance, a julgar por tais peças:
Spoiler: ultrapassaremos o sistema wcem uma ordem de magnitude sem problemas, obtendo um código completamente idiomático e gastando menos de meia hora em alterar e otimizar o código original.
Condições da experiência
Então, primeiro discutimos a configuração experimental.
Dados
Eu baixei esse arquivo e colei comigo, então o tamanho total é de aproximadamente 1,8 gigabytes:
% for i in `seq 1 10`; cat part.txt >> test.txt
% du -sh test.txt
1.8G test.txt
, test.txt tmpfs-, , IO.
Gentoo Linux Core i7 4770 32 .
- ghc 8.8.2.
wc coreutils 8.31, gcc 9.2 -O2 -march=native:
% wc --version
wc (GNU coreutils) 8.31
Packaged by Gentoo (8.31-r1 (p0))
, -march=native — C, - - , : x86_64-, wc coreutils ( SIMD-, ).
, -O3 -O2 — , -O2.
, time. time , , ,
- 0.3 ,
- , , .
, ( ) , .
? wc!
Unicode ( ), C-, wc LANG=C LC_ALL=C wc /path/to/test.txt. ? 10.4 . .
, (ru_RU.UTF-8) wc (7.20 ), , , , C- — , .
, ( ), 0.2 — , IO-bound-.
Haskell
, ?
, cs bs ( , ):
import qualified Data.ByteString.Lazy.Char8 as BS
import Data.Char
wc :: BS.ByteString -> (Int, Int, Int)
wc s =
let (bs, ws, ls, _) = BS.foldl' go (0, 0, 0, False) s
in (bs, ws, ls)
where
go :: (Int, Int, Int, Bool) -> Char -> (Int, Int, Int, Bool)
go (!bs, !ws, !ls, !wasSpace) c =
let addLine | c == '\n' = 1
| otherwise = 0
addWord | wasSpace = 0
| isSpace c = 1
| otherwise = 0
in (bs + 1, ws + addWord, ls + addLine, isSpace c)
, , ( ).
, , 9 wc. : 31.2 , 306% wc. 9 , , .
, ( , ), , , , .
?
-, , — . :
{-# LANGUAGE Strict #-}
{-# LANGUAGE RecordWildCards #-}
import qualified Data.ByteString.Lazy.Char8 as BS
import Data.Char
data State = State
{ bs :: Int
, ws :: Int
, ls :: Int
, wasSpace :: Bool
}
wc :: BS.ByteString -> (Int, Int, Int)
wc s = (bs, ws, ls)
where
State { .. } = BS.foldl' go (State 0 0 0 False) s
go State { .. } c = State (bs + 1) (ws + addWord) (ls + addLine) (isSpace c)
where
addLine | c == '\n' = 1
| otherwise = 0
addWord | wasSpace = 0
| isSpace c = 1
| otherwise = 0
bang-, - {-# LANGUAGE Strict #-}. , ?
, , — 4 ! 7.56 , 75% wc, ! ?
, : , , . -
data State = State
{ bs :: {-# UNPACK #-} !Int
, ws :: {-# UNPACK #-} !Int
, ls :: {-# UNPACK #-} !Int
, wasSpace :: !Bool
}
Int- . , nursery area, , , — , .
CSE
, isSpace c , — . , , go :
go State { .. } c = State (bs + 1) (ws + addWord) (ls + addLine) isSp
where
isSp = isSpace c
addLine | c == '\n' = 1
| otherwise = 0
addWord | wasSpace = 0
| isSp = 1
| otherwise = 0
? — 2.93 , 28% wc.
? ghc , , ? — isSpace ( ) , common subexpression elimination.
— ( ). , wc , . - , :
- LLVM- (
-fllvm) — , . - (
-O2) — -O , -O2 , ? - (
{-# INLINE wc #-}). , - , , , - . , , , .
, , , .
:
. -, . , -O2 , , LLVM- , ( ).
, . , C (19%!). , .
.
, State, .
, Bool , . , Int, 1 True, 0 — False? , , !
data State = State
{ bs :: Int
, ws :: Int
, ls :: Int
, wasSpace :: Int
}
wc :: BS.ByteString -> (Int, Int, Int)
wc s = (bs, ws, ls)
where
State { .. } = BS.foldl' go (State 0 0 0 0) s
go State { .. } c = State (bs + 1) (ws + addWord) (ls + addLine) isSp
where
isSp | isSpace c = 1
| otherwise = 0
addLine | c == '\n' = 1
| otherwise = 0
addWord | wasSpace == 1 = 0
| isSp == 1 = 1
| otherwise = 0
(, Bool), . , , addWord guard' ( if) :
addWord = (1 - wasSpace) * isSp
.
? , , 1.91 , 18% wc. , .
, ?
ASCII, isSpace. , . , , , .
, Data.ByteString.Lazy Data.ByteString.Lazy.Char8, Char.
:
{-# LANGUAGE Strict #-}
{-# LANGUAGE RecordWildCards #-}
module Data.WordCount where
import qualified Data.ByteString.Lazy as BS
data State = State
{ bs :: Int
, ws :: Int
, ls :: Int
, wasSpace :: Int
}
wc :: BS.ByteString -> (Int, Int, Int)
wc s = (bs, ws, ls)
where
State { .. } = BS.foldl' go (State 0 0 0 0) s
go State { .. } c = State (bs + 1) (ws + addWord) (ls + addLine) isSp
where
isSp | c == 32 || c - 9 <= 4 = 1
| otherwise = 0
addLine | c == 10 = 1
| otherwise = 0
addWord = (1 - wasSpace) * isSp
{-# INLINE wc #-}
, 1.45 , 14% wc.
, — , , .
, , . , : wasSpace , , :
wc s = (bs, ws + 1 - wasSpace, ls)
where
State { .. } = BS.foldl' go (State 0 0 0 1) s
...
, .
:
0.35 , ? - - ?
, , wc. :
main :: IO ()
main = do
[path] <- getArgs
contents <- BS.readFile path
print $ wc contents
readFile . mmap. bytestring-mmap, ByteString' mmap- , main
main :: IO ()
main = do
[path] <- getArgs
contents <- unsafeMMapFile path
print $ wc contents
, unsafeMMapFile ByteString, wc, . , unsafeMMapFile, , mmap- , .
— , 0.04-0.06 0.35 . , .
mmap , : , , . , : , .
?
, , Unix-, . , , ST .
, , : , wc, , « » wc , , . , .
Bem, para não atormentar, discutiremos planos para o futuro: no próximo artigo, trataremos de algo substancialmente mais interessante, onde o Haskell realmente brilhará. Mais especificamente, modularizamos o que temos hoje, vemos como tornar nosso brinquedo um haskell wcpouco menos e também avaliamos como isso afetará o desempenho.