常规 PHP 数组和 SplFixedArray 之间的性能比较

我们在上一节中遇到的一个关键问题是:为什么要使用 SplFixedArray 而不是 PHP 数组?现在我们就来探索答案。我们遇到的概念是,PHP 数组实际上不是数组,而是哈希映射。让我们运行一个 PHP 5.x 版本的小示例代码,看看 PHP 数组的内存使用情况。

让我们创建一个包含 100,000 个唯一 PHP 整数的数组。由于我运行的是 64 位机器,我预计每个整数占用 8 个字节。因此,数组将消耗大约 800,000 字节内存。代码如下:

$startMemory = memory_get_usage();
$array = range(1,100000);
$endMemory = memory_get_usage();
echo ($endMemory - $startMemory)." bytes";

如果我们在命令提示符下运行这段代码,就会看到 14,649,040 字节的输出结果。是的,没错。内存使用量几乎是我们计划的 18.5 倍。这意味着,对于一个 PHP 数组来说,数组中每个元素的开销为 144 字节(18 * 8 字节)。现在,这额外的 144 字节从何而来,为什么 PHP 会为每个数组元素使用如此多的额外内存?下面解释一下 PHP 数组使用的额外字节:

image 2023 11 07 11 18 40 393

本图显示了 PHP 数组的内部工作原理。它将数据存储在一个桶中,以避免碰撞并容纳更多数据。为了管理这种动态特性,数组内部实现了双链表和哈希表。最终,数组中的每个元素都会耗费大量额外的内存空间。下面是基于 PHP 数组实现代码(C 代码)的每个元素的内存消耗明细:

32 bit 64 bit

zval

16 bytes

24 bytes

+ cyclic GC info

4 bytes

8 bytes

+ allocation header

8 bytes

16 bytes

zval (value) total

28 bytes

48 bytes

bucket

36 bytes

72 bytes

+ allocation header

8 bytes

16 bytes

+ pointer

4 bytes

8 bytes

bucket (array element) total

48 bytes

96 bytes

Grand total (bucket+zval)

76 bytes

144 bytes

为了理解 PHP 数组的内部结构,我们需要深入研究 PHP 的内部结构。这超出了本书的范围。推荐阅读: https://nikic.github.io/2011/12/12/How-big-are-PHP-arrays-really-Hint-BIG.html

在新的 PHP 7 版本中,PHP 数组及其内部构造方式有了很大改进。因此,每个元素的开销从 144 字节减少到了 36 字节。这是一个很大的改进,而且适用于 32 位和 64 位操作系统。下面是一个数组中 100,000 个项目的对比图:

$array = Range(1,100000) 32 bit 64 bit

PHP 5.6 or below

7.4 MB

14 MB

PHP 7

3 MB

4 MB

因此,换句话说,在阵列存储方面,PHP 7 对 32 位系统的改进系数为 2.5 倍,对 64 位系统的改进系数为 3.5 倍。这确实是一个很好的改进。但这都是关于 PHP 数组的,那么 SplFixedArray 呢?让我们在 PHP 7 和 PHP 5.x 中使用 SplFixArray 运行相同的示例:

$items = 100000;
$startMemory = memory_get_usage();
$array = new SplFixedArray($items);
for ($i = 0; $i < $items; $i++) {
    $array[$i] = $i;
}
$endMemory = memory_get_usage();

$memoryConsumed = ($endMemory - $startMemory) / (1024*1024);
$memoryConsumed = ceil($memoryConsumed);
echo "memory = {$memoryConsumed} MB\n";

我们在这里编写了 SplFixedArray 的内存消耗功能。如果我们把 $array = new SplFixedArray($items); 这一行改成 $array = []; ,就可以运行与 PHP 数组相同的代码了。

由于操作系统、内存大小、调试器开/关等因素不同,基准测试结果可能因机器而异。建议在自己的机器上运行代码,生成类似的基准进行比较。

