大きなアライメントのメモリブロックを確保する方法

大きなアライメント(数百kbyte, 数Mbyteレベル)のブロックを確保する方法に困りました。

  • malloc()で大きめに確保して、無駄な部分を切り詰めるやり方 => 「無駄な部分」のサイズが大きすぎる。
  • posix_memalign() => これ確保したメモリはfree()できるので、余分に管理用の領域が確保されている。よって、ブロックの間に必ず隙間ができる。

後者に関しては

posix_memalign(&p, 1 << 20, 1 << 20);

を繰り返し実行してみると、

0xb7c00000
0xb7a00000
0xb7800000
0xb7600000
0xb7400000
0xb7200000
0xb7000000
0xb6e00000
0xb6c00000
0xb6a00000

というように、1Mバイト毎に確保したのに2Mバイト毎に配置されるという事になってしまいます。間の部分はもっと小さいメモリ要求の際に利用できるわけですが...。


で、GHCRTSのブロックアロケータで見つけたのが下の方法。

  1. 初回はブロックの2倍サイズでmmap()し、はみ出た部分をmunmap().
  2. 2回目以降は前回確保したブロックの次のアドレスをヒントとしてmmap()を実行。
    1. これがうまく行く確率が高いらしい。
    2. うまく行かなかったら1の方法でやりなおす。

mmap()は確保したメモリに管理用の領域を付与しないので、隙間を開けずに並べる事ができるということです。2の手順は高速化の為ですが、これがどの程度当たるかはよくわからないです。

この部分を抜き出した簡単なコード。(エラー処理などは省いてます)

#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>

static void *next_addr = NULL;

#define BLOCK_SIZE  (1 << 20)

static void *
alloc_block(void)
{
    size_t size, slop;
    void *ret;
    
    fprintf(stderr, "alloc_block called\n");
    size = 2 * BLOCK_SIZE;
    ret = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);

    slop = (unsigned long) ret & (BLOCK_SIZE - 1);
    munmap(ret, BLOCK_SIZE - slop);
    if (slop > 0)
        munmap(ret + size - slop, slop);
    ret += BLOCK_SIZE - slop;
    return ret;
}

