💨

glibc mallocメモリ断片化の解決例

に公開

20MBのメモリを一時的に確保するとして、一回目はすぐOSに返され、RSSが下がるのに、二回目から下がらず、断片化してもりもりRSSが上がっていくことがある。
これはglibc mallocが一回目mmapで確保するのに、その後brkで確保し始めるからだった。
glibc mallocのmmapとbrkの閾値は128 * 1024 bytesから始まるが、標準で動的です。

glibc/malloc/malloc.cより
# if __WORDSIZE == 32
#  define DEFAULT_MMAP_THRESHOLD_MAX (512 * 1024)
# else
#  define DEFAULT_MMAP_THRESHOLD_MAX (4 * 1024 * 1024 * sizeof(long))
# endif
#endif

64bit環境ではsizeof(long) == 8なので、閾値は32MBまで上がり得ます。

実際に確かめてみましょう。

main.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

void malloc_and_free() {
  const size_t size = 20 * 1024 * 1024;
  puts("malloc");
  void *p = malloc(size);
  memset(p, 0, size);
  sleep(5);
  puts("free");
  free(p);
  sleep(5);
}

int main() {
  malloc_and_free();
  malloc_and_free();
  malloc_and_free();
  return 0;
}
$ gcc main.c
$ ./a.out

タイミングで

$ ps u -p $(pgrep a.out)

なんかをして計れば、2回めのfreeからはRSSが下がらないことが見えるでしょう。同期的で単純な場合これでも問題ないし、ほんの少し速くなってむしろ嬉しいかもしれませんが、非同期だったり、マルチスレッドだったり、アリーナがわかれて再利用されずただただ無駄なRSSが上がっていくのは嬉しくありません。mmapの閾値を設定して解決してみましょう。

$ MALLOC_MMAP_THRESHOLD_=131072 ./a.out

10~30MBぐらいのメモリが断片化してRSSが膨れるなら、環境変数を適当に設定して、回避できますね。
はじめからかなりメモリを食うjemallocやmimallocを使うより、こちらのほうがいい場合もあるかもしれません。
おわり

Discussion