###### tags: `SEZAX` # Jmalloc導入に関する調査 # mallocとは 動的メモリ確保を行うC言語の標準ライブラリの関数。 必要なサイズが実行で時でないと判明しない場合に動的メモリ確保を使用するとよい。 C言語では`malloc`関数を使ってヒープ領域からメモリブロックを確保する。 ### ヒープ領域 動的に確保可能なメモリの領域。ヒープ領域は使用中ノードと未使用ノードに分かれている。確保したメモリが不要になった場合にメモリを解放し、ノードを未使用ラベルに書き換える。解放の都度、もしくはカウンタによって一定水準に達した時、連続したここの未使用ノードを結合し、大きな未使用ノードに還元する。 ## C言語での動的メモリアロケーション `malloc`はC言語におけるヒープ領域からのメモリ確保に使われる基本関数。 ```objectivec= void *mallow(size_t size) ``` ここで*size*バイトのメモリが確保される。確保が成功するとメモリブロックへのポインタが返される。 C言語では汎用ポインタ`void*`と任意の型`T`へのポインタ`T*`との間の型変換が暗黙的に実行されるため、戻り値は明示的な型変換を記述しなくてもいい。 ```objectivec= int *ptr = (int *)malloc(sizeof(int) * 10); ``` ### メモリの解放 `malloc`で確保されたメモリは持続性がある。プログラム終了時か明示的に解放しない限り存在し続ける。 ```objectivec= void free(void *pointer) ``` *ponter*の指すメモリブロックが解放される。 ### 関連する関数 `malloc`はメモリブロックを確保して返すが、初期化はしていない。`memset`関数で初期化できる。 また、`calloc`関数を使うことで、メモリ確保と初期化を行える。 ```objectivec= void *calloc(size_t nelements, size_t bytes) ``` *bytes*のサイズのメモリ領域を*nelements*個格納できるメモリを確保する。確保された領域はゼロで初期化される。 メモリブロックを大きくしたり小さくする場合は、`realloc`関数を使う。 ```objectivec= void *realloc(void *pointerm size_t bytes) ``` `realloc`は指定されたサイズのメモリ領域へのポインタを返す。 ## 使用例 スラック上に10個の整数の配列を作成する一般的な方法は次の通り。 ```objectivec= int array[10]; ``` 同様の配列を動的に確保するには、以下のように`malloc` を使う。 ```objectivec= # include <stdlib.h> /*10個のintの配列のためのメモリを確保*/ int *ptr = malloc(sizeof(int) * 10); if (NULL == ptr) { exit(EXIT_FAILURE); /*メモリを確保できなかったので、終了*/ } else { /*確保成功*/ /*ここでその配列を使った処理を行う*/ /*処理が完了し、使わなくなったメモリブロックを解放する*/ free(ptr); /*解放したメモリにアクセスしてはならないことを示すため、NULLを代入しておく*/ ptr = NULL; } ``` ## 一般的なエラー ### 確保エラー `malloc`は必ず成功するとは限らない。空きメモリ領域がないときや限界値を超えてメモリをしようとした場合、`malloc`はヌルポインタを返す。 ヌルポインタが返された時を考慮していないとプログラムがクラッシュする。 ### メモリリーク `malloc`で確保したメモリブロックを使用しなくなっても解放せずに、次々と新たなメモリブロックを確保していると空きメモリが少なくなってくる。これをメモリリークと呼ぶ。 ### 解放後の使用 メモリが解放された後でその領域への参照を行うと、その内容は未定義であり利用できない。 以下のようなコードは予測不能の振る舞いをする。また同じポインタを2回`free`関数に渡してしまうことで、二重解放が起きる。これを防ぐためにも、解放後のポインタ変数には*NULL*を格納しておく。`free(NULL)`では何もしないことが保証されている。 ```objectivec= int *ptr = malloc(sizeof(*ptr)); /*メモリ領域を確保*/ ... free(ptr); /*ptrは解放済みの領域をまださしたまま*/ *ptr = 0; /*何が起きるかわからない*/ ``` ## Jemallocとは 標準ライブラリで定義されているmalloc, freeなどのメモリアロケーションAPIの実装である。 具体的には、malloc(), calloc(), realloc(), posix_memaligne(), free()の実装を提供する。 ### 特徴 * マルチコア環境でのマルチスレッドプログラムのスケーラビリティを改善する * メモリ領域で発生するフラグメンテーションを減らす * メモリの使用にまつわる種々の問題を解決するための検査機構、ツールを提供する ### マルチスレッドプログラムのスケーラビリティ マルチスレッドプログラムの改善について、jemallocでは以下のような施策を行なっている。 * arena(1セットのメモリ領域)を複数保持する * スレッドごとにThread Local Storageを用いてarenaとの紐付けを行う * arena、もしくはarenaの中のサイズクラスごとにLockを持つことでスレッド間の排他を削除する この効果についての、MySQLベンチマークでの比較 https://www.percona.com/blog/2013/03/08/mysql-performance-impact-of-memory-allocators-part-2/ この記事では、次のようなことが述べられている。 * glibc mallocとjemalloc、tcmallocを比較 * コアとスレッドを変化させてパフォーマンスを計測 * スレッドの増加は64でほぼ飽和する * jemallocとtcmallocはコアの増加でトランザクション数/秒をきっちりスケール * tcmallocとjemallocはほとんど同じ性能 * glib mallocは8コア以上でロック競合のため性能が劣化している * 結論: MySQLを使うサーバのコアが8よりも大きい場合はglib以外のアロケータを試す価値がある ### フラグメンテーションを削減する jemallocは、小さいサイズの割り当て要求のため、複数のsize classというものを用意している。割り当て要求は最も近いsize classにround up(切り上げ)され、実際のメモリ領域が割り当てられる。この時、領域のフラグメンテーションを削減するために、round upするサイズの最大値を要求サイズの25%以下になるようにしている。 Size Class `8, 16, 32, ...(+16), 128, 160, 192, 224, 256, 320, ...` 例えば257byteの割り当て要求が来た場合、320byteにround upされる。この時に無駄になる領域は63byteとなり、これは257byteに対して、約24.5%となる。 しかし、glib mallocも同様にsize classを持っており、512byteまでは8byte刻みのclassが設定されているため、一概にjemallocが有利とは言えない jemallocに差し替えるとメモリ使用量が減少する理由として、以下のことが挙げられる。 jemallocの最小割り当てサイズが8byteであるのに対して、mallocは16byteである。16byteおよび8byte以下の割り当てが多発した場合、最小割り当てサイズがmallocに比べ小さいjemallocの方が、内部フラグメントが軽減され、メモリ使用量も減少する http://blog.matsumoto-r.jp/?p=3131 ### Ruby: mallocでマルチスレッドプログラムのメモリが倍増する理由 https://techracho.bpsinc.jp/hachi8833/2017_12_28/50109 https://repeatedly.hatenadiary.org/entry/20110110/1294634486 ## Jemalloc導入 ### ローカルマシンに導入する場合 ``` # mac $ brew install jemalloc $ RUBY_CONFIGURE_OPTS=--with-jemalloc rbenv install 2.*.* ``` jemallocでrubyがコンパイルされているか確認 `-ljemalloc`が表示されていれば有効化されている ```ruby $ irb irb(main):002:0> RbConfig::CONFIG['MAINLIBS'] > "-lz -lpthread -lrt -lrt -ljemalloc -lgmp -ldl -lcrypt -lm" ``` ### Dockerで構築した環境に導入する場合 ``` # Dockerfile FROM ruby:2.*.* # install modules RUN apt-get update -qq && \ apt-get install -y libjemalloc1 libjemalloc-dev && rm -rf /var/lib/apt/lists/* ENV LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.1 ``` jemallocでrubyがコンパイルされているか確認 Rubyアプリケーションのpid(pumaで動かしていればpumaのpid)を指定してメモリマップを確認し、jemallocへのパスが表示されれば有効化されてる ``` $ sudo strings /proc/***(pid)/maps | grep jemalloc 7f59e5ff8000-7f59e6028000 r-xp 00000000 103:03 12712 /usr/lib64/libjemalloc.so.1 7f59e6028000-7f59e6227000 ---p 00030000 103:03 12712 /usr/lib64/libjemalloc.so.1 7f59e6227000-7f59e6229000 rw-p 0002f000 103:03 12712 /usr/lib64/libjemalloc.so.1 ``` ## テスト `derailed_benchmarks`の`bundle exec derailed exec perf:mem_over_time`で測定 https://github.com/schneems/derailed_benchmarks Mallocでコンパイルした場合は130付近からメモリが増加していいき230あたりで横ばいになっている 一方、Jemallocでコンパイルした場合は、200にいかない付近でメモリの増減が繰り返されている ![](https://i.imgur.com/RXRrbpO.png) 平均値からJemallocを使用することで、約24パーセントの使用メモリ削減 ``` Malloc Average Memory 234.5831612 Jemalloc Average Memory 177.3408746 Difference 24.40170314% ```