Подтвердить что ты не робот

Производительность Haskell, реализующая Unix-программу "cat" с Data.ByteString

У меня есть следующий код Haskell, реализующий простую версию утилиты командной строки "cat" unix. Тестирование производительности с "временем" в файле 400 МБ, примерно на 3 раза медленнее. (точный script, который я использую, чтобы проверить, что он ниже кода).

Мои вопросы:

  • Является ли это допустимым испытанием производительности?
  • Как я могу заставить эту программу работать быстрее?
  • Как я могу определить узкие места производительности в программах Haskell вообще?

Что касается вопросов 2 и 3: я использовал GHC -prof, а затем работал с + RTS -p, но здесь я нашел вывод немного неинформативным.

Источник (Main.hs)

module Main where

import System.IO
import System.Environment
import Data.ByteString as BS

import Control.Monad

-- Copied from cat source code
bufsize = 1024*128

go handle buf = do
  hPut stdout buf
  eof <- hIsEOF handle
  unless eof $ do
    buf <- hGetSome handle bufsize
    go handle buf

main = do
  file    <- fmap Prelude.head getArgs
  handle  <- openFile file ReadMode
  buf     <- hGetSome handle bufsize
  hSetBuffering stdin $ BlockBuffering (Just bufsize)
  hSetBuffering stdout $ BlockBuffering (Just bufsize)
  go handle buf

Сроки script (run.sh):

#!/usr/bin/env bash

# Generate 10M lines of silly test data
yes aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa | head -n 10000000 > huge

# Compile with optimisation
ghc -O2 Main.hs

# Run haskell
echo "timing Haskell"
time ./Main huge > /dev/null

echo ""
echo ""

# Run cat
echo "timing 'cat'"
time cat huge > /dev/null

Мои результаты:

timing Haskell

real    0m0.980s
user    0m0.296s
sys     0m0.684s


timing 'cat'

real    0m0.304s
user    0m0.001s
sys     0m0.302s

Отчет о профилировании при компиляции с -prof и запуском с + RTS -p приведен ниже:

  Sat Dec 13 21:26 2014 Time and Allocation Profiling Report  (Final)

     Main +RTS -p -RTS huge

  total time  =        0.92 secs   (922 ticks @ 1000 us, 1 processor)
  total alloc = 7,258,596,176 bytes  (excludes profiling overheads)

COST CENTRE MODULE  %time %alloc

MAIN        MAIN    100.0  100.0


                                                       individual     inherited
COST CENTRE MODULE                   no.     entries  %time %alloc   %time %alloc

MAIN        MAIN                      46           0  100.0  100.0   100.0  100.0
 CAF        GHC.Conc.Signal           84           0    0.0    0.0     0.0    0.0
 CAF        GHC.IO.FD                 82           0    0.0    0.0     0.0    0.0
 CAF        GHC.IO.Handle.FD          81           0    0.0    0.0     0.0    0.0
 CAF        System.Posix.Internals    76           0    0.0    0.0     0.0    0.0
 CAF        GHC.IO.Encoding           70           0    0.0    0.0     0.0    0.0
 CAF        GHC.IO.Encoding.Iconv     69           0    0.0    0.0     0.0    0.0
4b9b3361

Ответ 1

Это лишь частичный ответ, который пытается решить второй вопрос:

Я попробовал что-то вроде этого с помощью GHC.IO.Buffer API:

module Main where

import System.IO
import System.Environment
import GHC.IO.Buffer
import Data.ByteString as BS

import Control.Monad

-- Copied from cat source code
bufsize = 1024*128

go handle bufPtr = do
  read <- hGetBuf handle bufPtr bufsize
  when (read > 0) $ do
    hPutBuf stdout bufPtr read
    go handle bufPtr

main = do
  file    <- fmap Prelude.head getArgs
  handle  <- openFile file ReadMode
  buf     <- newByteBuffer bufsize WriteBuffer

  withBuffer buf $ go handle

и, похоже, он приближается к производительности "cat", но все же определенно медленнее...

time ./Cat huge > /dev/null 
./Cat huge > /dev/null  0.00s user 0.06s system 76% cpu 0.081 total

time cat huge > /dev/null  
cat huge > /dev/null  0.00s user 0.05s system 75% cpu 0.063 total

Я думаю, используя API-интерфейс буфера, мы можем явно не выделять все бут-байты при использовании hGetSome, как в исходном коде, но я просто угадываю здесь и не знаю, что именно происходит в обоих скомпилированных кодах...

