Posted at

Haskell で yes コマンドを実装した

More than 1 year has passed since last update.

皆さんは yes コマンドをご存知でしょうか。私は知りませんでした、以下の記事を読むまでは。

上記記事は 10 分程度で読めて、かつとてもおもしろいので是非読んで欲しいのですが、忙しい諸兄姉のために 3 行でまとめます。



  • yes という y を標準出力するだけのコマンドがあるよ

  • 1979 年から現在まで活発に開発が続けられているよ

  • Rust で実装したらめっちゃ速くなったよ

これを読んで、「Haskell ならどれぐらいのスループットが出るんだろう」と思ったので実装してみました。


組み込みの yes

まずは組み込みの yes コマンドが Mac Book Pro 2015 年モデルでどれ位のものなのか見てみます。

$ yes | pv -r > /dev/null

[40.6MiB/s]

なるほど。


普通の Haskell 実装

では普通に Haskell で実装した場合どれぐらいでしょうか。


Main.hs

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 を使用します。


Main.hs

{-# 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 を使用してみます。


Main.hs

{-# 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]

若干スループットが上がったようですが、誤差の範囲です。


再起をやめる

戦略を変更し再起をやめてみます。


Main.hs

{-# 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 を使用してみます。


Main.hs

{-# 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 言語で最も簡単に実装したのが以下です。


main.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 とはこういう仕様だったんだ、とわかりました。


yes.hs

mport System.Environment

import System.Exit

main = getArgs >>= parse

parse :: [String] -> IO b
parse [] = putStrLn "y" >> parse []
parse (a:_) = putStrLn a >> parse [a]