int main()
{
    int i;
    for (i = 0; i < 10; ++i)
    {
        void *p;
        if (next_addr)
        {
            /* 2回め以降はアドレス直指定に挑戦 */
            p = mmap(next_addr, BLOCK_SIZE, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
            if ( (unsigned long)p & (BLOCK_SIZE - 1) )
            {
                /* だめだったらunmapして、真面目にやり直す */
                munmap(p, BLOCK_SIZE);
                p = alloc_block();
            }
        }
        else
            /* 初回は真面目に */
            p = alloc_block();

        printf("%p\n", p);
        next_addr = p;
    }
    return 0;
}

実行してみると

alloc_block called
0xb7c00000
0xb7b00000
0xb7a00000
0xb7900000
0xb7800000
0xb7700000
0xb7600000
0xb7500000
0xb7400000
0xb7300000

と確かに、隙間を開けずにブロックを確保できました。

構文木実装のいろいろ

忙しさにかまけてここの存在をしばらく忘れてしまっていました。
ここんとこ色々なコンパイラを読んだり書いたりしているのですが、構文木の実装にもいろいろなやり方がありおもしろかったので書いときます。読んだ処理系にだいぶ偏りがありますがご了承ください。

バリアントを使う

関数型言語で言語処理系を作るならやはりバリアントでしょう。
例えばocamlならば

type expression =
  { pexp_desc: expression_desc;
    pexp_loc: Location.t }

and expression_desc =
    Pexp_ident of Longident.t
  | Pexp_constant of constant
  | Pexp_let of rec_flag * (pattern * expression) list * expression
  | Pexp_function of label * expression option * (pattern * expression) list
...

これは式についての定義ですが、これが宣言やパターンや型などそれぞれについて定義されています。
また、同様にGHCならば

data Expr b	-- "b" for the type of binders, 
  = Var	  Id
  | Lit   Literal
  | App   (Expr b) (Arg b)		-- See Note [CoreSyn let/app invariant]
  | Lam   b (Expr b)
  | Let   (Bind b) (Expr b)		-- See [CoreSyn let/app invariant],
					-- and [CoreSyn letrec invariant]
  | Case  (Expr b) b Type [Alt b]  	-- Binder gets bound to value of scrutinee
					-- See Note [CoreSyn case invariants]
  | Cast  (Expr b) Coercion

のような感じになっています。

バリアントを使う事の意義は型を厳格につけられる、形式的な文法表現とコードが綺麗に対応する等でしょうか。
わかりやすさという点ではダントツだと思うので、言語処理系製作の入門としては適しているんじゃないかと思います。


厳格な型づけを嫌う人には構文要素の種類、中間言語の種類毎に型の定義とそれを操作する為の関数をかかなければならないというあたりが面倒くさいかもしれません。
また、これをC言語のstructやunionで真似はしないほうがいいです。なぜならパターンマッチがない言語で、ここまで階層化してしまうと

expr->function->body->loop->cond

みたいなアローの連鎖や2重3重にネストしたswitchやifだらけに陥りやすいからです。

継承を使う

オブジェクト指向言語では継承を使うという手があります。いわゆるCompositeパターンというやつです。
例えば、http://www.coins-project.org/では

/**
 *  If-statement.
**/
public interface
IfStmt extends Stmt
{
  ..
}

というようにインターフェースの継承により階層関係を表現しています。これとVisitorパターン等を組み合わせるとなかなかいい感じです。
高レベル中間言語での操作は木を辿りながらノードを書き換えていくというのが基本になりますから、Visitorパターンと仮想関数の組み合わせでいろいろできます。

struct,unionを使う

Cではパターンマッチなどの便利機能がないので安直に構文木を作ってしまうと実装が大変になります。
また、C固有の問題としてはメモリ等のリソース管理の手間が挙げられます。構造体の種類が多いと管理が大変になってしまいます。例えばGCCではコンパイラ内部でガベージコレクタが走っていますが、これを実装する上ではヒープ上のあらゆるものを一つの構造体として管理した方が楽になります。


GCCのtree.h(プリプロセッサで処理したもの)を読んでみると以下のように、定数も識別子も宣言も型もすべてのものが一つのunionに属しています。

union tree_node

{
  struct tree_base base;
  struct tree_common common;
  struct tree_int_cst int_cst;
  struct tree_real_cst real_cst;
  struct tree_fixed_cst fixed_cst;
  struct tree_vector vector;
  struct tree_string string;
  struct tree_complex complex;
  struct tree_identifier identifier;
  struct tree_decl_minimal decl_minimal;
  struct tree_decl_common decl_common;
...

また、unionの各フィールドを識別する為にはタグを埋め込む訳ですが、これは以下のようになっています。

enum tree_code {
PLUS_EXPR,
MINUS_EXPR,
MULT_EXPR,
POINTER_PLUS_EXPR,
TRUNC_DIV_EXPR,
CEIL_DIV_EXPR,
FLOOR_DIV_EXPR,

...

バリアントや継承を使う方法の場合には、通常「式」を表す型があってその中にオペレーションの種類を表すデータを格納する訳ですが、ここではオペレーションの情報をタグの中に直接埋め込んでいます。こうすれば一回のテストで「どういう種類の」式かまで判別できるので、パターンマッチのコストが減ります。階層構造を表現する為の情報は別に構造体中に埋め込んでやります。

enum tree_code_class {
  tcc_exceptional, /* An exceptional code (fits no category).  */
  tcc_constant,    /* A constant.  */
  /* Order of tcc_type and tcc_declaration is important.  */
  tcc_type,        /* A type object code.  */
};

S式

HugsはS式を利用した(といっていいかどうか分かりませんが)実装になっています。

これはHugsyaccのコードですが、

infixExpa : infixExpa qop '-' exp10a	{$$ = gc4(ap(NEG,ap(ap($2,$1),$4)));}
	  | infixExpa qop exp10a	{$$ = gc3(ap(ap($2,$1),$3));}
	  | '-' exp10a			{$$ = gc2(ap(NEG,only($2)));}
	  | exp10a qop '-' exp10a	{$$ = gc4(ap(NEG,
						     ap(ap($2,only($1)),$4)));}
	  | exp10a qop exp10a		{$$ = gc3(ap(ap($2,only($1)),$3));}

/* apやonlyの定義 */
#define ap(f,x)      pair(f,x)
#define only(t)			 ap(ONLY,t)

つまり

-x => (NEG (ONLY x))
a + b => ((PLUS (ONLY a)) b)

のようにS式を利用して構文木を定義しています。

CPU実験まとめ。

昨日CPU実験の発表会がありました。私たちのチーム「地下詰妖精」はコンパイラコード18.407秒、ハンドコード17.085秒で歴代の記録を更新し優勝しました。

私はコンパイラ係を担当しHaskellでML言語のコンパイラアセンブラの製作をしました。
この半年は一ヶ月以上家に帰らない月があったりと、非常に充実(?)したものでした。

来年からCPU実験の内容が変わるので、最後に何か面白いことをやりたいという班結成当初の目標の一つ目は達成できたのではないかと思います。
多分この班が初めて行った事。

  • 実験基盤でゲームを作る
    • テトリスを作成
    • 実機のkoiken君, プログラムのmagicant君, グラフィックのarc君のスキルが見事に合わさった。動いた時は感動した。
    • 自分はminCamlを拡張した言語(minCaml++)のコンパイラを作った。
  • レイトレーサのコードの徹底研究
    • uemura君
    • minCamlのコードをC言語で書き直し、コードの解析をした。
    • 今まで誰も見つけなかった、レイトレーサのコードのバグを見つけたり、最適化に関するアドバイスが的確だったり彼の仕事はすごかった。
  • CADソフトの作成
    • arc君
    • コンテストで使用するSLDファイルをリアルタイムでレンダリングしたり、編集したり、検査したりする機能があるソフト。uemura君のC版エンジン搭載。

この実験をしてて一番良かったのは学科のすごい人達と共に作業が出来たという事でした。
単位と全く関係のないことにも本気を出せる、プログラミング好きなメンバーが集まったすごく良い班でした。

個人的にはソフトウェア係のuemura君にものすごく感謝しています。彼のデバッグ技術と最適化のアドバイスに助けられてばかりでした。
あと、arc君のプレゼンに対する熱意はすごいと思いました。泊まり込んでプレゼン資料のロゴとかイラストを描いたりしてくれました。


個人的にはこの実験で

  • とにかくコンパイラの勉強になった
    • コンパイラの全コードをゼロから書いた(一万五千行ほど)。苦行だったが、これほど勉強になることはない。
    • たくさんコンパイラのコードを読んだ。GHC(これは趣味で), ocamlopt, SML#, SML/NJ, COINSプロジェクト
    • コンパイラ関係の本、論文を沢山読んだ。
  • Haskellの勉強になった。

という収穫がありました。

目標の二つめは10秒切りなのですが、それが達成できなかったのはとにもかくにもコンパイラ係である私の未熟さ故で、班員のみんなには申し訳ない気持ちです。

去年7月頃からHaskellの勉強を初め、すらすらコードを書けるようになるまで4〜5ヶ月かかり、並行してコンパイラの勉強も進め、結局初めて本番用のコンパイラコードが動作したのが1月になってから。
それから各種最適化を急いで実装するもまだアイデアがある段階で期限切れになってしまうという残念な結果になってしまいました。

総インストラクション数は14億4,5千万くらいだったかと思います。過去のコンパイラ係の資料とか見てみると12億切っていたりするところもあるのでしょぼいです...orz

コンパイラがやっている最適化等に関してはいずれ資料をアップするので良いとして、Haskellコンパイラを書いてみての感想です。

  • スラスラコードを書けるようになるまでが長い
    • 初心者にやさしくない言語
    • 副作用がないのでレジスタ割り当てとか地獄
    • Haskellに関するネット上の文献、書籍が少ない
    • インストールが難しい
  • 実行時の消費メモリが膨大
    • 予想はしていたけれど想像を越えていた。使用メモリが増えていくと突然実行時間が急激に低下する。
    • 終了間際にかなり頑張って実行時プロファイル情報を利用した大域的部分冗長性の除去を実装したら実行にン時間かかりコンテストまでにコンパイルが終わらないという事態に。これが高速に動けば上の記録も変わっていたはず。部分冗長性除去なしだとコンパイル時間は10秒〜30秒くらい。
    • テーブルにIOArrayを使用したり正格性の指定とか色々気を使ったけれどもなかなか改善しなかった。
      • 原因としては考えられる事
      • モナド変換子の使いすぎ。コンパイラモナドの上に各最適化用のモナドを乗せるという構成。最大で3段。
      • テーブルの使いすぎ。副作用が無いので、テーブルを作成しIDを使って参照するというポインタもどきな事をしたのだけれど、これが多分問題。データを読み書きするためにいちいちIO層までliftするという多段の間接参照が発生している。実はIOArrayを使わずに、最上層にMapを用意した方が速いという場面もあった。
      • 今後研究したい
  • 一部のアルゴリズムの記述が非常に簡単になる
    • 遅延評価があるため、直感的に書いたコードがなかなか高速に動く。仮想機械語以前の部分はまったく何も工夫しなくても高速。
    • コードの再利用が出来る。
      • 例えばocamloptの実装では「自由変数の集合を求める関数・ある変数が自由変数として出現するか調べる関数」、が別々の関数になっている。
      • 遅延評価言語で書く場合には不必要な計算はされないので、前者だけあれば問題無い。
      • 構文木の探索をしたかったら、構文木に含まれる式をリストにする関数を一つ用意すれば足りる。
    • やはり副作用バリバリのアルゴリズムの記述は難しい。

来年のコンパイラ係へ

  • とくにかく早い段階で動くコンパイラを作った方が良いです。私はminCamlを改造したもの、ocamloptを改造したものを作成した後に、Haskellでのコンパイラを作成しました。
    • 本番用コンパイラを別に作るという場合にも、ここでの経験が助けになると思います。
  • 0から書いた方が絶対に勉強になります。
    • 既存のコードに修正を加えるのと、丸写しでも自分で書くのでは全然違うと思います。
  • 良いハードウェアを生かすも殺すもコンパイラ次第です。来年はハードウェアが豪華になるらしいので、ハードウェアに並列化などの機能が搭載されるようになるかと思います。それらをどれだけ生かせるかはコンパイラ係に懸かっていると思います。
    • ハードウェアが自分でスケジューリングとかするようになってしまったら、コンパイラの仕事がなくなってしまうかもという話もありますが(笑)
  • 折角コンパイラを作るのであれば、何か一つは高度な最適化に挑戦してもらいたいです。データフロー方程式を解く大域的な物とか。骨が折れますが、やっぱり最適化の効果が高く、おもしろく、勉強になると思います。
  • オープンソースコンパイラコードで参考になるもの
    • ocamloptはあまり参考にしない方が良いです。実は大した最適化があまり行われていません。また、仮想機械語以後が独自の拡張を追加しにくいような形になっています。C--の最適化部とかレジスタ割り当てとかはとても参考になります。
    • SML New JerseyのFLITというとこのコードが読みやすくて参考になると思います。Loop unrollingとかCode Hoistingとかいろいろ揃ってます。CPS変換とかも。
    • COINSというプロジェクトがあるんですが、中間機械語以降が参考になるかもしれません。自分はデータフローの解析とかでちょっと読みました。先進的な最適化をしたくなったら、ここを読むしかないです。
  • 大学の課題をちゃんとやりましょう。
    • 好きな事ばかりやりすぎて、大学の課題を何もやってなかったので単位を落としまくりました。
    • CPU実験が終わって気が抜けて現実に戻った後に鬱になります。

CPU実験は終わっちゃいましたが、来年までにコンパイラコードで9秒を切るという使命が出来たので延長戦をすることにしました。
まず、試行錯誤しながら書いたためぐちゃぐちゃになったコードの整理と高速化(実装言語を変えるかも)をして、やりたかったけれども間に合わなかった大域的なスケジューリング・ポインタ解析(をして間接参照をなくす)、既存最適化の見直し、この班のハードに特化した最適化とかをしていこうと思います。

それからコンパイラHaskellに関してこの半年に勉強したことを、ここに少しずつ書いていこうか思います。
Haskellプログラムの高速化に関してもいろいろと実験とかしたいと思います。

自動微分モジュールで遊ぶ

HackageDBでnumbersというパッケージを見つけました。この中にData.Number.Difという自動微分のモジュールが含まれていたのですが、これがなかなか面白いです。
サンプルコードを全然見かけないので、いろいろと書いてみます。

自動微分とは

自動微分というのは関数の導関数の値を自動的に求めるということですが、これには「数値的」にやる方法と「記号的」にやる方法があります。数値的にというのは
f'(x) = \frac{f(x + h) - f(x)}{h}
という式のhに適当な微小量を与えて、近似的に導関数の値を求める方法を言います。
一方記号的にというのは
(\sin x)' = \cos x
の様に、微分公式を使用して具体的に関数を求める方法を言います。

Difモジュールが行うのは後者ですが、Haskellの遅延評価と多相型を生かして非常に使い勝手の良いものになっています。プリプロセッサを通すとかいった手間もありません。

自動微分アルゴリズムについてはAutomatic differentiationを参照して下さい。

簡単な例

DifモジュールではHaskellの数値型の多相性を利用して、通常どおりに定義した関数をそのまま微分できるようにしています。
通常どおりというのは、数式に限らずif文を利用した関数なども含みます。

> :m +Data.Number.Dif
> let f(x) = x^2 + 3*x + 2
> f 2 
12.0
> deriv f 2
7.0
> deriv (deriv f) 2
2.0

上では (x^2+3x+2)' = 2x+3微分をderivという関数によって行っています。
最初のf 2の部分は通常の関数と通常の数値ですが、deriv f 2の引数のfは微分を計算するために、Dif a -> Dif aという特殊な型になっています。
ここで、Dif aという型がNumクラスやFloating等のすべての数値クラスに属しているので、通常の式と全く同じく記述することができます。Haskellでは数値リテラルも多相的に振る舞ってくれます。
また、sinなどの無限回微分出来る関数も遅延評価を利用して自然に表現されています。

最後の式は二階微分の計算の例です。*1

応用編1

ニュートン法です。

import Data.Number.Dif

newton f init eps =
  let f' = deriv f
      next = init - (unDif f init) / (f' init)
  in if abs (next - init) < eps
    then init
    else newton f next eps

関数fと初期値、許容誤差を与えるとf(x) = 0の解の近似値が求まります。
*2

> let e = 1.0e-15
> newton (\x -> x**2 - 2) 1 e
1.4142135623730951
> newton (\x -> x**2 - 3) 1 e
1.7320508075688772
> newton sin 3 e
3.141592653589793
> newton (\x -> log x - 1) 2 e
2.7182818284590455

応用編2

Gnuplotと連携させてグラフを書いてみます。例として導関数と接線を書いてみます。f(x) = ...の部分を書き換えればいろいろな関数の微分が試せます。

import Data.Number.Dif
import Data.List
import System.IO
import System.Cmd

-- x=aのまわりでの一次近似 (接線)
tangent f a =
  \x -> (deriv f a) * (x - a) + (unDif f a)

main = 
  let 
    -- 変域設定
    xmin = -2*pi
    xmax = 2*pi
    step = 0.1

    -- 表示する関数の用意
    f(x) = sin x
    funs = [f, deriv f, tangent f 1.0, tangent f 2.0]
  in 

  let 
    range  = [xmin, xmin+step .. xmax]
    graphs = [map f range| f <- funs]
    datas  = transpose (range : graphs)
  in

  do

  -- データの用意
  (datafile, hd) <- openTempFile "/tmp" "diff-data"
  mapM (hPutStrLn hd) $
    [concat . intersperse "\t" . map show $ d | d <- datas]
  hFlush hd

  -- バッチファイルの用意
  (cmdfile, hc) <- openTempFile "/tmp" "diff-command"

  hPutStrLn hc"set xzeroaxis"
  hPutStrLn hc"set yzeroaxis"
  hPutStrLn hc $ "plot " ++
    ( concat 
    . intersperse "," 
    . map (\n -> show datafile ++ " using 1:"++show n++" w l")
    ) [2..length graphs + 1]
  hPutStrLn hc "pause -1"
  hFlush hc

  system $ "gnuplot " ++ cmdfile

*1:こうするとDif型の入れ子が発生するので、( val . df . df . f . dVar) 2と書いた方がいいかもしれません

*2:unDifというのはDif a -> Dif a型の関数をa -> a型の関数に戻す為のものです。これが必要になるのは、関数の引数として与えられたfが二通りの型を持てないという事が原因です。関数の引数としてでなく、letなどで定義した場合にはunDifは必要ありません。

ocamloptを読んだ

ocamloptを読んでみた。そして色々と改造してみたりして分かった事をメモしておきます。

  1. 整数値、アドレスのボックス化は2倍して1足すという方法で行っている。
    • 例えばprint_int 100と書くと実際にはprint_int 201という呼び出しになる。
    • 四則演算などは、この変換を行った状態で実行される。例えばたし算の場合は、足した後に1を引くという計算になる。
    • これはRubyとかでやっているのと同じ方法
  2. どんなに小さい関数でもlet rec ...と定義するとインライン化されない。
    • let recと定義された関数が再帰関数であるかどうかのチェックはしていない。
  3. [| ... |] と定義された配列と、Array.createで生成された配列は実体が異なる。
    • 特に[| 1.0; 2.0; ..|]のような配列は、メモリ上に直接浮動小数点数が配置されるが、Array.create 10 1.0の様に書くと、ポインタを介するアクセスになる模様。
  4. 高度な最適化といえるものはほとんど行われていない。
    • 関数型言語であることを生かした最適化と言うと、整数、浮動小数、ポインタに関するものくらい。
    • あとはリファレンスを通常の変数にする最適化をしている。
    • 正直なところ、OCamlでは高階関数などを多用して関数型言語らしいプログラムを書くより、リファレンスを多用した方が多くの場合に早いコードができそうな気がする。
  5. OCamlの例外は軽い。
    • 例外というと重いというイメージがあるけれど、OCamlの場合にはtry .. withという限定された構文でしか使えないので軽く実装できる。
    • forループからbreakで抜けるくらいの気軽さで使える。

ocamloptのコードについて。

  1. アーキテクチャ毎の定義ファイルの設計が美しいと思った。
    • スケジューラや命令選択等の一般的な定義を記述したスーパークラスを作成しておき、各アーキテクチャ毎のコードでそれを継承して、サブクラスを作っている。
    • サブクラス内でスーパークラス内のメソッドをオーバーライドするなどして、必要な部分だけ再定義するという書き方になっている。
    • OCamlオブジェクト指向については良く知らないのだけれど、inheritを使っているのをはじめてみた。
    • ところでi386向けのスケジューリング定義ファイルの中身は「何もしない」になっていた。i386の場合にはハードウェア任せにしてしまった方が十分速いのだとか。
    • ちなみにGHCは一つのファイルに#if i386_TARGET_ARCHみたいにプリプロセッサディレクティブを使ってごちゃっと書いている。
  2. 入れ子になったパターンマッチをじゃんじゃん使っている。
    • こういうコードを書いてもそこそこ速く動いてしまうというのがうらやましい。
    • ボックス化された整数くらいCboxed_...みたいに表して欲しい。正直読みにくい。
    Cconst_int n -> Cconst_int(n asr 1)
  | Cop(Caddi, [Cop(Clsl, [c; Cconst_int 1]); Cconst_int 1]) -> c
  | Cop(Cor, [Cop(Casr, [c; Cconst_int n]); Cconst_int 1])
    when n > 0 && n < size_int * 8 ->
      Cop(Casr, [c; Cconst_int (n+1)])
  | Cop(Cor, [Cop(Clsr, [c; Cconst_int n]); Cconst_int 1])
    when n > 0 && n < size_int * 8 ->
      Cop(Clsr, [c; Cconst_int (n+1)])
  | Cop(Cor, [c; Cconst_int 1]) -> Cop(Casr, [c; Cconst_int 1])
  | c -> Cop(Casr, [c; Cconst_int 1])
  1. 副作用を使いまくっている。
    • 例えばC--の内部表現は命令がポインタで連結された形で表現されている。スケジューリング等はこのポインタの張替えとして実装されている。


以前GHCを読んだ訳ですが、言語が違うと(人が違うと?)ここまでコードが変わるんだなぁという。
OCaml関数型言語であることを生かしてガンガン最適化をするというような言語ではなく、言語設計からして高速化を意図して作られているのだろうなぁ。

ByteStringでwcを書く

Haskellで速いwcを書いてみようという記事を読んだのですが、Haskellの文字列表現は他言語に比べ非常に効率が悪いわけで、それをつかってそこそこ速くしようとするとコードが汚くなってしまわざるを得ないという感じを受けました。

そこでByteStringで書いてみるとどうなるかという実験。

import qualified Data.ByteString as B
import Data.ByteString.Base

main = do
    text <- B.getContents

    -- lines
    putStr . show . length . B.split (c2w '\n') $ text
    putChar ' '

    -- words
    putStr . show . length. B.splitWith isSpaceWord8 $ text
    putChar ' '

    -- characters
    putStrLn . show . B.length $ text

textを三回使いまわし,別々に調べるという全くもって工夫の無いコードですが、Haskellで速いwcを書いてみようでも使っているFreeBSDのhandbook(3.8Mバイト)を入手して測定したところ

% ghc --make -O -fglasgow-exts wc.hs 
% time ./wc < book.txt                                                                                              
84709 654559 3848348
./wc < book.txt  0.19s user 0.02s system 97% cpu 0.214 total

0.19sと十分速いです。メモリ使用量も1Mで,非常に少なかったです。
ちなみに-Oと-fglasgow-extsなしだと1.67sでした。


Haskellで効率の良いプログラムを書きたいときには

  • ライブラリで用意されている関数を積極的に使う
    • 例えば、ByteStringモジュールでは複数のループを一つのループにするループ融合という最適化などが行われているのですが、再帰を使って自前でループを書くとこういった最適化が阻害されてしまいます。(今回のプログラムではほとんど関係ないですが)
  • データ構造を適切に選ぶ
    • 大量のデータを扱う場合はByteStringやArrayなどの方が適している場合がある
  • 最適化オプション-O -fglasgow-extsをつける。

あたりに注意するだけで,大分違ってくると思います。

Haskellでのエラー処理

なんでもセミナーで質問があった手前、GHCガベージコレクションの事とか調べようと思っていたのだけれど、このところ大学の実験で余裕がないので、しばらく実験中に気づいた事とか書いてこうと思います。

Haskellでのエラー処理の難しさ

Haskellで手軽に使えるエラー処理としてはerror関数を使う方法がありますが、例えば以下のコードの場合

parse '1' = 1
parse '2' = 2
parse '3' = 3
parse x   = error $ "invalid input : "  ++ show x

main = do
    input <- getContents

    putStrLn "parsing ... "
    nums <- return $ map parse input

    putStrLn "add numbers ... "
    result <- return $ sum nums

    putStr $ "result : " ++ show result

出力はこうなってしまいます。

% echo "123x123" | ./gen_error                                                                                                       
parsing ... 
add numbers ... 
gen_error: invalid input : 'x'

通常の感覚だとパースの時点でエラーになってほしいわけですが、Haskellは遅延評価なのでerrorが初めて評価されるsumのタイミングでエラーになっています。error関数の実体はErrorCallという例外の送出なわけですが、安易に多用するとおかしなことになります。

エラーをうまく扱うには

Haskellでエラーを扱う場合に問題になることには主に
1. エラー情報の扱い
1. エラー発生のタイミング
1. I/O処理
の3つが挙げられると思いますが、大きなプログラムでこれらを上手に扱う為には多くの場合

エラーを収集して、例外を飛ばす

という方法をとることなると思います。

エラーを収集というのは、エラー情報に適切に型を与え、特定の一箇所で確実に評価をしてやるということです。
例外を送出する関数は、戻り値が任意の型へ変換されるのが普通なので、他の値に紛れてしまい情報の正しい伝達と評価タイミングの操作が困難になります。
処理が一段落するポイントで確実にエラーを評価した上で、main関数に向かって例外としてthrowしてやれば、上の3つの問題にうまく対処することができます。

もちろん、プログラムの規模によっては例外を使わない方が安全だと思います。

エラーの収集

まず、エラーを起こしうる関数は中で例外を飛ばすのではなく、EitherやMaybeなどを使って戻り値としてエラーを返してやるようにします。
例えば

if (エラー発生)
  then Left "ERROR"
  else Right x

という感じで、Leftにエラーメッセージを入れて返すなどといった事をします。
後でパターンマッチによって中身を取り出す分けですが、その時にはLeftかRightかが分かるまでしか評価されないので、中身の値の評価は上手く遅延されてくれます。
データの持ち運びにはモナドを利用する事が多いと思いますが、自分で定義する以外にControl.Monad.Errorを使うという選択肢もあります。

import Control.Monad.Error

f x | x >= 0    = Right x
    | otherwise = Left "ERROR: negative number"

g x | x `mod` 2 == 0 = Right x
    | otherwise      = Left "ERROR: odd number"

-- Eitherをモナドとして扱える。
check x = do
    a <- f x
    b <- g a
    return b

main = 
    case check 3 of
         Right x    -> putStrLn "OK"
         Left msg   -> putStrLn msg

例外の送出

例外を送出する場合にはthrow系の関数を使います。とりあえず今回はthrowDynの紹介。
throwDynはユーザ定義型をDynamic型にして例外として送出する為の関数です。Typeableなものを何でも投げられます。

{-# OPTIONS_GHC -fglasgow-exts #-}
import Data.Typeable

data MyException 
    = ExceptionA String
    | ExceptionB ...
    | ...
    deriving (Show, Typeable)

投げるときは

throwDyn $ ExceptionA "Error"

の様にします。

捕捉する時にはcatchDynを使います。

main = do
    ......
    `catchDyn`
    (\e ->   ...
              case e::MyException of  -- Dynamicなので型の指定が必要
                   ExceptionA msg   ->  ....
                   ExceptionB ....
   )

例外はいつ飛んでくるのか分からないので、捕捉する側でしっかり配慮するべきです。他の言語に当てはまる注意がHaskellでも当てはまります。
特に、ファイルI/Oの最中などに例外に割り込まれると大変なので、block/unblock/bracket/finallyなど例外をブロックする関数が用意されています。使い方については、ソースコード中に丁寧に例が書いてあるので、そちらを参照してください。