作者:李德
small內存分配計算bin_num
在PHP源碼中,有一段對small內存規格的計算,具體在Zend/zend_alloc.c的zend_mm_small_size_to_bin函數中,其目的是傳入一個size,計算對應的規格。見代碼:
if (size <= 64) {
/* we need to support size == 0 ... */
return (size - !!size) >> 3;
} else {
t1 = size - 1;
t2 = zend_mm_small_size_to_bit(t1) - 3;
t1 = t1 >> t2;
t2 = t2 - 3;
t2 = t2 << 2;
return (int)(t1 + t2);
}
可以看出,這段代碼中分爲兩種情況進行討論:
- 1、size小於等於64的情況;
- 2、size大於64的情況;
下面我們對這兩種情況詳細分析下。
對於size小於等於64的情況
- 看
ZEND_MM_BINS_INFO
這個宏知道當size小於等於64的情況是一個等差數列,遞增8,所以使用size除以8就行(源碼中是右移3位)size >> 3
- 但是要考慮到size等於8、16等的情況,所以爲
(size - 1) >> 3
- 然後要考慮到爲0的情況,所以源碼中對於
-1
的處理是!!size
,當size爲0的情況!!0 = 0
。所以當size爲0的情況就把-1
轉換成了-0
,最終有了源碼中的表達式(size - !!size) >> 3
對於size大於64的情況
t1 = size - 1;
t2 = zend_mm_small_size_to_bit(t1) - 3;
t1 = t1 >> t2;
t2 = t2 - 3;
t2 = t2 << 2;
return (int)(t1 + t2);
初始懵逼
- 初看這個代碼,容易一臉懵逼,這些t1 t2 都是啥啊
- 不過不用怕,我們一點點來分析
步驟分析
/* num, size, count, pages */
#define ZEND_MM_BINS_INFO(_, x, y) \
_( 0, 8, 512, 1, x, y) \
_( 1, 16, 256, 1, x, y) \
_( 2, 24, 170, 1, x, y) \
_( 3, 32, 128, 1, x, y) \
_( 4, 40, 102, 1, x, y) \
_( 5, 48, 85, 1, x, y) \
_( 6, 56, 73, 1, x, y) \
_( 7, 64, 64, 1, x, y) \
_( 8, 80, 51, 1, x, y) \
_( 9, 96, 42, 1, x, y) \
_(10, 112, 36, 1, x, y) \
_(11, 128, 32, 1, x, y) \
_(12, 160, 25, 1, x, y) \
_(13, 192, 21, 1, x, y) \
_(14, 224, 18, 1, x, y) \
_(15, 256, 16, 1, x, y) \
_(16, 320, 64, 5, x, y) \
_(17, 384, 32, 3, x, y) \
_(18, 448, 9, 1, x, y) \
_(19, 512, 8, 1, x, y) \
_(20, 640, 32, 5, x, y) \
_(21, 768, 16, 3, x, y) \
_(22, 896, 9, 2, x, y) \
_(23, 1024, 8, 2, x, y) \
_(24, 1280, 16, 5, x, y) \
_(25, 1536, 8, 3, x, y) \
_(26, 1792, 16, 7, x, y) \
_(27, 2048, 8, 4, x, y) \
_(28, 2560, 8, 5, x, y) \
_(29, 3072, 4, 3, x, y)
#endif /* ZEND_ALLOC_SIZES_H */
-
size = size - 1;
這個是邊界情況,跟前面一樣,後面出現的size暫且都認爲已近減一了 - 假設不看這個源碼,我們要實現在
ZEND_MM_BINS_INFO
中找到對應的bin_num - 由
ZEND_MM_BINS_INFO
得知後續的增加4個爲一組,分別爲
2^4, 2^5, 2^6...
-
有了這個分組信息的話,我們要找siez對應的bin_num
- 找到這個size屬於哪一組
- 並且size在組內的偏移是多少
- 計算組的起始位置
- 那現在問題轉換成了上面3個小問題,我們一個一個來解決
找到size屬於哪一組
- 最簡單的辦法就是比大小是吧,可以使用if...else 來一個一個比,但是顯然php源碼不是這樣乾的,那我們還有什麼其它的辦法呢?
- 我們看十進制看不出來什麼名堂,就把這些值轉成二進制看看吧
64 | 100 0000
80 | 101 0000
96 | 110 0000
112 | 111 0000
128 | 1000 0000
160 | 1010 0000
192 | 1100 0000
224 | 1110 0000
256 | 1 0000 0000
320 | 1 0100 0000
384 | 1 1000 0000
448 | 1 1100 0000
.....
- 我們看下上面的二進制,會發現每組的內的二進制長度相等,並且後面每個都比前面多一位
- 那就是說我們可以計算二進制的長度來決定它的分組,那麼二進制的長度又是啥呢,其實就是當前二進制的最高位爲
1
的位數 - 那麼問題又轉換成了求二進制中最高位的
1
的位數 - 下面給出php源碼的解法,這裏暫時不對其解析,只要知道它返回的是二進制中最高位的
1
的位數
int n = 16;
if (size <= 0x00ff) {n -= 8; size = size << 8;}
if (size <= 0x0fff) {n -= 4; size = size << 4;}
if (size <= 0x3fff) {n -= 2; size = size << 2;}
if (size <= 0x7fff) {n -= 1;}
return n;
- 假設我們申請的size爲65,那麼這裏的n返回7
計算size在組內的偏移量
- 這個簡單,直接用size減去每組的起始siez大小然後除以當前組內的差值(16、32、64...)即可,也就是
(size-64)/16 (size-128)/32 (size-256)/64
- 現在來看看上一步中的返回的值,每個組分別是
7、8、9...
,那麼我們現在來看看這樣的數據怎麼計算組內的偏移量
(size - 2^4 * 4) / 16 = size / 2^4 - 4
(size - 2^5 * 4) / 32 = size / 2^5 - 4
(size - 2^6 * 4) / 64 = szie / 2^6 - 4
- 那是不是可以用
7、8、9
減去3
得到4、5、6
,這樣我們就可以根據它在哪一組的信息得到當前組的差值(16、32、64...) - 當size爲65時,偏移量是不是就是
(64-64) / 2^4 = 0
計算組的起始位置
- 現在我們有了偏移量的信息,假定我們分組是1、2、3
- 那是不是就是用最高位的
1
的位數減去6
就可以得到分組信息了 - 得到分組信息之後,怎麼知道每組的起始位置呢
- 我們知道起始位置分別是
8、12、16...
它也是一個等差數列,就是4n+4
-
我們在看看size=65的那個例子
- 計算的偏移量是0
- 計算的起始位置是
4*1 + 4 = 8
- 所以當size=65的bin_num就是起始位置加上偏移量
8 + 0 = 8
-
我們再看一個size=129的例子
-
偏移量是
- 二進制中最高位的
1
的位數爲8 - 然後用8減去3得到5
(129 - 1 - 32 * 4) / 64 = 0
- 二進制中最高位的
- 計算起始位置是
4 * 2 + 4 = 12
- 兩者相加就是
12 + 0 = 0
-
-
size=193
-
偏移量是
- 二進制中最高位的
1
的位數爲8 (193 - 1 - 32 * 4) / 64 = 2
- 二進制中最高位的
- 計算起始位置是
4 * 2 + 4 = 12
- 兩者相加就是
12 + 2 = 14
-
-
size=1793
-
偏移量是
- 二進制中最高位的
1
的位數爲11 (1793 - 1 - 256 * 4) / 256 = 3
- 二進制中最高位的
- 計算起始位置是
4 * 5 + 4 = 24
- 兩者相加就是
24 + 3 = 27
-
代碼分析
php實現代碼
1 t1 = size - 1;
2 t2 = zend_mm_small_size_to_bit(t1) - 3;
3 t1 = t1 >> t2;
4 t2 = t2 - 3;
5 t2 = t2 << 2;
6 return (int)(t1 + t2);
第一行
t1 = size - 1;
- 是爲了考慮size爲64、128...這些邊界情況
第二行
t2 = zend_mm_small_size_to_bit(t1) - 3;
- 這裏調用了
zend_mm_small_size_to_bit
這個函數,我們看看這個函數
/* higher set bit number (0->N/A, 1->1, 2->2, 4->3, 8->4, 127->7, 128->8 etc) */
int n = 16;
if (size <= 0x00ff) {n -= 8; size = size << 8;}
if (size <= 0x0fff) {n -= 4; size = size << 4;}
if (size <= 0x3fff) {n -= 2; size = size << 2;}
if (size <= 0x7fff) {n -= 1;}
return n;
- 看註釋我們就知道這個函數是用來返回當前size二進制中最高位1的位數,具體的做法呢其實就是二分法
-
我們通過
zend_mm_small_size_to_bit
這個函數獲取了size二進制中最高位1的位數,那麼這個-3
是什麼神奇的操作呢- 上問的分析中提到,我們計算size在組內的偏移量的公式
(size - 2^4 * 4) / 16 = size / 2^4 - 4 (size - 2^5 * 4) / 32 = size / 2^5 - 4 (size - 2^6 * 4) / 64 = szie / 2^6 - 4
- 這裏獲取二進制的位數是7、8、9...通過
-3
的操作來獲取相應的 4、5、6...
第三行
t1 = t1 >> t2;
- 把t1右移t2位,這又是什麼神奇的操作?
- 這裏我們把最後計算bin_num的數學公式給寫出來,它是等於每組的起始位置加上組內的偏移量
binnum = (4n + 4) + (size / 2^n - 4)
binnum = 4n + size / 2^n
- 所以第三行的意思我們就知道了,就是size右移2^n次方爲
第四行
t2 = t2 - 3;
- 這個好理解,可以參照上文得到每組的起始位置的方法
第五行
t2 = t2 << 2;
- 我們再看看bin_num的計算公式
binnum = (4n + 4) + (size / 2^n - 4)
binnum = 4n + size / 2^n
- 那麼這行就好理解了,就是計算每組的起始位置
4n
對吧,左移兩位就是乘以4
第六行
return (int)(t1 + t2);
- 這行沒啥說的,就是返回了一個int類型的bin_num