Mit zwanzig Haskell-Zeilen gewinnen: Schreiben Sie Ihre Wc

Hallo Habr.


Kürzlich Siemargllud mich ein, mit Hilfe von Haskell einen merkwürdigen Artikel über den Sieg über Unix zu übersetzen wc. Natürlich werde ich es nicht übersetzen, und das aus mehreren Gründen:


  • Der Autor drückte sich keineswegs alles aus der Single-Threaded-Version heraus, und die Single-Threaded-Version war viel langsamer wc.
  • In diesem Artikel war es notwendig, Multithreading für den Sieg zu verwenden (was an sich ein bisschen Betrug und Sieg ist, eher über den gesunden Menschenverstand als über wc).
  • Dafür musste sich der Autor vertiefen TrichoMonaden und Monoide - nicht, dies ist ein hervorragendes Beispiel für die Reize des monoidalen Denkens, aber meiner Meinung nach ein wenig zu viel für eine solche Aufgabe, zumal aus diesem Grund
  • der Code erwies sich als zu umfangreich,
  • und in der Tat, zu konkurrieren wc, das eine Reihe von Optionen und Funktionen hat, zu erkennen, dass es ein sehr Spielzeug-Analogon ist, im Allgemeinen ist es irgendwie seltsam und sogar ein wenig dumm.

Trotzdem ist es gut , seltsame Dinge zu tun. Deshalb werden wir heute versuchen, den ersten der oben genannten Punkte zu korrigieren und das Ergebnis von Chris (dem Namen des Autors des Originalartikels) zu verbessern.


Wie wir letztes Mal herausgefunden haben, kann ich keinen C-Code schreiben, also werde ich ihn auch nicht schreiben, und als Konkurrent der Haskell-Implementierung habe ich (wie Chris) einen synergistischen wcvon GNU Coreutils. Diese Typen wissen sicherlich, wie man in C schreibt, dieser Code ist kein Jahrzehnt alt, und sie haben sich um die Leistung gekümmert, gemessen an solchen Stücken:


/* 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: Wir werden das System wcohne Probleme um eine Größenordnung überholen , einen vollständig idiomatischen Code erhalten und weniger als eine halbe Stunde für Änderungen und Optimierungen des ursprünglichen Codes aufwenden.


Versuchsbedingungen


Also diskutieren wir zuerst den Versuchsaufbau.


Daten


Ich habe diese Datei heruntergeladen und mit mir zusammengeklebt, sodass die Gesamtgröße ungefähr 1,8 Gigabyte beträgt:


% 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 , , . , .


Um nicht zu quälen, werden wir Pläne für die Zukunft diskutieren: Im nächsten Artikel werden wir uns mit etwas wesentlich Interessanterem befassen, bei dem der Haskell wirklich glänzen wird. Insbesondere modularisieren wir das, was wir heute haben, sehen, wie wir unser Spielzeug ein haskell wcwenig weniger Spielzeug machen können, und bewerten auch, wie sich dies auf die Leistung auswirkt.


All Articles