您是否曾尝试将包含一百万行的 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(5) as $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(5) as $number){
echo $number.PHP_EOL;
}
foreach
主要区别在于:生成器不会存储所有内容——它会根据要求一次生成一个元素。
假设你有一个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或其它技术来实现,要分成几步,现在有了生成器,轻松就能搞定。
$startMemory=memory_get_usage();
$array=range(1,1_000_000);
// creates an array with a million numbers
echo "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,5) as $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 — — 执行时间没有明显变化。
Generator的实例
,同时实现Iterator
和Traversable
。yield
;Generator
对象。foreach
或current()
调用一直执行,直到第一个yield
。yield
暂停执行并返回一个值。next()
从暂停点恢复。yield
对应一条指令:zval
)并用每个步骤覆盖它。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
其中包括:
方法 | 时间 | 已用内存 | 峰值差异 | 行 |
---|---|---|---|---|
数组 | 1.401秒 | 120B | 395.92 MB | 1,000,001 |
Generators | 1.012秒 | 0 B | 0.00 MB | 1,000,001 |
这些结果表明,惰性生成器在处理 CSV 等大型数据集时可以显著减少内存使用量。
峰值内存使用量:
执行时间:
欢迎您随意分叉 repo、尝试基准测试或使其适应您自己的数据处理任务!
Generators(生成器)和Iterator API已经是现代 PHP 开发的必备工具。
它们能让我们能够处理数百万条记录,而不会耗尽内存。
对于简单的流逻辑,可以使用生成器;而对于需要更多控制力的,则可以使用迭代器 API。
那么,你在生产环境中使用生成器吗?在评论区分享你的使用案例哦!~
作者:场长
本文为 @ 场长 创作并授权 21CTO 发布,未经许可,请勿转载。
内容授权事宜请您联系 webmaster@21cto.com或关注 21CTO 公众号。
该文观点仅代表作者本人,21CTO 平台仅提供信息存储空间服务。