[PHP 原始碼] EXPECTED 和 UNEXPECTED 到底是啥?

zhm1992發表於2020-04-06

背景

我們經常能在php或者swoole原始碼看到裡看到EXPECTED/UNEXPECTED/likely/unlikely這樣的巨集,比如說下面的情景:

// zend/zend_alloc.c/zend_mm_alloc_small
// small規格記憶體快取在free_slot連結串列中
if (EXPECTED(heap->free_slot[bin_num] != NULL)) { 
    zend_mm_free_slot *p = heap->free_slot[bin_num];
    heap->free_slot[bin_num] = p->next_free_slot;
    return (void*)p;
} else {
    return zend_mm_alloc_small_slow(heap, bin_num ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
}

這段程式碼是用來獲取small規格的記憶體,如果在free_slot上沒獲取到,那麼再去上一級”緩慢”的獲取,轉化為我們業務程式碼,有點類似於從資料庫獲取資料,以下是虛擬碼:

if (EXPECTED($userInfo = $redis->get($userId))) {
    return $userInfo;
} else {
    $userInfo = $db->query("select * from user where id = {$userId}");
    $redis->set($userId, $userInfo);
    return $userInfo;
}

我們在平時業務中,經常會寫類似這樣的程式碼:資料從資料庫獲取一次之後就會被放入快取,以後每次讀取都是從快取中獲取。其實就是說:每次執行這段程式碼,大概率是走從快取獲取資料的分支。

這就是EXPECTED的語意,UNEXPECTED則相反。

解釋

我們首先來看一下這個巨集展開是什麼

# define EXPECTED(condition)   __builtin_expect(!!(condition), 1)
# define UNEXPECTED(condition) __builtin_expect(!!(condition), 0)

__builtin_expect 是GCC編譯器的一個內建巨集,原型是

long __builtin_expect(long exp, long c);

這個函式的用法和解釋是:

1. 這個函式的返回值就是exp
2. 告訴編譯器期望exp等於c

舉個例子:

// 不期望執行foo函式
if (__builtin_expect(x, 0)) {
    foo();
}
// 期望執行bar函式
if (__builtin_expect(y, 1)) {
    bar();
}

那麼這個巨集使用之後有什麼效果呢?我們先從快取層級開始說起

4401585397941_.pic.jpg

每次訪問資料,cpu都會從最頂層開始獲取,這層沒有再去下一層獲取,獲取到資料又在這一層快取起來,每一層都作為它下一層的快取,這樣一層一層向下訪問。造成這樣的結果是有原因的:離CPU越近的儲存器,速度越快,每位元組的成本越高,同時容量也因此越小。暫存器速度最快,離CPU最近,成本最高,大小非常有限,其次是快取記憶體(快取也是分級,有L1,L2等快取),再次是主存(普通記憶體),再次是本地磁碟。

暫存器和cache的速度一般是記憶體的幾十倍甚至上百倍,如果我們能夠有效的利用這個快取來為我們程式加速,可能會帶來很大提升。

如何使用好cache呢?一般遵循時間區域性性和空間區域性性兩個原則

  1. 時間區域性性:被引用過一次的儲存器位置在未來會被多次引用(通常在迴圈中)
  2. 空間區域性性:如果一個儲存器的位置被引用,那麼將來他附近的位置也會被引用

出於空間區域性性考慮,作業系統會在獲取資料時選擇快取資料附近位置的資料,典型的例子是讀磁碟,每次都是讀取一個連續的頁。當然,指令也不例外。試想:如果程式碼沒有任何跳轉,是從上到下一條條指令執行的話,快取親和性是比較高的,但如果出現了跳轉,就可能導致預快取的指令根本用不上,又要從記憶體讀一遍,執行一條指令只需要一個時鐘週期,但是讀記憶體卻需要幾十個時鐘週期,如果每次載入指令都需要從記憶體讀的話,這效率也太慢了,大部分時間都是在等記憶體。

如果有一種情況,我們已經知道了程式碼執行時各個分支的執行情況,事先就按這個順序把指令安排好位置,那麼我們是不是就可以避免cache miss的尷尬了?

__builtin_expect 的產生也是因為這種情況,因為有些分支我們是事先知道他的執行概率的,比如說

if (malloc(sizeof(int)) == NULL) {
    // error handle
}

再比如說引數檢查,像這種出錯情況本身就很少,如果還把出錯處理的指令安排在緊接著當前指令之後的話,會增加沒必要的跳轉,快取親和性會很差,白白浪費了寶貴的cache。

總結

對於一些程式執行前我們就知道的非常有可能執行或者不執行的分支,我們可以用__builtin_expect 來優化,增加快取親和性。其實提升快取親和性一直以來都是非常重要的一種優化手段,它對於cpu密集型程式可能會有非常大的提升,php7比起php5在這方面就做了非常多的優化,比如說hashTable結構的改變,從原來的大量隨機記憶體IO,變成了順序結構,訪問的記憶體空間都是連續的,執行效率提升非常多。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章