C GNU GCC malloc + memset Optimization

最近踩到 compiler optimization 的一個小坑,gcc5 版本以上,開啟 optimization level 2 時,會將 malloc + memeset 轉換成 calloc。看起來似乎蠻有道理的改善,卻因為我在進行客製化 malloc 時,沒有這注意到這點,導致最後程式執行的是 libc 的 calloc 而不是我的 wrap_malloc。

起因

使用 void *__wrap_malloc(size_t size) 將 malloc function 進行額外的處理 (How to wrap a system call (libc function) in Linux),其中一段 code 為:

1void *ptr = malloc(SIZE);
2if (ptr)
3    memset(ptr, 0x0, SIZE);

但實際上執行時卻不如預期執行到 malloc,dump code 之後發現這段 code 被調整成 calloc。

 1// gcc version 9.4.0
 2// gcc -O0
 3mov	x0, #SIZE
 4bl	malloc
 5str	x0, [sp, 24]
 6ldr	x0, [sp, 24]
 7cmp	x0, 0
 8beq	.L2
 9mov	x2, #SIZE
10mov	w1, 0
11ldr	x0, [sp, 24]
12bl	memset
1// gcc -O2
2mov	x1, 1
3mov	x0, #SIZE
4b	calloc

malloc + memset Optimization

GCC commit: PR tree-optimization/57742 (memset(malloc(n),0,n) -> calloc(n,1))

calloc 看起來結果跟 malloc + memset to zero 一樣,但是根據實作差異,calloc 在某些情況下,效率會較 malloc + memset 好。

舉例來說,在 allocate 指定大小的記憶體空間時,如果這個記憶體空間所對應的 page 是當前透過 mmap 來的,這表示此 page 對應的空間已被清空歸零,因此不需要再逐一清空。

當 allocator 剩餘空間不足以分配指定的記憶體空間,會透過 sysmalloc 的方式,使用 mmap 向系統新申請以 page 為單位的空間,而在呼叫 mmap 時,會帶有 MAP_ANONYMOUS 的 flag,這也意味著 page 所有的內容會被初始化歸零。基於安全考量,如果取得沒有初始化的 page ,會導致其他使用此 physical memory 的 process 處理的資料被取得,因此一般情況下都會被初始化,不過如果因為其他因素而不需要初始化,可以透過 CONFIG_MMAP_ALLOW_UNINITIALIZED kernel option 來設定)

除此之外,calloc 在將記憶體初始化歸零時,也可以透過實作來進行效能改善,以 libc 所實作的 calloc 為例:

 1 /* Unroll clear of <= 36 bytes (72 if 8byte sizes).  We know that
 2 contents have an odd number of INTERNAL_SIZE_T-sized words;
 3 minimally 3.  */
 4d = (INTERNAL_SIZE_T *) mem;
 5clearsize = csz - SIZE_SZ;
 6nclears = clearsize / sizeof (INTERNAL_SIZE_T);
 7assert (nclears >= 3);
 8
 9if (nclears > 9)
10return memset (d, 0, clearsize);
11
12else
13{
14  *(d + 0) = 0;
15  *(d + 1) = 0;
16  *(d + 2) = 0;
17  if (nclears > 4)
18    {
19      *(d + 3) = 0;
20      *(d + 4) = 0;
21      if (nclears > 6)
22        {
23          *(d + 5) = 0;
24          *(d + 6) = 0;
25          if (nclears > 8)
26            {
27              *(d + 7) = 0;
28              *(d + 8) = 0;
29            }
30        }
31    }
32}

透過 loop unrolling 的手法來初始化小於 36 bytes 的記憶體空間。

Issues

在查找相關資料的時候,看到一個有趣的討論 Bug 67618 - malloc+memset optimization breaks code,由於此改善,導致有人抱怨 code 出現問題,主要原因是:

1void *ptr = malloc(SIZE);
2if (ptr)
3    memset(ptr, 0x0, SIZE);

即使有 if 條件判斷的情況下,一樣會被調整成 calloc。不過 committer 表示,malloc 本意是分配一個可用的空間,其內容是初始化或是非初始化,都不應影響 code 運行。