UPDATE: добавление исходного кода на моем ноутбуке:

time ./Cat2 huge > /dev/null
./Cat2 huge > /dev/null  0.12s user 0.10s system 99% cpu 0.219 total

ОБНОВЛЕНИЕ 2: добавление некоторых базовых результатов профилирования:

Оригинальный код:

Cat2 +RTS -p -RTS huge

    total time  =        0.21 secs   (211 ticks @ 1000 us, 1 processor)
    total alloc = 6,954,068,112 bytes  (excludes profiling overheads)

COST CENTRE MODULE  %time %alloc

MAIN        MAIN    100.0  100.0


                                                       individual     inherited
COST CENTRE MODULE                   no.     entries  %time %alloc   %time %alloc

MAIN        MAIN                      46           0  100.0  100.0   100.0  100.0
 CAF        GHC.IO.Handle.FD          86           0    0.0    0.0     0.0    0.0
 CAF        GHC.Conc.Signal           82           0    0.0    0.0     0.0    0.0
 CAF        GHC.IO.Encoding           80           0    0.0    0.0     0.0    0.0
 CAF        GHC.IO.FD                 79           0    0.0    0.0     0.0    0.0
 CAF        System.Posix.Internals    75           0    0.0    0.0     0.0    0.0
 CAF        GHC.IO.Encoding.Iconv     72           0    0.0    0.0     0.0    0.0

Код Buffer-API:

Cat +RTS -p -RTS huge

    total time  =        0.06 secs   (61 ticks @ 1000 us, 1 processor)
    total alloc =   3,487,712 bytes  (excludes profiling overheads)

COST CENTRE MODULE  %time %alloc

MAIN        MAIN    100.0   98.9


                                                      individual     inherited
COST CENTRE MODULE                  no.     entries  %time %alloc   %time %alloc

MAIN        MAIN                     44           0  100.0   98.9   100.0  100.0
 CAF        GHC.IO.Handle.FD         85           0    0.0    1.0     0.0    1.0
 CAF        GHC.Conc.Signal          82           0    0.0    0.0     0.0    0.0
 CAF        GHC.IO.Encoding          80           0    0.0    0.1     0.0    0.1
 CAF        GHC.IO.FD                79           0    0.0    0.0     0.0    0.0
 CAF        GHC.IO.Encoding.Iconv    71           0    0.0    0.0     0.0    0.0

Обратите внимание, что большая разница в расходах на размещение...

Ответ 2

Первоначальный вопрос заставил меня подумать, что речь идет о поиске проблемы производительности в точном коде. Поскольку комментарий "Я надеюсь пойти на более идиоматический/высокий уровень" Решение Haskell ", противоречит этому предположению, я дам разумно исполняющееся идиоматическое решение Haskell.

Как бы я ни ожидал, любой случайный программист, знакомый с Haskell для решения этой проблемы, - с Lazy bytestrings. Это позволяет программисту просто указать задачу чтения ввода и ввода вывода, позволяя компилятору беспокоиться о том, чтобы сбрасывать с помощью буферизационных и циклических конструкций.

где

import System.IO
import System.Environment
import Data.ByteString.Lazy as BS

import Control.Monad

main :: IO ()
main = do
  file    <- fmap Prelude.head getArgs
  handle  <- openFile file ReadMode
  buf     <- BS.hGetContents handle
  hPut stdout buf

Результат является более читабельным и более эффективным, чем код в исходном вопросе:

timing 'cat'

real    0m0.075s
user    0m0.000s
sys     0m0.074s
timing strict bytestring with GHC -O2

real    0m0.254s
user    0m0.126s
sys     0m0.127s
timing strict bytestring with GHC -O2 -fllvm

real    0m0.267s
user    0m0.132s
sys     0m0.134s
timing lazy bytestring with GHC -O2

real    0m0.091s
user    0m0.023s
sys     0m0.067s
timing lazy bytestring with GHC -O2 -fllvm

real    0m0.091s
user    0m0.021s
sys     0m0.069s

То есть, ленивое решение для быстрого тестирования на 21% медленнее, чем cat. Полагая cat последним для преференциального поведения кеширования, получается в 59 мс время исполнения решения Haskell на 51% медленнее.

РЕДАКТИРОВАТЬ: Доны, предлагаемые с использованием отображаемого в памяти IO, более точно моделируют поведение кошки. Я не уверен, насколько точно это утверждение, но mmap почти всегда приводит к лучшей производительности, и эта ситуация, конечно же, не является исключением:

