Skip to content

Instantly share code, notes, and snippets.

@wanabe
Last active January 21, 2018 11:14
Show Gist options
  • Select an option

  • Save wanabe/351038360f6238978b73c10a9a583ed8 to your computer and use it in GitHub Desktop.

Select an option

Save wanabe/351038360f6238978b73c10a9a583ed8 to your computer and use it in GitHub Desktop.
手元の PC で YARV-MJIT のベンチマークを取りたい(on x86_64 Linux )

この文章は

「手元に YARV-MJIT の実行ファイルやそのソース一式がある前提で、手元の PC でベンチマークを取りたいとしたら、さてどうするか。」 というときに、どういった手段が取れるかを探るものです。 文章を書きながら探していく想定で、まだほとんど何も探せていません。

OS やアーキテクチャに大きく依存しそうなので、汎用的な話ではなく、とりあえず手元にある x86_64 Linux のみを対象にします。

とっかかり

餅は餅屋、速度はプログラミングコンテスト、ということで、情報オリンピックのページに決定性向上に関する記述があったのでこれを見ていくことにします。 (なおこれは自力でたどり着いたわけではなく、twitter で他人同士の会話を眺めていたらそういう話が出ていたのでそのまま見に行っただけです)

また他に、ベンチマークの詳しい理解と修正のコツ(翻訳) というページも役に立ちそうです。 ちょっとややこしいのですが、

  • Victor Stinner さんという Python のコア開発者の PyCon2017 の発表をもとに、
  • Hongli Lai さんという Passenger の開発者がベンチマークの方法論を書き、
  • hachi8833 さんという TechRacho の方が翻訳した

ページです。 (あとベンチマーク方法論と関係ない部分ですが、Victor Stinner さんの開発した perf は python のベンチマークフレームワークの名前であって linux の perf(1) とは別物なので注意。同じ名前なので最初混乱しました)

決定性(1) アドレス空間の乱数化を無効化する

/etc/sysctl.d のエントリから /proc/sys/kernel/randomize_va_space に 0 をセットするべし、と書かれています。 ちょっとこれは手元の PC で実行するのは勇気がいるのでできれば遠慮したいところです。 どうしても必要であれば、ネットワークを遮断した状態で実行すれば少しは安心できるかもしれません。

しかしここで、yupo5656 さんという方の日記に ASLR(address space layout randomization) に関する記事を発見しました。 ざっと見ると実行時に setarch -RL するのでいいんじゃないかという気がします。

PIE では

ただ、上の日記では PIE だと結局ランダムになっちゃうよというようなことも書かれているので、そして手元の miniruby にしろ ruby にしろ readelf -h すると

型: DYN (共有オブジェクトファイル)

と出てくるので、まだこれだけだとまずいんじゃないかと思われます。 configure.ac を見ると --disable-pie オプションがあるのでこれを試してみようかと考えています。 それで(性能や動作的に)まともな実行ファイルができるのかは試してみないとわかりません。

--disable-pie してみたところ

$(git rev-parse --show-toplevel)/configure --prefix="$RBENV_ROOT"/versions/yarv-mjit/ --disable-pie などとしてみたのですが、やっぱり「DYN (共有オブジェクトファイル)」のままでした。 よくわからないので後回しに。 もしかすると LDFLAGS に書かれた -rdynamic というフラグのせいなんじゃないかと思っているのですが、configure.ac では ELF バイナリかつ Linux なら -rdynamic をつけるというように書かれており、オプションで外したりはできなさそうに見えます。無理やり外すと今度は拡張ライブラリ .so が作成できなくなったり色々と面倒なことになりそうです。

決定性(2) (スレッドやプロセスごとに)単一の CPU で実行する

タスクがCPU 間を移動するとコストがかかるので移動させないように、とかかれています。

ただこれ、ruby とくに YARV-MJIT では結構めんどうそうです。 というのは、まず CRuby では

  1. メインスレッド
  2. タイマースレッド

が存在します。YARV-MJIT ではそれに加えて

  1. ワーカースレッド
  2. spawn で時々出現する GCC のプロセス

