概要
Swiftでメモリマップドファイルを使いたくて、C言語のmmap
を使ったコードを実装しました。
そのときのメモです。
コード全体を含めたサンプルコードはGitHubに置いています。
MemoryMappedFileSwift
この記事ではGitHubのコードをベースに解説用に変えています。
GitHubのコードはstruct
にして使いやすくしています。
メモリマップドファイルとは
メモリマップドファイルは、RAMの代わりにファイルを使ってデータを入れているにもかかわらず、RAMと同じような感覚で使えるものです。他のプログラムと共有するような使い方もあります。
Swiftでのコード
Swift専用APIやライブラリがあるかもしれませんが、C言語の関数を使って作りました。
SwiftからC言語の関数を使うと、Unsafe系の扱いが少し面倒でした。
ファイルの作成またはオープン
メモリマップドファイルで最初に行うのは、ファイルの作成です。
C言語の関数の「open」を使います。取得したファイルデスクリプタは後で使います。
// ファイルパス
let filePath = ...
var pathBuf = [Int8]()
pathBuf.append(contentsOf: filePath.utf8CString)
let fileDescriptor = Darwin.open(pathBuf, O_CREAT | O_RDWR, S_IREAD | S_IWRITE);
ファイルサイズの計算
メモリマップドファイルのファイルサイズはページサイズの倍数になっている必要があります。
ページサイズはgetpagesize
関数で取得できます。
必要なサイズ以上でページサイズの倍数になっているサイズで作成します。
// 必要なサイズ
let neededSize = ...
let pageSize = Int(getpagesize())
let mappedSize = ((neededSize + pageSize - 1) / pageSize) * pageSize
ファイルの拡張
メモリマップドファイルは、読み書きする領域が既にファイルにも存在している必要があります。
なかったらメモリ不足と一緒です。
ftruncate
関数でファイルサイズを調整します。
ftruncate(fileDescriptor, off_t(mappedSize))
マッピング
作ったファイルにマッピングします。
マッピングにはmmap
関数を使います。関数が返したバッファを使って以降は通常のメモリのようにアクセスできます。
var mappedBuffer = Darwin.mmap(UnsafeMutableRawPointer(mutating: nil), mappedSize,
PROT_READ | PROT_WRITE, MAP_SHARED, fileDescriptor, 0)
マッピングされた領域に書き込む
マッピングされた領域にデータを書き込むときは、次の2段階の手順となります。
- 通常のメモリアクセスと同様にバッファに書く。
-
msync
関数でファイルと同期させる。
msync
関数で同期するまでファイルの内容は更新されません。
メモリマップドファイルはファイルを使用するので、純粋なメモリアクセスよりは遅くなります。
毎回、変更する度にmsync
を呼ぶとパフォーマンスが想定以上に悪くなることもあるようです。
利用する場所に合わせて適切なタイミングを決定するのが良いようです。
if let buf = mappedBuffer.bindMemory(to: UInt8.self, capacity: 256) {
for i in 0 ..< 256 {
buf[i] = UInt8(i)
}
Darwin.msync(mappedBuffer, mappedSize, 0)
}
マッピングされた領域から読み込む
マッピングされた領域からの読み込みは、通常のメモリアクセスと同じです。
if let buf = mappedBuffer.bindMemory(to: UInt8.self, capacity: 256) {
for i in 0 ..< 256 {
let str = String(format: "%02X", buf[i])
print(str, separator: "", terminator: "")
if ((i + 1) % 4) == 0 {
print(" ", separator: "", terminator: "")
}
if ((i + 1) % 16) == 0 {
print()
}
}
}
少し上のコードで、先頭256バイトに、0から255までの値を書き込んでいるので、実行すると次のように出力されました。
書き込んだものが読み取れています。
00010203 04050607 08090A0B 0C0D0E0F
10111213 14151617 18191A1B 1C1D1E1F
20212223 24252627 28292A2B 2C2D2E2F
30313233 34353637 38393A3B 3C3D3E3F
40414243 44454647 48494A4B 4C4D4E4F
50515253 54555657 58595A5B 5C5D5E5F
60616263 64656667 68696A6B 6C6D6E6F
70717273 74757677 78797A7B 7C7D7E7F
80818283 84858687 88898A8B 8C8D8E8F
90919293 94959697 98999A9B 9C9D9E9F
A0A1A2A3 A4A5A6A7 A8A9AAAB ACADAEAF
B0B1B2B3 B4B5B6B7 B8B9BABB BCBDBEBF
C0C1C2C3 C4C5C6C7 C8C9CACB CCCDCECF
D0D1D2D3 D4D5D6D7 D8D9DADB DCDDDEDF
E0E1E2E3 E4E5E6E7 E8E9EAEB ECEDEEEF
F0F1F2F3 F4F5F6F7 F8F9FAFB FCFDFEFF
マッピング終了とファイルのクローズ
必要なくなったらマッピングを解除して、ファイルを閉じて完了です。
テンポラリファイルなどで使用しているときは、このタイミングで削除すれば良いと思います。
Darwin.munmap(mappedBuffer, mappedSize)
Darwin.close(fileDescriptor)
まとめ
大きなファイルのときも少ないメモリで処理できます。
搭載しているメモリよりも大きなデータを扱わなければいけないときなどには便利だと思います。
手元のマシンで5GBのメモリマップドファイルを作って、開いている状態のときのメモリ使用量を確認してみると6.6MBでした。
少しずつファイルを書き込んでいて、パフォーマンスが遅いみたいなときにも、データの書き込みタイミングと本当のファイルの更新タイミングを別々にできて、本当の更新回数を減らすことができ、パフォーマンス改善にもつなげられると思います。