下面是 PHP 数组和 SplFixedArray 在 64 位系统中对一个包含 100,000 个整数的数组的内存消耗比较:

100,000 items Using PHP array SplFixedArray

PHP 5.6 or below

14 MB

6 MB

PHP 7

5 MB

2 MB

不仅在内存使用方面,SplFixedArray 的执行速度也比一般的 PHP 数组操作(如访问值、赋值等)更快。

虽然我们像使用数组一样使用带有 []SplFixedArray 对象,但 PHP 数组函数并不适用于 SplFixedArray。我们不能直接使用任何 PHP 数组函数,如 array_sumarray_filter 等。

使用 SplFixedArray 的更多示例

由于 SplFixedArray 具有良好的性能提升指标,我们可以在大多数数据结构和算法中使用它来代替普通的 PHP 数组。现在,我们将探讨在不同场景中使用 SplFixedArray 的更多示例。

转换 PHP 数组为 SplFixedArray

我们已经看到了如何创建具有固定长度的 SplFixedArray。如果我想在运行时创建一个 SplFixedArray 数组,该怎么办?下面的代码块展示了如何实现:

$array =[1 => 10, 2 => 100, 3 => 1000, 4 => 10000];
$splArray = SplFixedArray::fromArray($array);
print_r($splArray);

在这里,我们使用 SplFixedArray 类的静态方法 fromArray 从已有数组 $array 中构造一个 SplFixedArray。然后使用 PHP print_r 函数打印数组。输出结果如下:

SplFixedArray Object
(
    [0] =>
    [1] => 10
    [2] => 100
    [3] => 1000
    [4] => 10000
)

我们可以看到,数组现在已经转换为 SplFixedArray,并且它保留了与实际数组中完全相同的索引号。由于实际数组中没有定义 0 索引,因此这里的 0 索引被保留为空。但是,如果我们想忽略先前数组中的索引,并为它们分配新的索引,那么就必须将先前代码的第二行改为这样:

$splArray = SplFixedArray::fromArray($array,false);

现在,如果我们再次打印数组,输出结果如下:

SplFixedArray Object
(
    [0] => 10
    [1] => 100
    [2] => 1000
    [3] => 10000
)

如果我们想在运行时将一个数组转换为固定数组,最好是在以后不使用时取消设置常规 PHP 数组。如果是一个大数组,这样做可以节省内存。

将 SplFixedArray 转换为 PHP 数组

我们可能还需要将 SplFixedArray 转换为普通的 PHP 数组,以便应用 PHP 中的一些预定义数组函数。和前面的例子一样,这也是非常简单的事情:

$items = 5;
$array = new SplFixedArray($items);
for ($i = 0; $i < $items; $i++) {
    $array[$i] = $i * 10;
}

$newArray = $array->toArray();
print_r($newArray);

这将产生以下输出:

Array
(
    [0] => 0
    [1] => 10
    [2] => 20
    [3] => 30
    [4] => 40
)

声明后更改 SplFixedArray 大小

由于我们在一开始就定义了数组的大小,因此以后可能需要更改大小。为此,我们必须使用 SplFixedArray 类的 setSize() 方法。示例如下:

$items = 5;
$array = new SplFixedArray($items);
for ($i = 0; $i < $items; $i++) {
    $array[$i] = $i * 10;
}

$array->setSize(10);
$array[7] = 100;

使用 SplFixedArray 创建多维数组

我们可能还需要使用 SplFixedArray 创建二维或多维数组。为了做到这一点,建议按照下面的示例进行操作:

$array = new SplFixedArray(100);
for ($i = 0; $i < 100; $i++)
    $array[$i] = new SplFixedArray(100);

实际上,我们是在每个数组索引内创建另一个 SplFixedArray。我们可以添加任意多的维数。但我们必须记住,有了维数,我们就等于乘上了数组的大小。因此,它可能会很快变大。