が存在するわけで、4 コアなら丸々使い切ってしまいます。 ベンチマーク専用機ならいざ知らず、手元の PC で他にまったくなにも動かさないということはないはずで、

  1. X Window System(最近だと Wayland ?)
  2. シェル
  3. ターミナル
  4. Linux カーネル
  5. その他サービスもろもろ

なんかが動いていそうです。 操作していなければほとんど動いていないようなものかもしれませんが、といっても YARV-MJIT に完全に専有させるわけにもいかず、キャッシュの破棄や復帰やコンテキストスイッチ自体や、もろもろコストがかかるのは避けられないような気がします。 それでもタスクがまるごと移動するよりはましなはずなので、sched_setaffinitypthread_setaffinity_np あたりを使うことにします。

なお、上に挙げたように YARV-mJIT では常時 3 スレッド + 時々 1 サブプロセスが基本なので、taskset コマンドを使うのはあまりうまくいかない気がしています。 isolcpus はどうなんでしょう、使ったことがないのでわかりませんが、これもあまりうまく使える気がしていません。

決定性(3) CPU 周波数が動的に変更されるのを防ぎ、「パフォーマンス重視」に設定する

電力や発熱の問題から、turboboost やらなんやらで CPU のパフォーマンスは動的に制御されているため、これを一定にせよとのこと。 Linux での実際の設定方法は ArchLinix の Wiki の「CPU 周波数スケーリング」のページ が詳しいです。

しかし、IOI のページではパフォーマンス重視=最大出力でいけと書かれていますが、Stinner によるとむしろ最小速度にせよとのこと。 どっちがいいのでしょうね。

まずは IOI を信じて sudo cpupower frequency-set -g performance してみることにします。

決定性(4) Turboboost を無効にする

と思ったら Turboboost だけは無効にしろとのこと。 echo 1 |sudo tee /sys/devices/system/cpu/intel_pstate/no_turbo してからベンチマークを取ることにします。

決定性(5) Java ではいくつかのコマンドラインオプションをつける

Java の話なので略。

決定性(6) 透過的 HugePages メモリを利用しない

/sys/kernel/mm/transparent_hugepage/enabled/sys/kernel/mm/transparent_hugepage/defrag に "madvise" か "never" をセットし、/sys/kernel/mm/transparent_hugepage/khugepaged/defrag に 0 をセットしておけ、とのこと。 「透過的 HugePages メモリ」ってなんだ、というと、メモリページあたり 4K ではなく 2 MB を使うように自動的になる設定、ということなんでしょうか。 自動的にの部分が「透過的」ってことなんじゃないですかね。すみません適当なことを言っています。

手元だと /sys/kernel/mm/transparent_hugepage/enabled/sys/kernel/mm/transparent_hugepage/defrag は madvise になっていたのでいいんだけすが、/sys/kernel/mm/transparent_hugepage/khugepaged/defrag だけ 1 になっていました。 echo 0 |sudo tee /sys/kernel/mm/transparent_hugepage/khugepaged/defrag することにします。

「ベンチマークの詳しい理解と修正のコツ」の方の話

IOI のページに出てこず「ベンチマークの詳しい理解と修正のコツ」の方に出てくる話もいくつかあります。

ウォームアップしろとか回数をちゃんと設定しろというのはまあそうだし、専用のハードウェア使えというのは前提の「手元の PC で」という部分を覆すので却下。そんな予算はありません。 「マルチプロセス」は上で書いたように元々が 3 + 1 並列なのであまり使えなさそう。連続実行するというのは回数設定と同じ話と考えて良さそうです。

ということで、実際できそうなのは「カーネルスケジューラを専用のコアで実行する」だと思います。nohz_fullrcu_nocbs でできるとのこと。どちらも知らない。試してみます。

と思ったのですが、起動前に grub で設定しないといけないもののようです。うーん面倒くさい。

もろもろ試す

アフィニティについてはプログラムの改変で対応することにしたので、こんなパッチを用意しました。 これを当てた上で、以下のようにしました。

