PHP數組的Hash衝突執行個體,你知道不知道, 插入65536個經過構造的索引值的元素到PHP數組, 會需要耗時30秒以上? 而一般的這個過程僅僅需要0.1秒..
請看如下的例子:
$size = pow(2, 16);
$startTime = microtime(true);
$array = array();
for ($key = 0, $maxKey = ($size - 1) * $size; $key <= $maxKey; $key += $size) { $array[$key] = 0;
}$endTime = microtime(true);
echo '插入 ', $size, ' 個惡意的元素需要 ', $endTime - $startTime, ' 秒', "\n"; $startTime = microtime(true);
$array = array();
for ($key = 0, $maxKey = $size - 1;
$key <= $maxKey; ++$key) { $array[$key] = 0;}$endTime = microtime(true);
echo '插入 ', $size, ' 個普通元素需要 ', $endTime - $startTime, ' 秒', "\n";
上面的例子, 在我的機器上的執行結果如下:
插入 65536 個惡意的元素需要 43.1438360214 秒插入 65536 個普通元素需要 0.0210378170013 秒
這個差別是不是很誇張?!
我在上一篇文章中介紹過, 經過特殊構造的索引值, 使得PHP每一次插入都會造成Hash衝突, 從而使得PHP中array的底層Hash表退化成鏈表:
Hash collision
這樣在每次插入的時候PHP都需要遍曆一遍這個鏈表, 大家可以想象, 第一次插入, 需要遍曆0個元素, 第二次是1個, 第三次是3個, 第65536個是65535個, 那麼總共就需要65534*65535/2=2147385345次遍曆….
那麼, 這個索引值是怎麼構造的呢?
在PHP中,如果索引值是數字, 那麼Hash的時候就是數字本身, 一般的時候都是, index & tableMask. 而tableMask是用來保證數字索引不會超出數組可容納的元素個數值, 也就是數組個數-1.
PHP的Hashtable的大小都是2的指數, 比如如果你存入10個元素的數組, 那麼數組實際大小是16, 如果存入20個, 則實際大小為32, 而63個話, 實際大小為64. 當你的存入的元素個數大於了數組目前的最多元素個數的時候, PHP會對這個數組進行擴容, 並且從新Hash.
現在, 我們假設要存入64個元素(中間可能會經過擴容, 但是我們只需要知道, 最後的數組大小是64, 並且對應的tableMask為63:0111111), 那麼如果第一次我們存入的元素的索引值為0, 則hash後的值為0, 第二次我們存入64, hash(1000000 & 0111111)的值也為0, 第三次我們用128, 第四次用192… 就可以使得底層的PHP數組把所有的元素都Hash到0號bucket上, 從而使得Hash表退化成鏈表了.
當然, 如果索引值是字串的話, 就稍微比較麻煩一些了, 但是PHP的Hash演算法是開源的, 已知的, 所以有心人也可以做到…
本文地址: http://www.laruence.com/2011/12/30/2435.html