Bonjour, Habr.
L'autre jour Siemarglm'a invité à traduire un article curieux sur la victoire contre Unix wc
avec l'aide de Haskell. Bien sûr, je ne le traduirai pas, et pour plusieurs raisons:
- l'auteur a évincé la version monothread en aucun cas tout, et la version monothread était beaucoup plus lente
wc
, - dans cet article, pour gagner, il fallait utiliser le multithreading (ce qui en soi est un peu de la triche et de la victoire plutôt sur le bon sens que sur
wc
), - pour cela, l’auteur a dû
trichomonades et monoïdes - non, c'est une excellente illustration des charmes de la pensée monoïdale, mais à mon humble avis un peu trop pour une telle tâche, d'autant plus qu'en raison de cette - le code s'est avéré trop volumineux,
- et en effet, pour concurrencer
wc
, qui a un tas d'options et de fonctionnalités, réalisant que c'est un analogue très jouet, en général c'est en quelque sorte étrange et même un peu stupide.
Néanmoins, faire des choses étranges est une bonne chose, donc aujourd'hui nous allons essayer de corriger le premier des points ci-dessus et d'améliorer le résultat de Chris (le nom de l'auteur de l'article original).
Encore une fois, comme nous l'avons découvert la dernière fois, je ne peux pas écrire de code C, donc je ne l'écrirai pas non plus, et en tant que concurrent de l'implémentation Haskell, j'ai (comme Chris) un wc
système GNU Coreutils. Ces mecs savent certainement écrire en C, ce code n'a pas une décennie, et ils ont pris soin de la performance, à en juger par ces morceaux:
Spoiler: nous dépasserons le système d' wc
environ un ordre de grandeur sans aucun problème, obtenant un code complètement idiomatique et passant moins d'une demi-heure sur les modifications et l'optimisation du code d'origine.
Conditions d'expérimentation
Donc, nous discutons d'abord de la configuration expérimentale.
Les données
J'ai téléchargé ce fichier et je l'ai collé avec moi, donc la taille totale est d'environ 1,8 gigaoctets:
% 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
, , . , .
Eh bien, afin de ne pas tourmenter, nous discuterons des plans pour l'avenir: dans le prochain article, nous traiterons de quelque chose de beaucoup plus intéressant, où le Haskell brillera vraiment. Plus précisément, nous modularisons ce que nous avons aujourd'hui, voyons comment rendre notre jouet un haskell wc
peu moins jouet et évaluons également comment cela affectera les performances.