初Qiita投稿です。
#調査動機
あるマルチスレッドプログラムの実行時間が想定よりも遅かったので
どこがボトルネックか調査したら、memsetでのゼロクリアが原因だった。
memsetにそんなに時間がかかるのか?と思ったので軽く調査しました
#結論
memsetがボトルネックになっていたのは
メモリが確保されるのがmalloc時ではなく
初めての書き込み時(今回だとmemsetでゼロクリア)だったため
書き込み時に実際の物理メモリの確保を行うため重い処理に見えてしまった。
#調査内容
mallocを実行するとヒープ、もしくはページからメモリがmalloc時に確保されるというのが自分の認識でした。
実際はmalloc時に仮想メモリを引数で指定した分だけ予約するのが正しかったみたいです。
書き込み時にメモリを確保している様子を実際に見てみます。
[調査環境]
AWSのサービスであるEC2を使用しました(t2.micro)
OS:Amazon Linux AMI
CPU:Intel(R) Xeon(R) CPU E5-2676 v3 @ 2.40GHz
メモリ:1GB
mallocとメモリ書き込みを行うソースコード
20MB分malloc後に、1秒ごとに1MBずつmemsetでゼロクリアしています
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#define TOTAL 20
#define PAGE_SIZE 4096
#define PER_MB 1 * 1024 * 1024
int main()
{
char *arr[TOTAL]={NULL};
int cnt_malloc=0;
//20MB memory alloc
for (int i=0; i < TOTAL; i++)
{
arr[i] = (char *)malloc(PER_MB);
if (arr[i] == NULL)
{
printf("can't alloc\n");
break;
}
printf("memory address %d:%p\n", i, arr[i]);
cnt_malloc++;
}
sleep(3);
//write memory
for (int i=0;i < cnt_malloc;i++)
{
//1秒ごとに書き込み
memset(arr[i], 0, PER_MB);
printf("write memory %p\n", arr[i]);
sleep(1);
}
for (int i=0;i < TOTAL;i++)
{
free(arr[i]);
}
return 0;
}
vmstatを使用してメモリの使用量を監視します。
以下がそのスクリプトファイルです。
#!/bin/bash
#vmstatで1秒ごとのメモリ使用量監視
vmstat -S M 1 > page.log 2>&1 &
PID=$!
sleep 3
#mallocとmemsetを行う
./malloc_page
sleep 3
kill $PID
exit 0
以下に結果を示します。
[ec2-user@ip-10-10-1-233 ~]$ cat page.log
procs -----------memory---------- ---swap-- -----io---- --system-- -----cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
0 0 0 208 52 572 0 0 29 126 26 48 1 0 99 0 0
0 0 0 208 52 572 0 0 0 0 12 18 0 0 100 0 0
0 0 0 208 52 572 0 0 0 40 30 71 1 0 99 0 0
ここでmalloc
1 0 0 208 52 572 0 0 0 0 43 94 1 1 98 0 0
0 0 0 208 52 572 0 0 0 0 29 62 0 0 100 0 0
0 0 0 208 52 572 0 0 0 0 13 24 0 0 100 0 0
3秒スリープ
1 0 0 208 52 572 0 0 0 0 9 26 0 0 100 0 0
1秒ごとに1MBのmemset開始。合計20MB
0 0 0 207 52 572 0 0 0 28 26 72 0 0 100 0 0
0 0 0 206 52 572 0 0 0 0 10 17 0 0 100 0 0
0 0 0 205 52 572 0 0 0 0 11 18 0 0 100 0 0
0 0 0 203 52 572 0 0 0 0 39 86 1 0 99 0 0
0 0 0 203 52 572 0 0 0 52 43 120 1 0 99 0 0
0 0 0 203 52 572 0 0 0 0 12 19 0 0 100 0 0
0 0 0 202 52 572 0 0 0 0 10 17 0 0 100 0 0
0 0 0 200 52 572 0 0 0 0 9 19 0 0 100 0 0
0 0 0 199 52 572 0 0 0 0 15 30 0 0 100 0 0
0 0 0 199 52 572 0 0 0 0 10 18 0 0 100 0 0
0 0 0 197 52 572 0 0 0 12 12 22 0 0 100 0 0
0 0 0 195 52 572 0 0 0 0 36 75 1 0 99 0 0
0 0 0 195 52 572 0 0 0 0 37 96 0 0 100 0 0
0 0 0 195 52 572 0 0 0 16 16 27 1 0 99 0 0
0 0 0 194 52 572 0 0 0 0 12 18 0 0 100 0 0
0 0 0 192 52 572 0 0 0 0 9 18 0 0 100 0 0
0 0 0 191 52 572 0 0 0 40 11 23 0 0 99 1 0
0 0 0 191 52 572 0 0 0 0 10 18 0 0 100 0 0
0 0 0 189 52 572 0 0 0 0 16 26 0 0 100 0 0
0 0 0 187 52 572 0 0 0 0 35 79 0 1 99 0 0
20MBのmemset終了。free関数で解放。
0 0 0 208 52 572 0 0 0 0 39 102 0 0 100 0 0
0 0 0 208 52 572 0 0 0 0 7 12 0 0 100 0 0
0 0 0 208 52 572 0 0 0 24 8 15 0 0 100 0 0
vstatで出力された結果の4列目freeの項目に注目してください。
20MBのmalloc終了3秒後からmemsetで書き込みを始めています。
そこからメモリが1秒ごとに1MBずつ使用されているのが分かります。
結果を見るに、malloc時には仮想メモリのX番地からXバイト分予約だけ行い、
実際に書き込む際に初めて物理メモリにマッピングされるのだと思います。
今回の問題が発生したプログラムはマルチスレッド実行でしたが、物理メモリを確保する処理はカーネルがシリアルに、排他的に処理を行うため(この情報が正しいかどうかはあまり自信がないです)、余計にmemsetの際の実行時間が長く見えてしまった。
今回はmallocでページサイズ分だけメモリを割り当てる際のみ検証を行いました。
ヒープ領域から割り当てる場合にどのような動きをするのかは分かりません。
ちなみにmallocするサイズを128KB以下にするとヒープ領域からメモリを割り当てられます。
ヒープ領域からメモリを割り当てるかどうかは割り当てサイズに依存します。デフォルトの閾値は128KB、128KB以下ならヒープ領域を使用します。
環境変数M_MMAP_THRESHOLDの値を変えることで閾値を調整可能です。
(調査してモヤモヤは晴れましたが、この知識が何かに使えるかと言われれば微妙)