天天看點

二十二、PHP核心探索:PHP雜湊演算法設計 ☞ PHP中的Hash算法

Hash Table是PHP的核心,這話一點都不過分。PHP的數組、關聯數組、對象屬性、函數表、符号表等等都是用HashTable來做為容器的。

PHP的HashTable采用的拉鍊法來解決沖突,這個自不用多說,我今天主要關注的就是PHP的Hash算法,和這個算法本身透露出來的一些思想。

PHP的Hash采用的是目前最為普遍的DJBX33A (Daniel J. Bernstein, Times 33 with Addition),這個算法被廣泛運用與多個軟體項目,Apache、Perl和Berkeley DB等。對于字元串而言這是目前所知道的最好的雜湊演算法,原因在于該算法的速度非常快,而且分類非常好(沖突小,分布均勻)。

算法的核心思想就是:

hash(i) = hash(i-1) * 33 + str[i]

在zend_hash.h中,我們可以找到在PHP中的這個算法:

static inline ulong zend_inline_hash_func(char *arKey, uint nKeyLength)
{
    register ulong hash = 5381;
 
    /* variant with the hash unrolled eight times */
    for (; nKeyLength >= 8; nKeyLength -= 8) {
        hash = ((hash << 5) + hash) + *arKey++;
        hash = ((hash << 5) + hash) + *arKey++;
        hash = ((hash << 5) + hash) + *arKey++;
        hash = ((hash << 5) + hash) + *arKey++;
        hash = ((hash << 5) + hash) + *arKey++;
        hash = ((hash << 5) + hash) + *arKey++;
        hash = ((hash << 5) + hash) + *arKey++;
        hash = ((hash << 5) + hash) + *arKey++;
    }
    switch (nKeyLength) {
        case 7: hash = ((hash << 5) + hash) + *arKey++; /* fallthrough... */
        case 6: hash = ((hash << 5) + hash) + *arKey++; /* fallthrough... */
        case 5: hash = ((hash << 5) + hash) + *arKey++; /* fallthrough... */
        case 4: hash = ((hash << 5) + hash) + *arKey++; /* fallthrough... */
        case 3: hash = ((hash << 5) + hash) + *arKey++; /* fallthrough... */
        case 2: hash = ((hash << 5) + hash) + *arKey++; /* fallthrough... */
        case 1: hash = ((hash << 5) + hash) + *arKey++; break;
        case 0: break;
EMPTY_SWITCH_DEFAULT_CASE()
    }
    return hash;
}      

相比在Apache和Perl中直接采用的經典Times 33算法:

hashing function used in Perl 5.005:
  # Return the hashed value of a string: $hash = perlhash("key")
  # (Defined by the PERL_HASH macro in hv.h)
  sub perlhash
  {
      $hash = 0;
      foreach (split //, shift) {
          $hash = $hash*33 + ord($_);
      }
      return $hash;
  }      

在PHP的hash算法中,我們可以看出很處細緻的不同。首先,最不一樣的就是,PHP中并沒有使用直接乘33,而是采用了:

hash << 5 + hash      

這樣當然會比用乘快了。

然後,特别要主意的就是使用的unrolled,我前幾天看過一篇文章講Discuz的緩存機制,其中就有一條說是Discuz會根據文章的熱度不同采用不同的緩存政策,根據使用者習慣,而隻緩存文章的第一頁(因為很少有人會翻文章)。

于此類似的思想,PHP鼓勵8位一下的字元索引,他以8為機關使用unrolled來提高效率,這不得不說也是個很細節的,很細緻的地方。

另外還有inline,register變量 … 可以看出PHP的開發者在hash的優化上也是煞費苦心。

最後就是,hash的初始值設定成了5381,相比在Apache中的times算法和Perl中的Hash算法(都采用初始hash為0),為什麼選5381呢?具體的原因我也不知道,但是我發現了5381的一些特性:

Magic Constant 5381:
  1. odd number
  2. prime number
  3. deficient number
  4. 001/010/100/000/101 b      
DJBX33A (Daniel J. Bernstein, Times 33 with Addition)
 
  This is Daniel J. Bernstein's popular `times 33' hash function as
  posted by him years ago on comp.lang.c. It basically uses a function
  like ``hash(i) = hash(i-1) * 33 + str[i]''. This is one of the best
  known hash functions for strings. Because it is both computed very
  fast and distributes very well.
 
  The magic of number 33, i.e. why it works better than many other
  constants, prime or not, has never been adequately explained by
  anyone. So I try an explanation: if one experimentally tests all
  multipliers between 1 and 256 (as RSE did now) one detects that even
  numbers are not useable at all. The remaining 128 odd numbers
  (except for the number 1) work more or less all equally well. They
  all distribute in an acceptable way and this way fill a hash table
  with an average percent of approx. 86%.
 
  If one compares the Chi^2 values of the variants, the number 33 not
  even has the best value. But the number 33 and a few other equally
  good numbers like 17, 31, 63, 127 and 129 have nevertheless a great
  advantage to the remaining numbers in the large set of possible
  multipliers: their multiply operation can be replaced by a faster
  operation based on just one shift plus either a single addition
  or subtraction operation. And because a hash function has to both
  distribute good _and_ has to be very fast to compute, those few
  numbers should be preferred and seems to be the reason why Daniel J.
  Bernstein also preferred it.
 
                   -- Ralf S. Engelschall <[email protected]>