Haskell

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

皆さんは 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]