起因
V2EX看到这帖子,平时是没试过这么玩,出于好奇翻了一下源码

分析
[] > 123; 为例

①略过词法分析语法分析AST生成,可以看到 op1 > op2 的操作等同于 op2 < op1,调用函数 is_smaller_function(result, op2, op1)


②接着调用 compare_function, 这里的 op1 == 123,op2 == []


③ compare_function 其实就是各种类型的比较判断,引用类型转换继而比较blablabla,省略这些代码,因为当我们 op2 == IS_ARRAY 的时候都不成立,最终到达上图的判断,可以看到op1的值已经被忽略,所以无论是 123 还是 PHP_MAX_INT,result = -1 接着返回成功。回到图2,比较结果变成了 result = -1 < 0; 恒为 true。

结论
数组 > 任意数字类型 == true; 其他类型的比较也在compare_function找到,有兴趣的翻翻吧。

参考

  • PHP7.2-SRC

TL;DR
数组初始化容量为8,最大是0x80000000,每次扩容为原容量的2倍。

扩容
数组插入新元素的时候会调用ZEND_HASH_IF_FULL_DO_RESIZE判断已使用容量是否大于等于数组容量(ht->nNumUsed >= ht->nTableSize);
条件成立再判断已使用是否大于有效元素加上有效元素除以32(ht->nNumUsed > ht->nNumOfElements + (ht->nNumOfElements » 5)),符合这条件的先进行rehash整理,不符合条件的判断数组容量是否小于HT_MAX_SIZE(0x80000000);
如果是则容量扩大至原来的2倍,uint32_t nSize = ht->nTableSize + ht->nTableSize; 在内存池申请 nSize* sizeof(Bucket) + nSize* sizeof(uint32_t)大小的新数据地址;
设置新的nTableSize和nTableMask,修改ht->arData指向新地址,把旧Bucket数据拷贝到新地址,释放旧地址内存,执行zend_hash_rehash

rehash
首先调用HT_HASH_RESET重置所有索引为-1;
如果数组所有元素都是有效元素则按arData数组顺序重新分配索引,原索引存在元素值zval.u2.next;
否则循环遍历bucket至nNumUsed,去除IS_UNDEF的元素,更新nNumUsed。

参考

  • 《PHP 7底层设计与源码实现》
  • PHP7.2-SRC

上次提到PHP7的数组分为packed array 和 hash array,区别在于packed array取值不需要通过slot获得arData的下标,它的key对应的就是下标,所以取值速度上更有优势和比较省内存。

packed array要满足的条件是key必须是递增的正整型。

<?php
$arr = [1, 2, 3]; // 是
$arr = [ 1 => 1, 3 => 3]; // 是(跨下标的有例外)
$arr = [-1 => 1, 0 => 2]; // 不是
$arr = [2 => 1, 1 => 2]; // 不是

转换为packed array的操作
shuffle,array_keys等函数最终都会调用 zend_hash_to_packed 转换为packed array。

转换为hash array的操作

  1. 往packed array添加string key的操作
  2. 添加的key是整型但是之前这个key被unset过(Z_TYPE == IS_UNDEF 状态)
  3. 添加的key 大于nTableSize(数组容量)并且 (key » 1) > nTableSize   (nTableSize » 1) > nNumOfElements 这里😂太拗口我就直接贴代码好了

关于②PHP数组unset操作并不会立马删除指定的bucket,而且把它标识为IS_UNDEF的状态,等待rehash或者扩容的时候再处理(这是后话,有机会再填坑吧)。
比如有一个packed array

<?php
$arr = [1, 2, 3];
unset($arr[1]);
$arr[1] = 4; 
// output
$arr == [0 => 1, 2 => 3, 1 => 4]

这时候就已经转化成了hash array

关于③先要各个字段之间的关系是,数组容量(nTableSize,初始化数组容量是8) = 已使用(nNumUsed) + 未使用, 已使用(nNumUsed) = 有效(nNumOfElements,我们count数组得到的就是这个大小) + 无效(unset的) 。结合上面的描述得到 $arr = [0 => 1, 8 => 2]; 是一个hash array。

打印出这时候的HashTable可以看到,u.flag = 26 & HASH_FLAG_PACKED(4) == 0 已经不是packed array了,虽然我们第二个元素下标是8超过了容量nTableSize,但是这时候并没有扩容,而是转为hash array使得内存使用边得更紧凑。

参考:

  • 《PHP 7底层设计与源码实现》
  • PHP7.2-SRC
  • nikic 博客

PHP的数组是一个 有序字典 ,特性是有序、可通过键查找。 为了两个特性,PHP5通过两个双向链实现,一个双链解决全局有序,一个双链解决键hash冲突。 简单概括PHP7这方面的改进就是抛弃链结构改为数组索引。

下图是初始化的空packed array结构(packed array 和 hash array以后有机会在写吧)

packed array 

这里有三个概念HashTable, solt, bucket。HashTable数组本体,slot存储索引,bucket实际数据的存储结构。图中的 *arData 指向的是一段连续的内存,起点位置是bucket的开始地址,往前(负索引)是slot的地址,这块内存同时存放了solt和bucket结构。

取值的流程是:key通过hash算法再和掩码nTableMask(负数的nTableSize)做位或运算得到slot的索引,slot取出bucket数组的下标,通过下标就能定位到对应的bucket。 (上图slot值为-1是因为packed array没有使用slot)

如何保证有序性?bucket数组本身是有序,添加数据的时候也是按照顺序加入。如何解决hash冲突?当冲突的时候slot记录新的下标值,新加入的bucket val.u2.next 记录了上一个bucket的下标。

参考:《PHP 7底层设计与源码实现》

之前吐槽过PHP为什么没__compare魔术方法《PHP __compare?》,可能开发组觉得没有必要吧,毕竟对象默认的比较一般情况已经够用了。 于是乎怀着no zuo no die心情尝试去实现一下,发现难度比预想要小。但由于拖延症的原因这篇文拖到现在才写,还有一方面就是修改的地方比较多和杂乱。

先看看效果吧!

<?php
//默认情况
class Foo
{
    private $v = [];

    public function __construct(array $v) {
        $this->v = $v;
    }
}

$o1 = new Foo([1, 2, 3]);
$o2 = new Foo([2, 1, 4]);
var_dump($o1 > $o2);
/* output */
bool(false)

//添加 __compare
class Foo
{
    private $v = [];

    public function __construct(array $v) {
        $this->v = $v;
    }

    public function __compare($o) {
        return $this->v[1] > $o->v[1];
    }
}

$o1 = new Foo([1, 2, 3]);
$o2 = new Foo([2, 1, 4]);
var_dump($o1 > $o2);
/* output */
bool(true)

可以看出,$o1, $o2的比较行为已经被__compare改变

Read more...