Gagner avec vingt lignes Haskell: écrire votre Wc

Bonjour, Habr.


L'autre jour Siemarglm'a invité à traduire un article curieux sur la victoire contre Unix wcavec 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 wcsystè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:


/* If the average line length in the block is >= 15, then use
   memchr for the next block, where system specific optimizations
   may outweigh function call overhead.
   FIXME: This line length was determined in 2015, on both
   x86_64 and ppc64, but it's worth re-evaluating in future with
   newer compilers, CPUs, or memchr() implementations etc.  */

Spoiler: nous dépasserons le système d' wcenviron 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 , , ,


  1. 0.3 ,
  2. , , .

, ( ) , .



? 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 , . - , :


  1. LLVM- ( -fllvm) — , .
  2. ( -O2) — -O , -O2 , ?
  3. ( {-# INLINE wc #-}). , - , , , - . , , , .

, , , .


:


LLVM-O2,% wc
2.9328
3.9638
2.6126
2.5925
2.2321
2.0219
2.0119
2.0119

. -, . , -O2 , , LLVM- , ( ).


, . , C (19%!). , .


.



, State, .


, Bool , . , Int, 1 True, 0False? , , !


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 wcpeu moins jouet et évaluons également comment cela affectera les performances.


All Articles