Haskellでは、リストや配列といったデータ構造に対する処理を高速化するために様々な試みが行われています。しかし、残念ながら現在のHaskell処理系やライブラリの実装には性能面でまだまだ課題があります(参考リンク1、参考リンク2)。Cの配列をHaskellで直接利用するようにすれば、性能の向上を期待できます。
Cで書かれたOSやライブラリのAPIには、Cの配列(およびそうした配列を扱うCの関数)を引数に取るものもあります。HaskellのFFIを利用すれば、HaskellからCの配列を利用できます。今回は、その方法を紹介しましょう。
Cの配列を利用する方法
第22回で説明したように、HaskellのFFIではCのポインタをPtr型で表現します。Cではポインタを使って配列を参照するので、Ptr型を使えばHaskellでCの配列を利用できます。
ただし、ポインタを操作するStorableクラスのメソッドで配列の処理を直接記述すると、ソースコードだけでは配列を扱っていることがわかりにくくなってしまいます。そもそも、ポインタで配列を扱うのは記述が面倒です。
そこでbaseパッケージのForeign.Marshal.Arrayモジュールでは、Cの配列を扱うためのラッパー関数が提供されています。
その一つがmallocArray関数です。Cの配列を作成するものです。
-- |Allocate storage for the given number of elements of a storable type -- (like 'Foreign.Marshal.Alloc.malloc', but for multiple elements). -- mallocArray :: Storable a => Int -> IO (Ptr a) mallocArray = doMalloc undefined where doMalloc :: Storable a' => a' -> Int -> IO (Ptr a') doMalloc dummy size = mallocBytes (size * sizeOf dummy)
作成したCの配列からデータを読み出すのがpeekArray関数、データを書き込むのがpokeArray関数です。
-- marshalling -- ----------- -- |Convert an array of given length into a Haskell list. The implementation -- is tail-recursive and so uses constant stack space. -- peekArray :: Storable a => Int -> Ptr a -> IO [a] peekArray size ptr | size <= 0 = return [] | otherwise = f (size-1) [] where f 0 acc = do e <- peekElemOff ptr 0; return (e:acc) f n acc = do e <- peekElemOff ptr n; f (n-1) (e:acc)
-- |Write the list elements consecutive into memory -- pokeArray :: Storable a => Ptr a -> [a] -> IO () #ifndef __GLASGOW_HASKELL__ pokeArray ptr vals = zipWithM_ (pokeElemOff ptr) [0..] vals #else pokeArray ptr vals0 = go vals0 0# where go [] _ = return () go (val:vals) n# = do pokeElemOff ptr (I# n#) val; go vals (n# +# 1#) #endif
peekArrayは、Cの配列から指定した長さのデータを読み取り、Haskellのリストにして返します。pokeArrayは、Haskellのリスト内の要素をCの配列に書き込みます。
型の上では、CのポインタとCの配列の区別はありません。しかし、mallocArray、peekArray、pokeArrayは、いずれも関数名にArrayという接尾辞が付いています。これにより、Cの配列を扱っていることを明示できます。また、これらの関数はいずれもStrorableクラスのメソッドを使って実装されているため、たとえCやHaskellの組み込みの型ではないユーザー定義の型の値であっても、Storableクラスのインスタンスとして定義されている型の値であれば、Cの配列に書き込んだり読み出したりできます。
では、HaskellでCの配列を扱う処理を書いてみましょう。ここでは、外部関数の呼び出しにInt型を直接使うのではなく、fromIntegral関数を使ってInt型とCInt型を相互変換しています。64ビットOSでありながら32ビットのint型を採用しているような環境で問題が起こるのを防ぐためです。
Array.hs:
import Control.Exception ( bracket ) import Foreign.C.Types ( CInt(..) ) import Foreign.Marshal.Alloc ( free ) import Foreign.Marshal.Array import Foreign.Storable ( Storable(..) ) import Foreign.Ptr ( Ptr(..) ) useAsCArray :: Storable a => [a] -> (Ptr a -> IO b) -> IO b useAsCArray list process = bracket -- リストをCの配列に変換 (do carr <- mallocArray len pokeArray carr list -- carr <- newArray list return carr) -- Cの配列を開放 free -- Cの配列を利用して処理 process where len = length list main :: IO () main = do let xs :: [CInt] xs = [0..9] len = length xs ys <- useAsCArray xs $ \carr -> do -- Cの配列を扱う関数を利用 cUpdateArray carr 4 (fromIntegral len) -- 処理の結果を読み込み peekArray len carr print ys foreign import ccall "updateArray" cUpdateArray :: Ptr CInt -> CInt -> CInt -> IO ()
carray.h:
void updateArray ( int*, int, int );
carray.c:
#include "carray.h" void updateArray ( int *xs, int y, int len ) { int i; for (i = 0; i < len; i++) { xs[i] = xs[i] + y; }; }
実行結果:
$ ghc carray.h carray.c Array.hs Linking Array ... $ ./Array [4,5,6,7,8,9,10,11,12,13]
この例では、Cの配列を作成し、その配列を使うCの関数で処理を行い、処理の結果を読み込んで出力しています。注目するべき点は二つです。一つは、useAsCArray関数の内部で使われている「mallocArray関数とpokeArray関数を組み合わせてHaskellのリストをCの配列に変換する処理」、もう一つが、main関数の内部で使われている「peekArray関数でCの配列をHaskellのリストに変換する処理」です。
HaskellのリストとCの配列は異なるデータ構造であり、使われる型や内部表現は異なります。Haskellのリストに対して、Cの配列を扱う関数を使うことはできません。この例ではmallocArrayとpokeArrayを使ってリストをいったんCの配列に変換し、Cの配列に対してCの関数を適用しています。その後、処理を行ったCの配列をpeekArrayでリストに変換し、結果を出力しています。
このように、Haskellのデータ構造に対してCの配列を扱う関数を使うには、Haskellのデータ構造をいったんCの配列に変換する必要があります。ただ、こうした処理をいちいち書くのは面倒です。メモリーの解放し忘れや早すぎるメモリー解放といった問題が生じる可能性もあります。そこでCの配列を扱うためのForeign.Marshal.Arrayモジュールでは、Haskellのデータ構造をCの配列として扱えるようにするラッパー関数を提供しています。
-- |Temporarily store a list of storable values in memory -- (like 'Foreign.Marshal.Utils.with', but for multiple elements). -- withArray :: Storable a => [a] -> (Ptr a -> IO b) -> IO b withArray vals = withArrayLen vals . const -- |Like 'withArray', but the action gets the number of values -- as an additional parameter -- withArrayLen :: Storable a => [a] -> (Int -> Ptr a -> IO b) -> IO b withArrayLen vals f = allocaArray len $ \ptr -> do pokeArray ptr vals res <- f len ptr return res where len = length vals
Foreign.Marshal.Arrayモジュールでは、useAsCArrayに相当するラッパー関数としてwithArrayとwithArrayLenが提供されています。withArrayLenは、Cの配列を扱う関数に「作成したCの配列の長さ」を追加で渡します。実際には、withArrayはwithArrayLenを使って実装されています。
useAsCArrayとwithArray/withArrayLenには、メモリー管理をソースコード上で行うのか、メモリー管理を言語処理系に委ねるのか、という違いがあります。useAsCArrayはbracketを利用することで、Haskellソースコード上で、「Cの配列を使った処理の終了時」や「Cの配列を使った処理の中で例外が発生した時」にmalloc(mallocArray)で確保されたメモリーが開放されることを保障しています。これに対し、withArray/withArrayLenはメモリーの確保にalloca(allocaArray)を利用することで、確保したメモリーをHaskell処理系側で管理しています。この違いは、場合によっては処理の安全性や性能面の差として現れることもあるでしょう(参考リンク1、参考リンク2)。
withArrayやwithArrayLenは以下のように使います。特に理由がなければ、内部でCの配列扱いたい場合にはこれらの関数を使うのがよいでしょう。
import Foreign.C.Types ( CInt(..) ) import Foreign.Marshal.Array import Foreign.Ptr ( Ptr(..) ) main :: IO () main = do let xs :: [CInt] xs = [0..9] len = length xs ys <- withArray xs $ \carr -> do -- ys <- withArrayLen xs $ \len carr -> do cUpdateArray carr 4 (fromIntegral len) peekArray len carr print ys foreign import ccall "updateArray" cUpdateArray :: Ptr CInt -> CInt -> CInt -> IO ()
実行結果:
$ ghc carray.h carray.c Array.hs Linking Array ... $ ./Array [4,5,6,7,8,9,10,11,12,13]