mallocの動作を追いかける(環境変数編)

# はじめに

malloc沼、ハマってますか？＞挨拶

僕はどっぷりハマってもうイヤになりました。必要に応じてメモリを確保し、解放するという、一見単純な作業はとても奥が深いです。

特にglibc mallocはかなりアドホックなことをやっており、チューニング可能なパラメータがたくさんあります。それらのパラメタは、多くの一般的なアプリケーションでそこそこの性能が出るように選ばれていますが、アプリケーションによっては相性が悪く、性能が劣化する場合があります。その場合、mallocに関連するパラメタをいじる必要がでてきますが、これがなかなか面倒だし、設定したことでどんな変化が表れるかがわかりにくいことが多いです。

本稿では、環境変数から設定可能なmalloc関連のパラメタをいくつか紹介します。特に、環境変数を設定することで振る舞いがなるべくわかりやすく変わるサンプルコードを用意しました。

mallocがらみのパラメタについては、glibcの公式ドキュメントの[3.2.3.7 Malloc Tunable Parameters](https://www.gnu.org/software/libc/manual/html_node/Malloc-Tunable-Parameters.html)に詳細な記述があるので参照してください。

また、mallocの動作については~~malloc DOGAを見る~~例えば以下の記事を参照してください。

* [mallocの動作を追いかける(mmap編)](https://qiita.com/kaityo256/items/9e78b507940b2292bf79)
* [mallocの動作を追いかける(prev_size編)](https://qiita.com/kaityo256/items/2e9a368a5b627daa2ff6)
* [mallocの動作を追いかける(main_arenaとsbrk編)](https://qiita.com/kaityo256/items/94a84dbe922eb5996a27)
* [mallocの動作を追いかける(fastbins編)](https://qiita.com/kaityo256/items/ca54b1b921d8ab96cb82)
* [mallocの動作を追いかける(マルチスレッド編)](https://qiita.com/kaityo256/items/bf6563361c502bbf062e)
* [mallocの動作を追いかける(環境変数編)](https://qiita.com/kaityo256/items/fc9a27d0df1371246951) ← イマココ

# MALLOC_MMAP_THRESHOLD_

mmapで確保するチャンクの最小値を決めます。この値より大きなサイズのバッファはmmapで確保するようになります。

```cpp
#include <cstdio>
#include <cstdlib>
#include <cstdint>

int main(void) {
  uint8_t *buf = (uint8_t *)malloc(1000);
  size_t *p = (size_t *)buf;
  if (*(p - 1) & 2) {
    printf("chunk in mmap\n");
  } else {
    printf("chunk in heap\n");
  }
  free(buf);
}
```

このサンプルコードでは1000バイトのメモリを確保しています。このままだとチャンクはヒープに取られます。

```sh
$ g++ test.cpp
$ ./a.out
chunk in heap
```

チャンクがヒープに取られたか、mmapされた領域に取られたかは、確保したバッファのアドレスの少し前に情報があります。

さて、`MALLOC_MMAP_THRESHOLD_`を1000にすると、このチャンクはmmapされた領域に確保されます。

```sh
$ MALLOC_MMAP_THRESHOLD_=1000 ./a.out
chunk in mmap
```

ここではmallocしているのは1000バイトですが、実際に確保されるバッファは管理領域も含めて1000バイトより大きくなります。

さらにスレッショルドを大きくすると、確保するメモリがスレッショルド以下になるので、またヒープに取られるようになります。

```sh
$ MALLOC_MMAP_THRESHOLD_=2000 ./a.out
chunk in heap
```

mmapは遅いので、ある程度大きなメモリを多数malloc/freeしたい場合は`MALLOC_MMAP_THRESHOLD_`を大きな値にすると性能向上につながる場合もあるようです(ただしメモリのフラグメンテーションとのトレードオフ)。

# MALLOC_MMAP_MAX_

mmapで確保するチャンクの最大数を指定します。デフォルトは65536です。

こんなコードを書いてみます。

```cpp
#include <cstdio>
#include <cstdlib>
#include <cstdint>

int main(void) {
  const int N = 10;
  uint8_t *buf[N];
  for (int i = 0; i < N; i++) {
    buf[i] = (uint8_t *)malloc(1024 * 1024);
    size_t *p = (size_t *)buf[i];
    if (*(p - 1) & 2) {
      printf("%d:chunk in mmap\n", i);
    } else {
      printf("%d:chunk in heap\n", i);
    }
  }
  for (int i = 0; i < N; i++) {
    free(buf[i]);
  }
}
```

多くの場合、1MBを超えるメモリをmallocしようとすると、mmapで確保されたメモリが割り当てられます。ここでは1MBのメモリを10個確保していますが、デフォルトでは全てmmapされたメモリにチャンクができます。

```sh
$ g++ test.cpp
$ ./a.out
0:chunk in mmap
1:chunk in mmap
2:chunk in mmap
3:chunk in mmap
4:chunk in mmap
5:chunk in mmap
6:chunk in mmap
7:chunk in mmap
8:chunk in mmap
9:chunk in mmap
```

しかし、例えばmmapで確保するチャンクの数を5個に制限すると、6個目からはヒープに取られます。

```sh
$ MALLOC_MMAP_MAX_=5 ./a.out
0:chunk in mmap
1:chunk in mmap
2:chunk in mmap
3:chunk in mmap
4:chunk in mmap
5:chunk in heap
6:chunk in heap
7:chunk in heap
8:chunk in heap
9:chunk in heap
```

mmapは遅いので、ある程度大きなメモリをmalloc/freeしたい場合は`MALLOC_MMAP_MAX_`を0にすると性能向上につながる場合もあるようです(ただしメモリのフラグメンテーションとのトレードオフ)。

# MALLOC_PERTURB_

malloc、及びfreeの直後に、バッファをこの環境変数の値で埋めます。デバッグ用だそうです。

こんなコードを書いてみます。

```cpp
#include <cstdio>
#include <cstdlib>
#include <cstdint>

int main(void) {
  uint8_t *buf = (uint8_t *)malloc(1024);
  printf("after malloc %u\n", (uint8_t)buf[0]);
  buf[0] = 0;
  free(buf);
  printf("after free   %u\n", (uint8_t)buf[0]);
}
```

バッファをmallocした直後と、0を代入した後でfreeした直後にバッファの先頭の値を表示しています。
mallocした直後の値は保証されていないと思いますが、多くの場合0が表示されると思います。
しかし、環境変数`MALLOC_PERTURB_`を指定すると、malloc/freeの直後にその値で埋められます。

```sh
$ g++ test.cpp
$ ./a.out
after malloc 0
after free   0
$ MALLOC_PERTURB_=1 ./a.out
after malloc 254
after free   1
$ MALLOC_PERTURB_=2 ./a.out
after malloc 253
after free   2
```

`buf[0]`に0を代入した後にもかかわらず、free後には`MALLOC_PERTURB_`に設定した値になっていることがわかるかと思います。
malloc直後は254や253になっていますが、これは、`0xff`とXORをとった値で埋められるからです。

glibc mallocのソースを見るとこうなっています。

[https://code.woboq.org/userspace/glibc/malloc/malloc.c.html#1947](https://code.woboq.org/userspace/glibc/malloc/malloc.c.html#1947)

```cpp
static int perturb_byte;
static void
alloc_perturb (char *p, size_t n)
{
  if (__glibc_unlikely (perturb_byte))
    memset (p, perturb_byte ^ 0xff, n);
}
static void
free_perturb (char *p, size_t n)
{
  if (__glibc_unlikely (perturb_byte))
    memset (p, perturb_byte, n);
}
```

環境変数により`perturb_byte`の値がセットされます。malloc直後は`perturb_byte ^ 0xff`が、free直後は`perturb_byte`の値そのものがmemsetされていることがわかります。

[glibcのドキュメント](https://www.gnu.org/software/libc/manual/html_node/Malloc-Tunable-Parameters.html)で、環境変数が
`MALLOC_MMAP_PERTURB_`と間違って記載されており(正しくは`MALLOC_PERTURB_`)、[bugzilla](https://sourceware.org/bugzilla/show_bug.cgi?id=23015)にも報告があるものの一年近く放置されているところを見ると、誰も使っていないのかもしれません。

# MALLOC_TOP_PAD_

mallocやfreeする際に、ヒープが足りなければ確保します。最初はヒープサイズはゼロです。ヒープの拡充はbrkというシステムコールによりOSに依頼しますが、これは遅いので、ちょこちょこbrkを呼ばずにすむように一番最初にmallocが呼ばれたときに「アリーナ」と呼ばれる大きな領域を「がこ」っとって来て、その中でやりくりするようにします。

しかし、それでも足りなくなるともう一度brkを呼び、さらに足りなくなるとbrkを呼び・・・という感じで、必要になるたびにbrkを呼びます。そのbrkを呼ぶ単位を制御するのが`MALLOC_TOP_PAD_`です。これはfreeした際にメモリをOSを返す際にも参照されます。

あまり大きくするとメモリ効率が悪くなり、あまり小さくするとbrkが多数呼ばれて性能劣化原因になります。それを見てみましょう。

```cpp
#include <cstdio>
#include <cstdlib>
#include <cstdint>

int
main() {
  size_t size = 1024;
  const int N = 10000;
  uint8_t *buf[N];
  for (int i = 0; i < N; i++) {
    buf[i] = (uint8_t *)malloc(size);
  }
  for (int i = 0; i < N; i++) {
    free(buf[i]);
  }
}
```

単に1024バイトを1000回mallocしてfreeするだけのコードです。このコードで何回brkが呼ばれたか、straceでカウントしてみましょう。

```sh
$ g++ test.cpp
$ strace -c -e brk ./a.out
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
  0.00    0.000000           0        80           brk
------ ----------- ----------- --------- --------- ----------------
100.00    0.000000                    80           total
```

デフォルトでは80回brkが呼ばれました。`MALLOC_TOP_PAD_`の値をいろいろ変えてどうなるか見てみましょう。

```sh
$ MALLOC_TOP_PAD_=1000 strace -c -e brk ./a.out
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
100.00    0.000016           0      2543           brk
------ ----------- ----------- --------- --------- ----------------
100.00    0.000016                  2543           total

$ MALLOC_TOP_PAD_=10000 strace -c -e brk ./a.out
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
  0.00    0.000000           0       850           brk
------ ----------- ----------- --------- --------- ----------------
100.00    0.000000                   850           total

$ MALLOC_TOP_PAD_=100000 strace -c -e brk ./a.out
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
  0.00    0.000000           0       105           brk
------ ----------- ----------- --------- --------- ----------------
100.00    0.000000                   105           total

$ MALLOC_TOP_PAD_=1000000 strace -c -e brk ./a.out
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
100.00    0.000659          47        14           brk
------ ----------- ----------- --------- --------- ----------------
100.00    0.000659                    14           total
```

`MALLOC_TOP_PAD_`の値が小さいとbrkが多数呼ばれ、大きいと回数が減ったのがわかるかと思います。

参照：[https://qiita.com/angel_p_57/items/47138c90791adb5a1b83](https://qiita.com/angel_p_57/items/47138c90791adb5a1b83)

# まとめ

mallocの性能に影響する環境変数を紹介しました。他にもいろいろ、特にマルチスレッドまわりを制御する変数もあったりするのですが、僕はもうイヤになりました。この記事が将来、誰かがmalloc沼にハマった際に、そこを脱出するための一助になることを祈ります。