餡子付゛録゛

ソフトウェア開発ツールの便利な使い方を紹介。

Rでmmap

計算機でmapと言うと、リストの要素を無名関数で一括処理することや(Python)、ハッシュテーブルのような構造(Java)を思い浮かべる人が多いかも知れませんが、mmapはファイルに仮想メモリーを割り当てるOSの提供するAPIです。巨大データを処理したり、高速ファイルアクセスが必要なときに使われます。

主記憶利用量の削減

Rでもmmapパッケージを用いると、mmapを利用することができます。取扱説明書(Vignettes)の利用例をそのまま説明に使います。まず、

library(mmap)
r <- rnorm(20e6)

とベクトルをつくりましょう。

gc(verbose = TRUE)

とガーベッジ・コレクターを走らせると、メモリー利用量が分かります。

gc()によるメモリー利用量確認

このベクトル r を、mmapオブジェクトの r に変更します。

r <- as.mmap(r)

もう一度、ガーベッジ・コレクターを走らせて、メモリー利用量を確認しましょう。

gc()によるメモリー利用量確認

157.2MB使っていたVcellsが、4.6MBの利用に減りました。減った、一時ファイルに退避されています。

mmapオブジェクトへのアクセス方法

残念なことにmmapオブジェクトは透過的には利用できないです。以下は標準のベクトルへの操作と同じですが、

r[1:10]
r[87643]
head(r)
tail(r)
length(r)
r[1:10] <- 1:10
r[87643] <- pi
head(r)
r[87643]

getElementを通して制御しているため、合計や平均の参照は以下のようにします。

sum(r[]) # sum(r)はエラー
mean(r[]) # mean(r)はエラー

後述の例で使いますが、構造体のmmapオブジェクトの場合はr[]$x,r[123]$xのようにアクセスするので、一般のRユーザーには非直感的だと思います。

仮想メモリーの解放

利用後、mmapオブジェクトはmunmap関数で開放します。

munmap(r)

開放してもファイルは残るのですが、ここでは一時ファイル領域につくられているのでOSの再起動時などに消去されます。

既存ファイルを仮想メモリーとして使う

主記憶領域の拡張としてではなく、永続的な保存領域として活用することもできます。

データを生成して仮想メモリーに保存する

仮想メモリーとして使われるファイルの中身はシンプルなバイナリ・データなので、R以外で作成したものを取り出してもよいのですが、Rの乱数生成したデータを保存してみましょう。

library(mmap)
# ファイル名は保存できれば何でもよい
fname <- paste(Sys.getenv("HOME"), "mmap.dat", sep="/")
# レコード型(データ構造)をmmap::structを使って宣言
record.type <- struct(y = double(), x = double(), z = double())
# レコード数
n <- 100
# レコード型のサイズ×レコードの数のraw型のmmapを作り、即開放
munmap(as.mmap(raw(nbytes(record.type)*n), mode = raw(), file = fname))
# レコード型を指定してファイルを仮想メモリに割り当てなおす
r <- mmap(fname, mode = record.type)
# 記録する
r[, "x"] <- runif(n)
r[, "z"] <- runif(n)
r[, "y"] <- 1 + r[,]$x - r[,]$z + rnorm(n)
# mmapを開放、削除
munmap(r); rm(r)

これでユーザーフォルダにmmap.datができました。なお、レコード型のデータ構造は browseVignettes("mmap") で見られる Vignette の表を参照してください。

既存ファイルを仮想メモリーにする

ファイル名とレコード型が分かっていたら、仮想メモリーに割り当てて再利用できます。

# record.type <- struct(y = double(), x = double(), z = double())
# 再度、ファイルを仮想メモリーに割り当てる
s <- mmap(fname, mode = record.type)
# 最尤法をかけてみる
r_nlm <- nlm(function(p){
    if(p[1]<=0) return(Inf)
    -sum(dnorm(s[]$y - p[2] - p[3]*s[]$x - p[4]*s[]$z, sd = p[1], log = TRUE))
}, c(1, 0, 0, 0))
# 仮想メモリーを解放する
munmap(s)
r_nlm$estimate # 誤差項の標準偏差,切片項、xの係数、zの係数がだいたいDSG通りに出るハズ

lmやglmのような内部的にmodel.matrixを使う関数は、仮想メモリー領域のデータから同じ長さの行列を作ってから推定を行うことになり、mmapの御利益が乏しいので最尤法にしました。

データ構造体へのアクセスの癖

s$yとアクセスできずs[]$yとアクセスすることは上の通りですが、同様にs[2]$yとアクセスするのであって、s$y[2]とアクセスできなかったりします。

C言語の構造体を保存したファイル

ありそうな応用例で、C言語の構造体を保存したファイルを仮想メモリーに割り当てる例を示します。

Cで構造体を保存

R側でパディングを処理するのが手間なので、構造体のアライメントは無効(というか1バイト単位)にしておきましょう。

#include<stdlib.h>
#include<stdio.h>

#define N 100

#pragma pack (push, 1)
typedef struct {
	unsigned int a;
	double b;
} SD;
#pragma pack (pop)

int main(){

	SD *v;
	unsigned	i;
	FILE		*os;
	unsigned char	*fname = "mmap_c.dat";
	unsigned n = N;

	v = malloc(sizeof(SD)*N);

	for(i = 0; i < N; i++){
		v[i].a = i;
		v[i].b = 1.23;
	}

	if(NULL == (os = fopen(fname, "wb"))){
		fprintf(stderr, "Couldn't open the file: %s\n", fname);
		return -1;
	}
	fwrite(v, sizeof(SD), N, os);
	fclose(os);

	free(v);
	return 0;
}

Rで仮想メモリーに割り当て

mmap::structで構造体の要素の型を指定すれば、intやdoubleの配列を読むのと同じようにできます。

fname <- "mmap_c.dat"
library(mmap)
record.type <- struct(a = integer(), b = double())
s <- mmap(fname, mode = record.type)
# munmap(s)

不正メモリーアクセスでRが即死

mmapパッケージはフールプルーフではないので、mmapオブジェクトの添字を誤るとRが即死したりします。

まとめ

最近の端末の主記憶容量から考えてまず要らないと思いますが、巨大データを扱う場合の他、Fortranあたりがつくった単純な構造の数値データのファイルを、仮想メモリーとして割り当てるような事も出来るので、局面によっては非常に便利だと思います。Rはバイナリデータの扱いが楽というわけではないので。