$ $(git rev-parse --show-toplevel)/configure --prefix="$RBENV_ROOT"/versions/yarv-mjit/ --disable-pie
$ grep '["optflags"]=".*-D_GNU_SOURCE' config.status || sed -i config.status -e 's/"optflags"]="[^"]*/& -D_GNU_SOURCE/'
$ ./config.status
$ make -j4 install-nodoc
$ sudo cpupower frequency-set -g performance
$ echo 1 |sudo tee /sys/devices/system/cpu/intel_pstate/no_turbo
$ echo 0 |sudo tee /sys/kernel/mm/transparent_hugepage/khugepaged/defrag
$ for i in `seq -1 1 20`; do (setarch `uname -m` -R -L ruby -j -I.ext/x86_64-linux optcarrot/bin/optcarrot --benchmark optcarrot/examples/Lan_Master.nes || exit) | if [ "$i" -gt 0 ]; then sed -ne 's/fps: //p'; else cat >/dev/null; fi; done| tee plot1.log

再起動が面倒だったので nohz_fullrcu_nocbs は試していません。

比較

何も対策していない状態で plot2.log も作成した後、

$ (sed plot1.log -e 's/^/plot1 /'; sed plot2.log -e 's/^/plot2 /')|gnuplot -e "set xtics rotate; set yrange [20:55]; plot '<cat' using (1.0):2:(0):1 with boxplot; pause 100"

などとして箱ひげ図を書いてみました。

プロット結果

差がないどころか、むしろ対策後のはずの plot1 のほうが最大値・最小値の幅が大きいです。

終わりに

手元の PC で、かつ見様見真似では全然効果が出ませんでした。 やっぱり素人には統計は手強いので、黙って試行回数を増やすのが無難そうです。無念。

diff --git a/mjit.c b/mjit.c
index 548234d530..b780531e97 100644
--- a/mjit.c
+++ b/mjit.c
@@ -78,6 +78,7 @@
#include <dlfcn.h>
#endif
+#include <sched.h>
#include "vm_core.h"
#include "mjit.h"
#include "version.h"
@@ -325,6 +326,13 @@ start_process(const char *path, char *const *argv)
_exit(1);
}
}
+ {
+ cpu_set_t mask;
+ CPU_ZERO(&mask);
+ CPU_SET(3, &mask);
+ sched_setaffinity(pid, sizeof(cpu_set_t), &mask);
+ }
+
#endif
return pid;
}
diff --git a/thread_pthread.c b/thread_pthread.c
index f5985d80c8..d7f149e524 100644
--- a/thread_pthread.c
+++ b/thread_pthread.c
@@ -34,6 +34,7 @@
#if defined(__HAIKU__)
#include <kernel/OS.h>
#endif
+#include <sched.h>
void rb_native_mutex_lock(rb_nativethread_lock_t *lock);
void rb_native_mutex_unlock(rb_nativethread_lock_t *lock);
@@ -730,6 +731,12 @@ ruby_init_stack(volatile VALUE *addr
)
{
native_main_thread.id = pthread_self();
+ {
+ cpu_set_t mask;
+ CPU_ZERO(&mask);
+ CPU_SET(0, &mask);
+ pthread_setaffinity_np(native_main_thread.id, sizeof(cpu_set_t), &mask);
+ }
#ifdef __ia64
if (!native_main_thread.register_stack_start ||
(VALUE*)bsp < native_main_thread.register_stack_start) {
@@ -1653,6 +1660,13 @@ rb_thread_create_timer_thread(void)
return;
}
+ {
+ cpu_set_t mask;
+ CPU_ZERO(&mask);
+ CPU_SET(1, &mask);
+ pthread_setaffinity_np(native_main_thread.id, sizeof(cpu_set_t), &mask);
+ }
+
/* validate pipe on this process */
timer_thread_pipe.owner_process = getpid();
timer_thread.created = 1;
@@ -1802,6 +1816,12 @@ rb_thread_create_mjit_thread(void (*child_hook)(void), void (*worker_func)(void)
&& pthread_create(&worker_pid, &attr, mjit_worker, worker_func) == 0) {
/* jit_worker thread is not to be joined */
pthread_detach(worker_pid);
+ {
+ cpu_set_t mask;
+ CPU_ZERO(&mask);
+ CPU_SET(2, &mask);
+ pthread_setaffinity_np(worker_pid, sizeof(cpu_set_t), &mask);
+ }
return TRUE;
}
else {
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment