Swiftでメモリマップドファイルを使う

# 概要
Swiftでメモリマップドファイルを使いたくて、C言語の`mmap`を使ったコードを実装しました。
そのときのメモです。

コード全体を含めたサンプルコードはGitHubに置いています。
[MemoryMappedFileSwift](https://github.com/akirark/MemoryMappedFileSwift "MemoryMappedFileSwift")

この記事ではGitHubのコードをベースに解説用に変えています。
GitHubのコードは`struct`にして使いやすくしています。

# メモリマップドファイルとは
メモリマップドファイルは、RAMの代わりにファイルを使ってデータを入れているにもかかわらず、RAMと同じような感覚で使えるものです。他のプログラムと共有するような使い方もあります。

# Swiftでのコード
Swift専用APIやライブラリがあるかもしれませんが、C言語の関数を使って作りました。
SwiftからC言語の関数を使うと、Unsafe系の扱いが少し面倒でした。

## ファイルの作成またはオープン
メモリマップドファイルで最初に行うのは、ファイルの作成です。
C言語の関数の「open」を使います。取得したファイルデスクリプタは後で使います。

```swift
// ファイルパス
let filePath = ...

var pathBuf = [Int8]()
pathBuf.append(contentsOf: filePath.utf8CString)

let fileDescriptor = Darwin.open(pathBuf, O_CREAT | O_RDWR, S_IREAD | S_IWRITE);
```

## ファイルサイズの計算
メモリマップドファイルのファイルサイズはページサイズの倍数になっている必要があります。
ページサイズは`getpagesize`関数で取得できます。
必要なサイズ以上でページサイズの倍数になっているサイズで作成します。

```swift
// 必要なサイズ
let neededSize = ...

let pageSize = Int(getpagesize())
let mappedSize = ((neededSize + pageSize - 1) / pageSize) * pageSize
```

## ファイルの拡張
メモリマップドファイルは、読み書きする領域が既にファイルにも存在している必要があります。
なかったらメモリ不足と一緒です。
`ftruncate`関数でファイルサイズを調整します。

```swift
ftruncate(fileDescriptor, off_t(mappedSize))
```

## マッピング
作ったファイルにマッピングします。
マッピングには`mmap`関数を使います。関数が返したバッファを使って以降は通常のメモリのようにアクセスできます。

```swift
var mappedBuffer = Darwin.mmap(UnsafeMutableRawPointer(mutating: nil), mappedSize, 
                               PROT_READ | PROT_WRITE, MAP_SHARED, fileDescriptor, 0)
```

## マッピングされた領域に書き込む
マッピングされた領域にデータを書き込むときは、次の2段階の手順となります。

1. 通常のメモリアクセスと同様にバッファに書く。
2. `msync`関数でファイルと同期させる。

`msync`関数で同期するまでファイルの内容は更新されません。
メモリマップドファイルはファイルを使用するので、純粋なメモリアクセスよりは遅くなります。
毎回、変更する度に`msync`を呼ぶとパフォーマンスが想定以上に悪くなることもあるようです。
利用する場所に合わせて適切なタイミングを決定するのが良いようです。


```swift
if let buf = mappedBuffer.bindMemory(to: UInt8.self, capacity: 256) {
    for i in 0 ..< 256 {
        buf[i] = UInt8(i)
    }

    Darwin.msync(mappedBuffer, mappedSize, 0)
}
```

## マッピングされた領域から読み込む
マッピングされた領域からの読み込みは、通常のメモリアクセスと同じです。

```swift
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 
```

## マッピング終了とファイルのクローズ
必要なくなったらマッピングを解除して、ファイルを閉じて完了です。
テンポラリファイルなどで使用しているときは、このタイミングで削除すれば良いと思います。

```swift
Darwin.munmap(mappedBuffer, mappedSize)
Darwin.close(fileDescriptor)
```

# まとめ
大きなファイルのときも少ないメモリで処理できます。
搭載しているメモリよりも大きなデータを扱わなければいけないときなどには便利だと思います。
手元のマシンで5GBのメモリマップドファイルを作って、開いている状態のときのメモリ使用量を確認してみると6.6MBでした。

少しずつファイルを書き込んでいて、パフォーマンスが遅いみたいなときにも、データの書き込みタイミングと本当のファイルの更新タイミングを別々にできて、本当の更新回数を減らすことができ、パフォーマンス改善にもつなげられると思います。