PHP實現Bitmap的探索 - GMP擴展使用

原文地址:https://blog.fanscore.cn/p/22/php

1、背景

公司當前有一個用戶羣的系統,核心功能是根據不一樣的條件組去不一樣的業務線中get符合條件的uid列表,而後存到redis中的bitmap中。linux

舉個🌰,若是一個用戶羣中有兩個用戶: 3和7,即[3,7],用bitmap表示那就是:00010001git

最後利用redis提供的bitOp命令: bitOp AND \ bitOp XOR \ bitOp OR對各個條件組對應的uid列表bitmap作交併差集計算,得出最終的用戶羣並存儲到redis bitmap中。github

2、問題

對於上面描述的系統,若是用戶羣人數較多的那咱們就須要執行較屢次的setBit {uid} 1命令,並且若是用戶羣中的第一個uid是一個特別大的值好比10億的話,就可能會一次malloc 1000000000/1024/1024/8 ~= 120M的內存,這可能會致使redis卡住一段時間,在高併發的redis實例上執行這個操做是至關危險的。並且能夠預想到對於兩個較大的bitmap key執行bitOp也是很是消耗CPU的,應該儘可能避免在存儲型的redis實例中作這種十分消耗CPU的計算操做。redis

3、解決方案

針對上述的問題,能夠將bitmap的計算挪到應用程序中來,只將最終統計出來的bitmap存儲到redis中便可。
  若是最終結果用戶羣中的第一個uid是一個特別大的值的話,能夠先set 1K再設置2K..3K...這樣緩存的增長bitmap的大小避免redis卡住。數組

4、PHP實現Bitmap

因爲該系統目前是使用的PHP,因此下面記錄下PHP實現Bitmap的」心路歷程「。緩存

因爲要操做PHP變量的某一位,因此就要藉助位運算來實現,可是又因爲PHP的位運算只能做用在整型數上,因此咱們沒法使用字符串或者浮點數來實現,因此最早考慮的就是使用整型數組來實現。併發

爲何是數組呢?由於在64位機器上一個整型變量最多隻能使用64位,又因爲PHP的整型是有符號的,因此最高位沒法供咱們使用,因此一個整型變量能存儲的最大的uid就是63,這真是太雞肋了-_-||,因此只能搞個用多個整型變量了實現了。函數

OK,到此爲止貌似找到一個看起來不錯的解決方案。可是咱們再思考這樣一個問題:假設咱們系統中最大的uid是63x100萬=3.6千萬(對主流互聯網公司來講這很正常吧😸),那爲了存儲全部uid,咱們須要1百萬個整數才行,即咱們須要一個擁有1百萬個元素的數組,那麼若是我在進程中製造了一個這樣的數組會佔用多少內存呢?會是64 * 1百萬 / 1024 / 1024 / 8 ~= 7.6M嗎?答案是否認的,由於php數組是由HashTable實現的,這是一個複雜的結構體,除了數組元素佔用的內存外,還有其餘的佔用。(這裏先不作展開,有興趣能夠自行查看下php數組的實現)
眼見爲實:高併發

<?php
ini_set('memory_limit','4G');
$arr = [];
for ($i = 0; $i < 64 * 1000000; $i++)
{
    $arr[] = PHP_INT_MAX;
}

echo "done\n";
while(1){
}

查看內存佔用

image.png

能夠看到大概是1.5G,比咱們上面預計的大的多,這太可怕了,必須優化下咱們的內存佔用,才能真正在生產環境中使用。

這裏須要提一句,個人機器只有8G,因此程序可能會用到swap分區,而ps命令結果中的RSS不統計swap分區的佔用,在我實際實現中發現ps結果中RSS一列顯示佔用的內存會隨着時間慢慢減小,可是個人程序中arr變量佔用的內存是不可能被回收的,因此推測是物理內存中佔用的部份內存被置換到了swap分區中。若是你要進行這個實驗的話建議關閉swap分區,這樣你能獲得一個更準確的結果。

5、繼續優化

基於上面的經驗,若是咱們要佔用盡量小的內存,那咱們必須可以操做一段近乎無限長的內存且不能產生其餘額外佔用才能夠。幸運的是PHP給咱們提供了這樣一個擴展:GMP,這個擴展可讓咱們使用一個任意長度的整數。OK如今咱們擁有了得到一塊連續的內存而不會產生其餘額外佔用的手段,再寫一段代碼使用下並驗證下內存佔用狀況:

<?php

$gmp = gmp_init(0);
gmp_setbit($gmp, 64 * 1000000, true);
echo "done\n";
while(1){}

image.png

Awesome,此次只使用了15M的內存。更加興奮的是這個擴展提供了諸如:gmp_andgmp_orgmp_xor這樣進行位運算的函數,極大的方便了咱們的使用。

到此爲止咱們彷佛找到了一個完美的解決方案,可是真的完美嗎?No!其實還能夠再優化一下,想象下若是咱們有一個用戶羣,裏面只有一個uid:64000000(表示爲數組的話就是:[64000000]),爲了存儲這個用戶咱們須要佔用7.6M內存,而這個用戶羣中僅僅只有一個元素,這真是極大的浪費啊!

爲了優化這個問題能夠擁抱上面被咱們唾棄的數組😸,一個大的bitmap拆分爲一個個小bitmap的數組,這一個個小的bitmap咱們限制大小爲1Kw位。
image.png

回到上面的問題,若是咱們要存儲[64000000]這個用戶羣的話只須要在數組的第6個元素中設置一個little bitmap: 1便可。這樣咱們就由一開始的佔用7.6M內存優化爲了佔用1位內存。

OK,到此爲止咱們找到一個還不錯的解決方案😸。

後言

爲了在Mac中安裝GMP擴展又耗費了不少時間,固然,這又是另一個故事了。有時間我會分享Mac中安裝GMP擴展的過程當中我遇到的問題。

參考資料

  1. GNU Multiple Precision
  2. Process Memory Management in Linux
  3. 從源碼看 PHP 7 數組的實現
相關文章
相關標籤/搜索