xpmem プロセス間大容量データコピー
概要
xpmem (Cross-Partition Memory) は、あるプロセスの仮想メモリ領域を別プロセスの仮想アドレス空間に直接マッピングするLinuxカーネルモジュールです。
xpmem
xpmemがプロセス間でメモリをマッピングする手続きは以下になります。
システムコールを介さずユーザ空間で直接データをLoad/Store できるため、共有メモリやパイプよりも高速なプロセス間データ転送が可能です。
本稿ではWSL2上でxpmemを使ったプロセス間大容量データコピーのベンチマークを行い、POSIX共有メモリ(Shared Memory:shm) や memcpy と性能の比較をおこないます。
1. 環境構築手順
1.1 使用環境
- OS: Ubuntu 22.04/24.04 (WSL2)
WSL2 + Ubuntu インストールは下記サイトの構築手順[1]と同じです。
ChatGPT + Playwright-MCP + Docker Desktop + WSLの組み合わせでラップバトルbotを構築 - ビルドツール:
build-essential,autoconf,automake,libtool - C(C++)コンパイラ
以下手順内の「ビルドツールのインストール」であらかじめインストールしておきます。
[C++] WSL C/C++ 開発環境 構築手順
1.2 WSL2での注意事項
WSL2 のデフォルトカーネルはカーネルモジュールのビルド/ロードに制限があります。
カスタムカーネルのビルドが必要です。
# === WSL2 カスタムカーネルビルド ===
# 1. WSL2カーネルソースの取得
cd ~
git clone https://github.com/microsoft/WSL2-Linux-Kernel.git --depth=1 -b linux-msft-wsl-6.6.y
cd WSL2-Linux-Kernel
# 2. ビルド依存パッケージのインストール
sudo apt update && sudo apt install -y build-essential flex bison dwarves \
libssl-dev libelf-dev bc python3 pahole cpio
# 3. ncursesパッケージ(menuconfigはターミナル上にGUIメニューを表示するため)をインストール
sudo apt update
sudo apt install -y libncurses-dev pkg-config
# 4. カーネル設定 (モジュールロード対応を有効化)
make menuconfig KCONFIG_CONFIG=Microsoft/config-wsl
# → "Enable loadable module support" が有効であることを確認
# 5. ビルド
make -j$(nproc) KCONFIG_CONFIG=Microsoft/config-wsl
make modules -j$(nproc) KCONFIG_CONFIG=Microsoft/config-wsl
sudo make modules_install
# 6. カーネルイメージのコピー
mkdir -p /mnt/c/Users/<USERNAME>/.wsl-kernels/
cp ~/WSL2-Linux-Kernel/arch/x86/boot/bzImage /mnt/c/Users/<USERNAME>/.wsl-kernels/
# 7. .wslconfig の編集 (Windows側: %USERPROFILE%\.wslconfig)
# (.wslconfigファイルに下記kernel=の行を "> " 抜きで追記)
> [wsl2]
> kernel=C:\\Users\\<USERNAME>\\.wsl-kernels\\bzImage
# 8. exit - logout
exit
# 8. WSL再起動 (PowerShell)
wsl --shutdown
wsl
1.3 xpmem カーネルモジュールのビルドとインストール
# 1. 依存パッケージ
sudo apt update
sudo apt install -y build-essential autoconf automake libtool \
linux-headers-$(uname -r) git
# 2. xpmem ソースの取得
cd ~
git clone https://github.com/hpc/xpmem.git
cd xpmem
# 3. ビルド
sudo apt install -y autoconf automake libtool
autoreconf --install
./configure --prefix=/usr/local
make -j$(nproc)
# 4. インストール
sudo make install
# 5. カーネルモジュールのロード
find /usr/local/lib/modules -name "xpmem.ko"
# findで見つかったパスに対してinsmodを実行
# findの結果、今回は以下のパスにxpmem.koが存在
# /usr/local/lib/modules/6.6.114.1-microsoft-standard-WSL2+/kernel/xpmem/xpmem.ko
sudo insmod /usr/local/lib/modules/6.6.114.1-microsoft-standard-WSL2+/kernel/xpmem/xpmem.ko
# 6. デバイスファイルの確認
ls -la /dev/xpmem
# 7. パーミッション設定 (必要に応じて)
sudo chmod 666 /dev/xpmem
# 8. 起動時に自動ロードする場合
echo "xpmem" | sudo tee /etc/modules-load.d/xpmem.conf
1.4 ライブラリパスの設定
echo "/usr/local/lib" | sudo tee /etc/ld.so.conf.d/xpmem.conf
sudo ldconfig
# ヘッダの確認
ls /usr/local/include/xpmem.h
2. プロジェクト構成
2.1 ファイル・ディレクトリ構成
xpmem_bench/
├── Makefile
├── common.h // 共通定義・ユーティリティ
├── xpmem_exporter.c // xpmem: メモリ公開側プロセス (サーバ)
├── xpmem_importer.c // xpmem: メモリアタッチ・コピー側 (クライアント)
├── shm_bench.c // POSIX共有メモリ比較用ベンチマーク
└── run_bench.sh // 自動ベンチマーク実行スクリプト
2.2 xpmem_exporterとxpmem_importerの同期
xpmem_exporter xpmem_importer
│ │
│ memory allocate : xpmem_make() │
│ (shared importer's memory) │
│ │
│ │
│ signal_file(READY_FILE) ----> wait_for_file(READY_FILE)
│ make "/tmp/xpmem_ready" │
│ │
│ │ xpmem_get / attach
│ │ execute benchmark
│ │
│ │
│ wait_for_file(DONE_FILE) <---- signal_file(DONE_FILE)
│ make "/tmp/xpmem_done"
│ │
│ │
│ clean up │ terminate
3. ビルドと実行
# 上記2.1の *.c/*.h/*.sh/Makefile の各ファイルをLinux側"~/dev/xpmem_bench"に配置
mkdir -p ~/dev/xpmem_bench
# ビルド
cd ~/dev/xpmem_bench/
make clean
make
# パーミッションの設定
chmod 755 ~/dev/xpmem_bench
chmod 755 ~/dev/xpmem_bench/results
chmod 644 ~/dev/xpmem_bench/*.c ~/dev/xpmem_bench/*.h ~/dev/xpmem_bench/Makefile
chmod 755 ~/dev/xpmem_bench/xpmem_exporter ~/dev/xpmem_bench/xpmem_importer ~/dev/xpmem_bench/shm_bench ~/dev/xpmem_bench/run_bench.sh
# パーミッションの確認
ls -la ~/dev
# 実行ビットが-xになっていること
drwxr-xr-x 3 user user 4096 Feb DD hh:mm xpmem_bench
ls -la ~/dev/xpmem_bench
# 実行ビットが-xになっていること
-rwxr-xr-x 1 user user 4687 Feb DD hh:mm run_bench.sh
-rwxr-xr-x 1 user user 17040 Feb DD hh:mm shm_bench
-rwxr-xr-x 1 user user 17096 Feb DD hh:mm xpmem_exporter
-rwxr-xr-x 1 user user 21272 Feb DD hh:mm xpmem_importer
# .bashrcにモジュールロードとパーミッションの設定を追加
# 正しい設定を追加(モジュールロードとセット)
echo 'sudo insmod $(find /usr/local/lib/modules -name "xpmem.ko") 2>/dev/null; sudo chmod 666 /dev/xpmem 2>/dev/null' >> ~/.bashrc
# プロセス間メモリアクセスの制限を無効にする(ptrace_scope=0)
sudo sysctl -w kernel.yama.ptrace_scope=0
# ここでwslを再起動
実行方法1: 自動ベンチマーク
chmod +x run_bench.sh
./run_bench.sh
実行方法2: 手動
A. エクスポータ起動
cd ~/dev/xpmem_bench
# 再度実行するときはまず同期ファイルをクリア
rm -f /tmp/xpmem_segid /tmp/xpmem_ready /tmp/xpmem_done
./xpmem_exporter &
./xpmem_importer
# 再実行(1GBだと時間がかかるので512MBに絞ると速い)
./xpmem_exporter 512 &
./xpmem_importer
# タイムスタンプ付きのベンチマーク結果ログを出力する場合は以下
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
sudo rm -f /tmp/xpmem_segid /tmp/xpmem_ready /tmp/xpmem_done
./xpmem_exporter 512 > exporter_${TIMESTAMP}.log 2>&1 &
./xpmem_importer 2>&1 | tee importer_${TIMESTAMP}.log
4. 実行結果
run_bench.shの実行結果ログから各メモリコピー手順での速度の違いを解析します。
xpmem-cpy と xpmem-dir(direct) の違い
xpmem-cpy(bench_xpmem_memcpy関数)
// リモートメモリ → ローカルバッファへ memcpy
memcpy(local_buf, attached_ptr, size);
xpmem-dir(bench_xpmem_direct関数)
// ローカルバッファを使わず、リモートメモリを直接 load
const uint64_t *p = (const uint64_t *)attached_ptr;
for (size_t i = 0; i < count; i++) {
checksum += p[i]; // attached_ptr を直接読む
}
メモリアクセスの違い
【xpmem-cpy】
エクスポータのメモリ ──memcpy──→ インポータのローカルバッファ
(リモートページ) (ローカルページ)
↑
2回メモリアクセス(read + write)
【xpmem-dir】
エクスポータのメモリ ──load──→ CPU レジスタ(checksum に加算)
(リモートページ)
↑
1回メモリアクセス(read のみ)
ローカルバッファへの書き込みなし = ゼロコピー
結果サマリー
=== システム情報 ===
日時: Thu Feb 26 08:52:51 JST 2026
ホスト名: DESKTOP-BLD811U
カーネル: 6.6.114.1-microsoft-standard-WSL2+
CPU: Intel(R) Core(TM) i5-7300U CPU @ 2.60GHz
CPUコア数: 2
メモリ: 7.8Gi
環境: WSL2
ウォーム時帯域(iter2〜5平均(iter1は平均から除外)、実効帯域)単位: GB/s
ウォーム(Warm)= キャッシュにデータが乗った状態
コールド(Cold)= キャッシュが空の状態
※iterはイテレーションの略
| サイズ | xpmem-cpy | xpmem-dir | LOCAL-cpy | SHM-cpy |
|---|---|---|---|---|
| 4KB | 14.00 | 4.24 | 33.07 | 11.21 |
| 64KB | 35.59 | 4.38 | 35.62 | 19.88 |
| 1MB | 19.43 | 3.81 | 18.45 | 11.33 |
| 16MB | 4.49 | 3.68 | 3.52 | 4.01 |
| 64MB | 5.11 | 3.76 | 4.06 | 3.44 |
| 256MB | 5.03 | 3.59 | 4.32 | 4.25 |
| 512MB | 5.19 | 3.44 | 4.07 | 4.17 |
| 1GB | 4.54 | 3.77 | 4.36 | 4.39 |
結果比較
1. xpmem-cpy は大きいサイズで LOCAL-cpy より速い(116〜128%)
16MB以上でxpmem-cpyがLOCAL-cpyを上回っています。
原因はxpmemのページマッピング機構にあります。
xpmem-cpyはエクスポータ側のページをインポータの仮想アドレス空間にマッピングしているため、コピー元がすでにエクスポータ側でウォームアップ済みのページとなり、CPUキャッシュの競合が少ない状態でmemcpyが走るためと考えられます。
2. xpmem-dir(ゼロコピー直接アクセス)は一定の 3.5〜4.3 GB/s で頭打ち
xpmem-dirはコピーをせず直接リモートメモリをloadしています。
全サイズで均一な帯域になっているのは、恐らくメモリバンド幅上限に達しているためです。
この頭打ちはWSL2やxpmemの問題ではなく、i5-7300Uのメモリハードウェア仕様によるものと思われます。
3. コールド時(iter1)はxpmem-cpyが大幅に遅い
xpmem-cpyのiter1では全サイズで遅めになります。xpmemが初回アクセス時にページテーブルエントリをオンデマンドで構築するコスト(ページフォルト処理)が乗るためです。
xpmem-dirはiter1から安定しているため、ページフォルトのコストが小さいことがわかります。
4. SHM-cpyは64KBで高速
SHM-cpyの64KBウォーム時19.88 GB/sはPOSIX共有メモリがL2キャッシュに収まりきっているためと思われます。それ以上のサイズではxpmem-cpyと同程度になります。
今回の結果 xpmem-cpy が xpmem-dir より速い理由
| サイズ | xpmem-cpy | xpmem-dir |
|---|---|---|
| 64MB | 5.11 GB/s | 3.76 GB/s |
| 512MB | 5.19 GB/s | 3.44 GB/s |
| 1GB | 4.54 GB/s | 3.77 GB/s |
memcpy はglibc が提供する高度に最適化された実装となっている模様です。
一方 xpmem-dir のループは uint64_t(64bit)単位の単純な逐次readで、コンパイラが最適化してもmemcpyほどの幅では動きません。
- memcpy : 256bit (32byte) 単位でバーストアクセス → メモリバスを広く使える
- xpmem-dir: 64bit (8byte) 単位のループ → バス幅を使い切れない
つまり 「コピーしない = 速い」ではなく「メモリバスをどれだけ効率よく使えるか」 が帯域を決めていると考えられます。。
xpmem-dir を本当にゼロコピーの利点として活かしたいなら、読み取り結果を別の処理にそのままパイプライン(例:in-place 演算) する使い方が適していると考えられます。
単純な帯域計測では memcpy に負けます。
今回の動作環境では 大容量データ転送(16MB以上)においてxpmem-cpyが最も高い帯域を実現 しており、その効果を確認することができました。
5.GitHub
以下ソースコード・シェルスクリプト・Makefileを保存しています。
