加快数组处理速度

数组是任何 PHP 程序的重要组成部分。事实上,处理数组是不可避免的,因为程序日常处理的许多实际数据都是以数组的形式到达的。其中一个例子就是 HTML 表单中的数据。这些数据最终会以数组形式出现在 $_GET$_POST 中。

在本节中,我们将向您介绍 SPL 中包含的一个鲜为人知的类:SplFixedArray 类。将数据从标准数组迁移到 SplFixedArray 实例不仅能提高性能,而且所需的内存也会大大减少。学习如何利用本章所介绍的技术,可以对目前使用数组处理大量数据的程序代码的速度和效率产生重大影响。

在 PHP 8 中使用 SplFixedArray

在 PHP 5.3 中引入的 SplFixedArray 类实际上是一个类似于数组的对象。但与 ArrayObject 不同的是,该类要求对数组大小设置硬限制,并且只允许整数索引。之所以要使用 SplFixedArray 而不是 ArrayObject,是因为 SplFixedArray 占用的内存少得多,而且性能很高。事实上,SplFixedArray 占用的内存比具有相同数据的标准数组还要少!

SplFixedArray 与数组和 ArrayObject 的比较

一个简单的基准测试程序说明了标准数组、ArrayObjectSplFixedArray 之间的差异:

  1. 首先,我们定义几个稍后在代码中使用的常量:

    // /repo/ch10/php7_spl_fixed_arr_size.php
    define('MAX_SIZE', 1000000);
    define('PATTERN', "%14s : %8.8f : %12s\n");
  2. 接下来,我们定义一个函数,用于添加由 64 字节长的字符串组成的 100 万个元素:

    function testArr($list, $label) {
        $alpha = new InfiniteIterator(
            new ArrayIterator(range('A','Z')));
        $start_mem = memory_get_usage();
        $start_time = microtime(TRUE);
        for ($x = 0; $x < MAX_SIZE; $x++) {
            $letter = $alpha->current();
            $alpha->next();
            $list[$x] = str_repeat($letter, 64);
        }
        $mem_diff = memory_get_usage() - $start_mem;
        return [$label, (microtime(TRUE) - $start_time),
            number_format($mem_diff)];
    }
  3. 然后,我们调用该函数三次,分别将 arrayArrayObjectSplFixedArray 作为参数:

    printf("%14s : %10s : %12s\n", '', 'Time', 'Memory');
    $result = testArr([], 'Array');
    vprintf(PATTERN, $result);
    $result = testArr(new ArrayObject(), 'ArrayObject');
    vprintf(PATTERN, $result);
    $result = testArr(new SplFixedArray(MAX_SIZE), 'SplFixedArray');
    vprintf(PATTERN, $result);
  4. 以下是 PHP 7.1 Docker 容器的运行结果:

    root@php8_tips_php7 [ /repo/ch10 ]#
    php php7_spl_fixed_arr_size.php
                   : Time         : Memory
    Array          : 1.19430900   : 129,558,888
    ArrayObject    : 1.20231009   : 129,558,832
    SplFixedArray  : 1.19744802   : 96,000,280
  5. 在 PHP 8 中,所需的时间大大减少,如图所示:

root@php8_tips_php8 [ /repo/ch10 ]#
php php7_spl_fixed_arr_size.php
              : Time          : Memory
Array         : 0.13694692    : 129,558,888
ArrayObject   : 0.11058593    : 129,558,832
SplFixedArray : 0.09748793    : 96,000,280

从结果可以看出,PHP 8 处理数组的速度比 PHP 7.1 快 10 倍。两个版本使用的内存量完全相同。无论使用哪个版本的 PHP,SplFixedArray 占用的内存都比标准数组或 ArrayObject 少得多。现在让我们看看 SplFixedArray 的使用在 PHP 8 中发生了哪些变化。

使用 PHP 8 中的 SplFixedArray 更改

您可能还记得在 第 7 章 "使用 PHP 8 扩展时避免陷阱" 中的 TraversableIteratorAggregate 移植一节中对 Traversable 接口的简要讨论。该节中提出的注意事项同样适用于 SplFixedArray。尽管 SplFixedArray 没有实现 Traversable,但它实现了 Iterator,而后者又扩展了 Traversable

在 PHP 8 中,SplFixedArray 不再实现 Iterator。相反,它实现了 IteratorAggregate。这一变化的好处是,PHP 8 中的 SplFixedArray 更快、更高效,而且在嵌套循环中使用也更安全。缺点是,如果将 SplFixedArray 与以下方法一起使用,可能会导致代码断开:current()key()next()rewind()valid()

如果需要访问数组导航方法,现在必须使用 SplFixedArray::getIterator() 方法访问内部迭代器,所有导航方法都可以从内部迭代器中获取。下面所示的一个简单代码示例说明了潜在的代码中断:

  1. 我们首先从数组构建 SplFixedArray 实例:

    // /repo/ch10/php7_spl_fixed_arr_iter.php
    $arr = ['Person', 'Woman', 'Man', 'Camera',
    'TV'];$fixed = SplFixedArray::fromArray($arr);
  2. 然后我们使用数组导航方法来迭代数组:

    while ($fixed->valid()) {
        echo $fixed->current() . '. ';
        $fixed->next();
    }

在 PHP 7 中,输出是数组中的五个单词:

root@php8_tips_php7 [ /repo/ch10 ]#
php php7_spl_fixed_arr_iter.php
Person. Woman. Man. Camera. TV.

然而,在 PHP 8 中,结果完全不同,如下所示:

root@php8_tips_php8 [ /repo/ch10 ]#
php php7_spl_fixed_arr_iter.php
PHP Fatal error: Uncaught Error: Call to undefined method
SplFixedArray::valid() in /repo/ch10/php7_spl_fixed_arr_iter.
php:5

要在 PHP 8 中运行该示例,只需使用 SplFixedArray::getIterator() 方法访问内部迭代器即可。其余代码无需重写。以下是为 PHP 8 重写的修订代码示例:

// /repo/ch10/php8_spl_fixed_arr_iter.php
$arr = ['Person', 'Woman', 'Man', 'Camera', 'TV'];
$obj = SplFixedArray::fromArray($arr);
$fixed = $obj->getIterator();
while ($fixed->valid()) {
    echo $fixed->current() . '. ';
    $fixed->next();
}

现在输出的是五个单词,没有任何错误:

root@php8_tips_php8 [ /repo/ch10 ]#
php php8_spl_fixed_arr_iter.php
Person. Woman. Man. Camera. TV.

现在你已经了解了如何提高数组处理性能,我们将把注意力转向数组性能的另一个方面:排序。