LoginSignup
7
2

More than 5 years have passed since last update.

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

Posted at

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
2