はじめに
わたしの手持ちのノートPCにはWindows10が入っています。もっぱらWindows Subsystem for Linux(WSL, いわゆるbash on Ubuntu on Windows)上で作業をしています。そこそこのスペックのマシンなのですが、ちょっと重い処理を動かすだけで、やけにもっさりした動作になるのが気になっていました。そこで、ざくっとWSL(Windowsのバージョンは1703, OSビルドは15063.540)の性能を測定して、同じく手元にあるnativeマシン上のUbuntu 16.04(以下native Ubuntuと記載)のそれと比較してみました。
マシンスペック
名前 | WSL | native Ubuntu |
---|---|---|
カーネル | 4.4.0-43-Microsoft | 4.10.0-27-generic |
CPU | core m3-6Y30 | Ryzen 1800X |
RAM | 8GB | 32GB |
storage | SanDisk X300S SD7TB3Q-256G-1006(256GB: read 520MB/s, write 470MB/s) | Crucial CT275MX300SSD1(256GB: read 530MB/s write 500MB/s) |
filesystem | lxfs | btrfs |
上記の通りスペックがかなり異なること、および後述の性能測定方法の精度が粗いことより、両者の性能値を厳密に比較してもあまり意味がありません。そのため、あくまで「それぞれの性能が一桁くらい違えばさすがに違いがあると言えるだろう」という程度のざくっとした比較にとどめています。
プロセス/スレッド生成コスト
次のようなプログラムを使ってプロセス/スレッド生成時のコストを採取しました。
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>
#include <pthread.h>
#include <time.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <err.h>
char *progname;
static int concurrency;
static void show_usage(void)
{
fprintf(stderr, "usage: %s <p|t> <n>\n"
"\tmeasure process/thread creation time.\n"
"\t<p|t>: 'p' for process and set 't' for thread\n"
"\t<n>: concurrency\n",
progname);
exit(EXIT_FAILURE);
}
static void measure_process_creation_time(void)
{
pid_t *pids;
pids = malloc(sizeof(pid_t) * concurrency);
if (pids == NULL)
err(EXIT_FAILURE, "malloc() failed");
struct timespec begin, end;
if (clock_gettime(CLOCK_MONOTONIC_RAW, &begin) == -1)
err(EXIT_FAILURE, "clock_gettime() failed");
int i;
for (i = 0; i < concurrency; i++) {
pid_t pid;
pid = fork();
if (pid == -1) {
warn("fork() failed\n");
int j;
for (j = 0; j < i; j++) {
if (kill(pids[j], SIGINT) == -1)
warn("kill() failed");
}
free(pids);
exit(EXIT_FAILURE);
} else if (pid == 0) {
pause();
} else {
pids[i] = pid;
}
}
if (clock_gettime(CLOCK_MONOTONIC_RAW, &end) == -1) {
warn("clock_gettime() failed");
int j;
for (j = 0; j < concurrency; j++)
if (kill(pids[j], SIGINT) == -1)
warn("kill() failed");
free(pids);
exit(EXIT_FAILURE);
}
if (system("free") == -1) {
for (i = 0; i < concurrency; i++)
if (kill(pids[i], SIGINT) == -1)
warn("kill() failed");
free(pids);
err(EXIT_FAILURE, "system() failed");
}
long diff = (end.tv_sec - begin.tv_sec) * 1000000000 + (end.tv_nsec - begin.tv_nsec);
printf("%ld\n", diff);
for (i = 0; i < concurrency; i++) {
if (kill(pids[i], SIGINT) == -1)
warn("kill() failed");
}
free(pids);
}
static void *thread_fn(void *arg)
{
pause();
/* shouldn't reach here */
abort();
}
static void measure_thread_creation_time(void)
{
pthread_t *tids;
tids = malloc(sizeof(pthread_t) * concurrency);
if (tids == NULL)
err(EXIT_FAILURE, "malloc() failed");
struct timespec begin, end;
if (clock_gettime(CLOCK_MONOTONIC_RAW, &begin) == -1)
err(EXIT_FAILURE, "clock_gettime() failed");
int i;
for (i = 0; i < concurrency; i++) {
int ret;
ret = pthread_create(&(tids[i]), NULL, thread_fn, NULL);
if (ret) {
errno = ret;
free(tids);
err(EXIT_FAILURE, "pthread_create() failed\n");
}
}
if (clock_gettime(CLOCK_MONOTONIC_RAW, &end) == -1) {
free(tids);
err(EXIT_FAILURE, "clock_gettime() failed");
}
if (system("free") == -1) {
free(tids);
err(EXIT_FAILURE, "system() failed");
}
long diff = (end.tv_sec - begin.tv_sec) * 1000000000 + (end.tv_nsec - begin.tv_nsec);
printf("%ld\n", diff);
free(tids);
}
int main(int argc, char *argv[])
{
progname = argv[0];
if (argc < 3)
show_usage();
char op = argv[1][0];
if (op != 'p' && op != 't') {
fprintf(stderr, "operation should be 'p' or 't': %c\n", op);
show_usage();
}
concurrency = atoi(argv[2]);
if (concurrency <= 0) {
fprintf(stderr, "concurrency should be >= 1: %d\n", concurrency);
show_usage();
}
switch (op) {
case 'p':
measure_process_creation_time();
break;
;;
case 't':
measure_thread_creation_time();
break;
;;
default:
show_usage();
}
exit(EXIT_SUCCESS);
}
プロセス生成の場合
上記プログラムを用いて1000個プロセスを生成した際の1プロセスあたりの生成時間と同メモリ使用量を採取しました。1プロセス当たりのメモリ使用量を厳密に求めるのは難しいので、(1000プロセス生成時のメモリ総使用量 - プログラム実行前のシステムメモリ総使用量)/1000
で代替しました。このような性能を5回測定をした際の平均値を次に示します。
名前 | WSL | native Ubuntu |
---|---|---|
生成時間[ms] | 4.53 | 0.0448 |
メモリ使用量[MB] | 2.19 | 0.105 |
生成時間は100倍以上の差がついているので、いくら両者のプロセッサ性能が違うにしても、それだけでは説明がつきません。WSLのほうがnative Ubuntuに比べてはるかに遅いと言っていいでしょう。
メモリ使用量についても20倍程度の差がついています。この値はハードスペックは関係無く、かつ、プロセス生成時には生成した子プロセスのユーザメモリはほぼ割り当てられていない(子プロセスのメモリにwriteしたときにCopy-on-Writeによって子プロセス用の新規メモリが割り当てられる)であろうことを考えると、WSLはプロセスごとに消費するカーネルメモリの量がnative Ubuntuに比べて非常に多いと言えるでしょう。
余談ですが、私のノートPCでは2000と少々のプロセスを生成した時点でswapが始まりました。あまりやる人はいないと思いますが、WSL上でプロセスを大量に動かすような処理を動かすのはnative Ubuntuの場合に比べてかなりのハンデを背負うでしょう。
スレッド生成の場合
上記プログラムを使ってスレッド生成時の生成時間および1スレッドあたりのメモリ使用量も求めました。5回測定した平均値を次に示します。
名前 | WSL | native Ubuntu |
---|---|---|
生成時間[ms] | 0.248 | 0.0108 |
メモリ使用量[MB] | 0.0508 | 0.0366 |
生成時間もメモリ使用量もプロセスの場合と同じくWSLのほうが性能が悪いですが、差は明らかに縮まっています。前者については100倍の差があったものが20倍程度の差になっています。後者については20倍の差があったものが2倍弱ほどの差です。native Ubuntuの場合においてもスレッド生成コストはプロセス生成コストよりも低いですが、WSLの場合はとくにそれが顕著であると言えるでしょう。
I/O性能
read性能
1GBのファイルを1MBごとに1024回に分けて読み出すことによってread I/O性能を採取しました。ファイルがページキャッシュに乗っている場合と乗っていない場合について採取しました。ページキャッシュの削除には、下記ddの前にecho 3 >/proc/sys/vm/drop_caches
を実行するという方法をとりました。採取方法は次の通り。
dd if=testfile of=/dev/zero bs=1M count=1024
10回測定した際の平均値を次に示します。
名前 | WSL | native Ubuntu 16.04 |
---|---|---|
with page cache[MB/s] | 2170 | 12100 |
without page cache[MB/s] | --- | 489 |
WSLの性能はnative Ubuntuに比べると6倍程度遅いです。二つの環境におけるストレージの性能差を見るにハード性能の差だけでこれだけの差が出るとは思えないので、WSLはnative Ubuntuに比べて読み出し性能が劣ると言えるでしょう。
ページキャッシュなしの場合についてはWSLにおいてページキャッシュを削除する簡単な方法が思いつかなかったので、比較はできませんでした1。
write性能
1MBのデータを1024回、合計1GBのファイルを新規ファイルに書き込むことによってwrite I/O性能を計測しました。データはbuffered I/O, direct I/Oそれぞれについて次のように採取しました。
dd if=/dev/zero of=testfile bs=1M count=1024; rm testfile
dd if=/dev/zero of=testfile oflag=direct bs=1M count=1024; rm testfile
10回測定した際の平均値を次に示します。
名前 | WSL | native Ubuntu 16.04 |
---|---|---|
buffered I/O[MB/s] | 324 | 3420 |
direct I/O[MB/s] | 328 | 151 |
buffered I/OについてはWSLはnative Ubuntuに比べて一桁程度遅いことがわかります。ストレージのwrite性能はそれほど違わないことを考えると、ファイルシステム(lxfs) and/or その他OSのオーバーヘッドが非常に大きいと考えられます。direct I/OについてはWSLのほうが速いです。理由は不明ですが、ストレージの差から来ているのかもしれません。
さらにWSLはbuffered I/Oよりもdirect I/Oのほうが速いという意味不明な結果になっています。これについても理由は不明です。WSLはbuffered I/Oとdirect I/Oの性能がほとんど変わらないという面白いデータがとれました。write時はread時とは異なりページキャッシュを使わないのかもしれない…などという想像が膨らむのですが、実装を知らないので本当のところは不明です。
おわりに
かつて、何かの記事でWSLのCPU性能はnative Ubuntuに近いするようなことが書いていましたが、それ以外の面については、少なくとも本記事で触れたような部分についてはnative Ubuntuに比べて、文字通り桁違いに性能が悪いことがあると明らかになりました。これなら常々感じていたもっさりした動作にも納得です。私はWSL自身は非常に気に入っているので、今後の改善に期待です。
今後、気が向けば別のデータもとってみたいです。また、ストレージアクセス性能測定についての、いくつかの直感に反する結果点についても明らかにしたいです(誰か詳しいかたがいれば教えてほしいです)。
-
起動直後に上記ddを実行すればページキャッシュ無しの場合のデータを採取できると思いますが、時間の都合で今はそこまではやっていません。 ↩
windowsマシンで、virtualboxの中に入れたUbuntuでのスペック比較の比較もお願いできますでしょうか
面白そうですね。時間と気合があればやってみます。コメントありがとうございました。