皆さんは yes
コマンドをご存知でしょうか。私は知りませんでした、以下の記事を読むまでは。
上記記事は 10 分程度で読めて、かつとてもおもしろいので是非読んで欲しいのですが、忙しい諸兄姉のために 3 行でまとめます。
-
yes
という y を標準出力するだけのコマンドがあるよ - 1979 年から現在まで活発に開発が続けられているよ
- Rust で実装したらめっちゃ速くなったよ
これを読んで、「Haskell ならどれぐらいのスループットが出るんだろう」と思ったので実装してみました。
組み込みの yes
まずは組み込みの yes
コマンドが Mac Book Pro 2015 年モデルでどれ位のものなのか見てみます。
$ yes | pv -r > /dev/null
[40.6MiB/s]
なるほど。
普通の Haskell 実装
では普通に Haskell で実装した場合どれぐらいでしょうか。
module Main where
main :: IO ()
main = loop
where
loop = putStrLn "y" >> loop
これを実行してみます。
$ .stack-work/dist/x86_64-osx/Cabal-1.24.2.0/build/yes/yes | pv -r > /dev/null
[10.9MiB/s]
なるほど。Python の 2 倍ぐらいは出てますかね。
最適化オプション
stack で作成したテンプレートには最適化オプションがついていないので付けてみます。ついでに不要なオプションは削除します。
diff --git a/yes.cabal b/yes.cabal
index 5b3fd34..a780153 100644
--- a/yes.cabal
+++ b/yes.cabal
@@ -16,7 +16,7 @@ cabal-version: >=1.10
executable yes
hs-source-dirs: app
main-is: Main.hs
- ghc-options: -threaded -rtsopts -with-rtsopts=-N
+ ghc-options: -O2
build-depends: base
default-language: Haskell2010
上記変更を加えた状態で実行した結果です。
$ .stack-work/dist/x86_64-osx/Cabal-1.24.2.0/build/yes/yes | pv -r > /dev/null
[10.9MiB/s]
何も変わりませんでした。
ByteString を使用する
Haskell の String は遅いので ByteString を使用します。
{-# LANGUAGE OverloadedStrings #-}
module Main where
import qualified Data.ByteString.Char8 as BC8
main :: IO ()
main = loop
where
loop = BC8.putStrLn "y" >> loop
この結果は以下の通りです。
$ .stack-work/dist/x86_64-osx/Cabal-1.24.2.0/build/yes/yes | pv -r > /dev/null
[23.3MiB/s]
倍以上のスループットが出るようになりました。
低レベル I/O
putStrLn
ではなく hPutStrLn
を使用してみます。
{-# LANGUAGE OverloadedStrings #-}
module Main where
import qualified Data.ByteString.Char8 as BC8
import System.IO
main :: IO ()
main = loop
where
loop = BC8.hPutStrLn stdout "y" >> loop
この結果は以下のとおりです。
$ .stack-work/dist/x86_64-osx/Cabal-1.24.2.0/build/yes/yes | pv -r > /dev/null
[23.7MiB/s]
若干スループットが上がったようですが、誤差の範囲です。
再起をやめる
戦略を変更し再起をやめてみます。
{-# LANGUAGE OverloadedStrings #-}
module Main where
import qualified Data.ByteString.Char8 as BC8
import System.IO
main :: IO ()
main = mapM_ (BC8.hPutStrLn stdout) (repeat "y")
この結果は以下のとおりです。
$ .stack-work/dist/x86_64-osx/Cabal-1.24.2.0/build/yes/yes | pv -r > /dev/null
[23.7MiB/s]
まったく変わりませんでした。
更に低レベル I/O
hPutStrLn
ではなくhPut
を使用してみます。
{-# LANGUAGE OverloadedStrings #-}
module Main where
import qualified Data.ByteString.Char8 as BC8
import System.IO
main :: IO ()
main = mapM_ (BC8.hPut stdout) (repeat "y\n")
この結果は以下のとおりです。
$ .stack-work/dist/x86_64-osx/Cabal-1.24.2.0/build/yes/yes | pv -r > /dev/null
[30.4MiB/s]
スループットが上がりましたが、組み込みの yes
にはまだ届きません。
残念ながらここでチューニングのネタ切れです。こうするといいよ、という提案があれば是非ご教示ください。
ソースコードは Github の以下にあります。
おまけ: C 言語だとどうなの?
C 言語で最も簡単に実装したのが以下です。
#include <stdio.h>
int main() {
for (;;) {
puts("y");
}
return 0;
}
これを gcc -O3 -o yes main.c
でビルドして実行した結果は以下のとおりです。
$ ./yes | pv -r > /dev/null
[40.7MiB/s]
さすがは C 言語、組み込みの yes
と同じスループットが出ています。
おまけ: 様々な yes
こちら yes/yes.hs at master · mubaris/yes で様々な言語で yes
が実装されています。
Haskell は以下のように実装されており、なるほど yes
とはこういう仕様だったんだ、とわかりました。
mport System.Environment
import System.Exit
main = getArgs >>= parse
parse :: [String] -> IO b
parse [] = putStrLn "y" >> parse []
parse (a:_) = putStrLn a >> parse [a]