7.2. 非ボックス化型とプリミティブ演算

GHCは大量のプリミティブなデータ型と演算を基礎としている。(「プリミティブ」だというのは、Haskell自体では定義できないという意味である)。高速なコードを書くためにこれらを使うこともできるが、高水準の言語機能やライブラリを使った方が、通常はずっと苦痛が少なく、長期的に見て良い結果になる。運が良ければ、書いたコードは最適化されて、結局、効率的な非ボックス化版になる。もしそうならないときは知らせてほしい。

これらのプリミティブなデータ型と演算は全てGHC.Primというライブラリからエクスポートされている。これには詳細なオンライン説明書がある。(この説明書はcompiler/prelude/primops.txt.ppというファイルから生成されている)

プログラム中でプリミティブなデータ型や演算に言及したいなら、まずGHC.Primをインポートしてそれらをスコープに導入しなければならない。多くは「#」で終わる名前を持っているので、そのようなものに言及するには拡張-XMagicHash(7.3.2. 魔法の井桁(magic hash))が必要である。

プリミティブ演算は広範にわたって非ボックス化型非ボックス化タプルを使っている。ここではこれらについて短くまとめる。

7.2.1. 非ボックス化型

GHCにおいて、大部分の型はボックス化されている。つまり、その型の値はヒープ中オブジェクトへのポインタで表現されている。例えば、HaskellのIntの表現は、二ワードのヒープ中オブジェクトである。一方、非ボックス化型は、値そのものによって表現され、ポインタやヒープ確保とは無関係である。

非ボックス化型は、Cで使う「生の機械上の」型に相当する。Int# (long int)、Double# (double)、Addr# (void *)などである。これらの型の上のプリミティブ演算(PrimOp)は予想される通りのものである。例えば、(+#)Int#についての加算であり、我々が皆愛する機械上の加算(通常一命令)である。

プリミティブな(非ボックス化)型はHaskellで定義できないので、言語とコンパイラに組み込まれている。プリミティブ型は決して持ち上げられていない。すなわち、プリミティブ型の値はボトムになり得ない。慣習として、プリミティブな型、値、演算には#を接尾辞として付ける(ただし、あくまで慣習である)(7.3.2. 魔法の井桁(magic hash)を見よ)。プリミティブ型には、リテラルのために特別な構文があるものもあるが、それも同じ節で述べている。

しばしば、プリミティブな型は単なるビットパターンで表される。例えば、Int#Float#Double#がそうである。しかし、常にそうという訳ではない。プリミティブな値がヒープに確保されたオブジェクトへのポインタで表現されていることもある。例としてArray#、すなわちプリミティブ配列の型がある。プリミティブ配列はヒープに確保されているが、これは、大きすぎてレジスタに収まらず、コピーして持ち回るのはコストが掛かりすぎるからである。ある意味では、これがポインタで表現されているというのは偶然に過ぎない。プリミティブ型がポインタで表現されるときは、ポインタは必ずその値を指すのであって、未評価のサンクや間接参照等々を指すことはない。たくさんの数値計算を行うプログラムでは、非ボックス化型を使うことで「標準的な」書き方をしたものよりもずっと速くなることがある。ある例では、三倍もの高速化が達成された。

プリミティブ型を使うにあたって、いくつか制限がある。

  • もっとも基本的な制限は、プリミティブ値は、多相的な関数に渡したり多相的なデータ型中に保持したりできないということである。これによって[Int#](つまり、プリミティブ整数のリスト)のようなものが禁止される。この制限があるのは、多相的な引数や多相的な構築子フィールドはポインタであると期待されているからである。これらに非ボックス化整数が入り込むと、GCの追跡対象となり、予測できないスペースリークを引き起こす。また、その多相的な要素に対してseqを行うと、ポインタとして参照外しを試みることになり、壊滅的な結果を招く。さらに悪いことに、非ボックス化型はポインタよりも大きいことがある。(Double#が一例)

  • newtypeを定義するとき、その表現の型(データ構築子の引数の型)が非ボックス化型であってはならない。よって、以下は許されない。

      newtype A = MkA Int#
    

  • 最上位の束縛で、非ボックス化型を持つ変数を束縛することはできない。

  • 再帰的な束縛で、非ボックス化型を持つ変数を束縛することはできない。

  • 非ボックス化変数を(非再帰的、非最上位)パターン束縛で束縛することはできるが、そのようなパターン照合はすべて正格にしなければならない。例えば、次のように書くことはできない。

      data Foo = Foo Int Int#
    
      f x = let (Foo a b, w) = ..rhs.. in ..body..
    

    bの型はInt#なので、代わりに、次のように書かなければならない。

      data Foo = Foo Int Int#
    
      f x = let !(Foo a b, w) = ..rhs.. in ..body..
    

7.2.2. 非ボックス化タプル

非ボックス化タプルはGHC.Extsでエクスポートされている訳ではない。これは言語フラグ-XUnboxedTuplesによって有効になる構文的拡張である。非ボックス化タプルは次のような形をしている。

(# e_1, ..., e_n #)

ここで、e_1..e_nは任意の型(プリミティブであるかを問わない)をもつ式である。非ボックス化タプルの型も同様に書く。

非ボックス化タプルが使われるのは、関数が複数の値を返す必要があり、なおかつ、通常のタプルを使ったときのようなヒープ確保を避けたい場合である。非ボックス化タプルが返されるとき、要素がレジスタかスタックに直接置かれる。非ボックス化タプルそれ自体は複合物としての表現を持たない。primops.txt.ppに載っているプリミティブ演算の多くが非ボックス化タプルを返す。特に、IOモナドおよびSTモナドでは、操作の連結に際して不要なメモリ確保をしなくても済むようにするために非ボックス化タプルが使われている。

非ボックス化タプルの使用に際しては、かなり厳しい規則が存在する。

  • 非ボックス化タプル型の値は、通常の非ボックス化型と同じ制約を受ける。すなわち、多相的なデータ構造に入れることはできないし、多相的な関数に渡すこともできない。

  • 非ボックス化タプル型の変数は存在し得ない。構築子や関数の引数が非ボックス化タプル型の引数を取ることはできない。以下の例は全て不正である。

      data Foo = Foo (# Int, Int #)
    
      f :: (# Int, Int #) -> (# Int, Int #)
      f x = x
    
      g :: (# Int, Int #) -> Int
      g (# a,b #) = a
    
      h x = let y = (# x,x #) in ...
    

  • 非ボックス化タプルはネストできない。よってこれは許されない。

    f :: (# Int, (# Int, Int #), Bool #)
    

非ボックス化タプルの典型的な使いかたは、単純に複数の値を返し、その複数の結果をcase式で束縛するというものである。

  f x y = (# x+1, y-1 #)
  g x = case f x x of { (# a, b #) -> a + b }

パターン束縛中に非ボックス化タプルを用いることができる。

  f x = let (# p,q #) = h x in ..body..

pqの型が非ボックス化型でないなら、通常のHaskellのパターン束縛と同様に、この束縛は遅延する。上記の例は次のように脱糖できる。

  f x = let t = case h x o f{ (# p,q #) -> (p,q)
            p = fst t
            q = snd t
        in ..body..

実際、束縛は再帰的であっても良い。