17611538698
webmaster@21cto.com

PHP 中的惰性求值:使用生成器节省实际内存

编程语言 0 16 2025-08-19 11:07:46
图片
简介

您是否曾尝试将包含一百万行的 CSV 文件加载到内存中,但最终却得到如下的结果?

PHP Fatal error:Allowed memory size of 134217728 bytes exhausted
即使,我们设置更高的memory_limit,当您逐行处理时,整个数据集仍然注入于内存中。

那么解决方案是什么?我们可以使用惰性求值——一种仅在实际需要时才生成和处理数据的技术。

在 PHP 中,您可以通过两种方式实现这一点:使用生成器yield) 和通过迭代器 API

接下来让我们来探索一下这两种实现方式。

什么是惰性求值?


通常情况,当您创建一个数组时,PHP 会一次性将所有元素加载到内存中:

function getNumbersArray(int$count):array{   $result=[];    for($i=1;$i<=$count;$i++){        $result[]=$i;    }    return $result;}foreach(getNumbersArray(5as $number){    echo $number.PHP_EOL;}
在这里,整个数组[1, 2, 3, 4, 5]都将存储在内存中。

现在,让我们尝试一种称为“懒惰”的编程方法:

function getNumbersGenerator(int$count):Generator{    for($i=1;$i<=$count;$i++){            yield$i;    }}foreach(getNumbersGenerator(5as $number){    echo $number.PHP_EOL;}
foreach主要区别在于:生成器不会存储所有内容——它会根据要求一次生成一个元素。

轻松读取大尺寸的 CSV 文件


假设你有一个data.csv,大小约2 GB 的文件。如果你尝试用数组加载它,比如file()fgetcsv()函数很可能都会耗尽内存。

但有了yield生成器,这一切将变得非常容易:

function readCsv(string $filename):Generator{    $handle=fopen($filename,'r');    if($handle===false){                throw new RuntimeException("Cannot open file $filename");    }     while(($row=fgetcsv($handle))!==false){      yield $row;    }    fclose($handle);}
foreach(readCsv('data.csv'as $row){    // Process the row    // For example: echo implode(', ', $row) . PHP_EOL;}
内存使用情况:即使使用 2 GB 的 CSV,一次也只在内存中存储一行,因此使用量仍以KB为单位。

如果没有使用生成器,要么将文件分块,要么分行读取,使用Ajax或其它技术来实现,要分成几步,现在有了生成器,轻松就能搞定

基准测试:数组 vs. 生成器


$startMemory=memory_get_usage();$array=range(1,1_000_000);// creates an array with a million numbersecho "Array: ".(memory_get_usage()-$startMemory)/1024/1024." MB";unset($array);$startMemory=memory_get_usage();function bigGenerator():Generator{    for($i=1;$i<=1_000_000;$i++){       yield$i;    }}foreach(bigGenerator() as $n){   // Just iterate}echo "Generator: ".(memory_get_usage()-$startMemory)/1024/1024." MB";
在我机器上运行后的结果:

Array: 120 MB
Generator: 0.5 MB

迭代器 API

生成器快捷简便。但有时您需要更多控制权——保存状态、管理自定义键或动态切换数据源。

这时,迭代器 API就派上用场了。

代码示例:自定义迭代器


class RangeIterator implements Iterator{
    private int $start;
    private int $end;
    private int $current;

    public function __construct(int $start,int $end){
        $this->start=$start;
        $this->end=$end;
        $this->current=$start;
    }

    public function current():int{
        return$this->current;
    }

    public function key():int{
        return $this->current;
    }

    public function next():void{
        $this->current++;
    }

    public function rewind():void{
        $this->current=$this->start;
    }

    public function valid():bool{
        return$this->current<=$this->end;
    }
}

foreach(new RangeIterator(1,5as $num){
    echo $num.PHP_EOL;
}

何时该使用哪种方法?

设想推荐方法
需要按需传输数据

Generator

需要复杂的逻辑或内部状态

Iterator API

读取大文件或数据库流

Generator

保留状态的多次迭代

Iterator API


实际生产案例


我们正在解析一个汽车销售 API,它返回了数十万条记录。

最初,我们将所有内容加载到一个数组中——该脚本差不多占用了 1-2 GB 的内存。

切换到生成器后:

function fetchCars():Generator{
    $page=1;
    do{
        $data=apiRequest('cars',['page'=>$page]);
        foreach($data['items'as $car){
        yield $car;
    }
    $page++;
    }while(!empty($data['items']));
}

内存使用量从 2 GB 下降到仅 10 MB — — 执行时间没有明显变化。


Generators的工作原理


1. Generators 是一个对象


在 PHP 中,Generators是类Generator的实例,同时实现IteratorTraversable

它可以:


  • 存储功能状态;
  • 暂停执行yield
  • 从同一位置继续。

2.执行流程


  • 调用生成器函数不会运行代码——它会返回一个Generator对象。
  • 第一个foreachcurrent()调用一直执行,直到第一个yield
  • 每个都会yield暂停执行并返回一个值。
  • next()从暂停点恢复。
  • 当函数结束时,生成器被标记为已完成。

3. Zend Engine 内部结构


如果使用 VLD(Vulcan Logic Disassembler)编译生成器函数,则每个函数都yield对应一条指令:


  • 保存调用堆栈;
  • 存储变量上下文;
  • 将控制权交还给调用者。

4. 与数组相比


  • 数组一次性将所有元素存储在内存中。
  • 生成器仅保存当前元素zval)并用每个步骤覆盖它。
  • 仅使用几百千字节即可处理一百万个元素。


5. 无限生成器示例


function counter():Generator{
    $i=0;
    while(true){
        yield$i++;
    }
}

foreach(counter() as $num){
    if($num>5)break;
    echo $num.PHP_EOL;
}

你不能用数组来做到这一点——内存会很快耗尽。


源代码


您可以自己尝试一切——本文中使用的代码是开源的:

https://github.com/phpner/phpner-php-lazy-evaluation-demo

其中包括:

  • CSV 基准测试:数组(eager) vs 生成器(lazy)
  • NDJSON流模拟
  • 内存和时间分析
  • CLI 友好工具
  • 样本数据生成器
  • PHPUnit 测试

基准测试结果(1,000,001 行)


方法时间已用内存峰值差异
数组1.401秒120B395.92 MB1,000,001

Generators

1.012秒0 B0.00 MB1,000,001


这些结果表明,惰性生成器在处理 CSV 等大型数据集时可以显著减少内存使用量。

视觉效果


峰值内存使用量:

峰值记忆

执行时间:

执行时间

欢迎您随意分叉 repo、尝试基准测试或使其适应您自己的数据处理任务!

结语


Generators(生成器)和Iterator API已经是现代 PHP 开发的必备工具。

它们能让我们能够处理数百万条记录,而不会耗尽内存。

对于简单的流逻辑,可以使用生成器;而对于需要更多控制力的,则可以使用迭代器 API。

那么,你在生产环境中使用生成器吗?在评论区分享你的使用案例哦!~

作者:场长

评论