timing memory mapped lazy bytestring with GHC -O2

real    0m0.008s
user    0m0.004s
sys     0m0.003s

Что было создано:

module Main where

import System.IO (stdout)
import System.Environment
import System.IO.Posix.MMap.Lazy
import Data.ByteString.Lazy (hPut)

import Control.Monad

main :: IO ()
main = do
  file    <- fmap Prelude.head getArgs
  buf     <- unsafeMMapFile file
  hPut stdout buf

Ответ 3

Замечание post festum:

Я не уверен, что вопрос в том, что люди немного загрузили его. Я хотел посмотреть, что было с bytestring-mmap, и поэтому я сделал версию для труб, чтобы "исправить" свой ленивый модуль тестирования. https://github.com/michaelt/pipes-bytestring-mmap В результате я собрал все эти программы, используя метод тестирования sibi. Единственными двумя модулями в https://github.com/michaelt/pipes-bytestring-mmap/tree/master/bench, которые кажутся чем-то вроде глупого хлеба-масла, являются те, которые используют причудливое явное управление буферами.

В любом случае, вот некоторые результаты: размер файла увеличивается на 10 *, когда мы двигаемся вправо. Интересно посмотреть, насколько отличаются программы в разных размерах файлов. Программы, которые не используют mmap, начинают показывать свой символ как "линейный по длине файла" на 420M. В этот момент и после этого все они почти точно совпадают, предполагая, что довольно расходящееся поведение при меньших размерах нельзя воспринимать слишком серьезно. Файлы mmap все ведут себя одинаково (друг с другом) с несколькими раритетами (которые я реплицировал). Все это на os x.

4200000           42000000          420000000         4200000000

timing 'cat'

real  0m0.006s    real  0m0.013s    real  0m0.919s    real  0m8.154s
user  0m0.002s    user  0m0.002s    user  0m0.005s    user  0m0.028s
sys   0m0.003s    sys   0m0.009s    sys   0m0.223s    sys   0m2.179s


timing lazy bytestring - idiomatic Haskell (following Thomas M. DuBuisson) 

real  0m0.009s    real  0m0.025s    real  0m0.894s    real  0m9.146s
user  0m0.002s    user  0m0.006s    user  0m0.078s    user  0m0.787s
sys   0m0.005s    sys   0m0.016s    sys   0m0.288s    sys   0m3.001s


timing fancy buffering following statusfailed

real  0m0.014s    real  0m0.066s    real  0m0.876s    real  0m8.686s
user  0m0.005s    user  0m0.028s    user  0m0.278s    user  0m2.724s
sys   0m0.007s    sys   0m0.035s    sys   0m0.424s    sys   0m4.232s


timing fancier use of GHC.Buf following bmk

real  0m0.011s    real  0m0.018s    real  0m0.831s    real  0m8.218s
user  0m0.002s    user  0m0.003s    user  0m0.034s    user  0m0.289s
sys   0m0.006s    sys   0m0.013s    sys   0m0.236s    sys   0m2.447s


timing Pipes.ByteString following sibi

real  0m0.012s    real  0m0.020s    real  0m0.845s    real  0m8.241s
user  0m0.003s    user  0m0.004s    user  0m0.020s    user  0m0.175s
sys   0m0.007s    sys   0m0.014s    sys   0m0.239s    sys   0m2.509s

Тогда с mmap

timing Lazy.MMap following dons and Thomas M. DuBuisson 

real  0m0.006s    real  0m0.006s    real  0m0.037s    real  0m0.133s
user  0m0.002s    user  0m0.002s    user  0m0.006s    user  0m0.051s
sys   0m0.003s    sys   0m0.003s    sys   0m0.013s    sys   0m0.061

timing Pipes.ByteString.MMap with SafeT machinery

real  0m0.006s    real  0m0.010s    real  0m0.051s    real  0m0.196s
user  0m0.002s    user  0m0.004s    user  0m0.012s    user  0m0.099s
sys   0m0.003s    sys   0m0.005s    sys   0m0.016s    sys   0m0.072s


timing Pipes.ByteString.MMap 'withFile' style

real  0m0.008s    real  0m0.008s    real  0m0.142s    real  0m0.134s
user  0m0.002s    user  0m0.002s    user  0m0.007s    user  0m0.046s
sys   0m0.004s    sys   0m0.004s    sys   0m0.016s    sys   0m0.066s