作为PHP开发人员,我们通常不必担心内存管理。PHP引擎在我们背后的清理工作做得很好,短暂的执行上下文Web服务器模型意味着即使是最马虎的代码也不会产生持久的影响。在极少数情况下,我们可能需要走出这个舒适区——比如当我们试图在一个大项目上运行Composer以创建我们可以创建的最小VPS时,或者当我们需要读取一个大文件时。后一个问题是我们将在本教程中深入探讨的内容。本教程的源代码可以在GitHub上找到。衡量成功确保我们对代码进行任何改进的唯一方法是测试一个糟糕的案例,然后在我们修复它之后将我们的测量结果与另一个进行比较。换句话说,除非我们知道“解决方案”对我们有多大帮助(如果有的话),否则我们不知道它是否真的是解决方案。这里有两个我们可以联系起来的措施。首先是CPU使用率。我们正在处理的流程有多快或多慢?其次是内存占用。脚本执行需要多少内存?这两者通常成反比——这意味着我们可以以牺牲CPU使用率为代价来减少内存使用量,反之亦然。在异步执行模型(例如多进程或多线程PHP应用程序)中,CPU和内存使用是重要的考虑因素。在传统的PHP架构中,当任一值达到服务器的限制时,这些通常会成为问题。在PHP中测量CPU使用率是不切实际的。如果这是您关注的领域,请考虑在Ubuntu或MacOS上使用类似top的工具。对于Windows,考虑在Ubuntu中使用Linux子系统。出于本教程的目的,我们将测量内存使用情况。我们将看看“传统”脚本中使用了多少内存。我们将实施一些优化策略并对其进行测量。最后,我希望你能够做出有经验的选择。我们检查使用了多少内存的方法是://formatBytesistakenfromthephp.netdocumentationmemory_get_peak_usage();functionformatBytes($bytes,$precision=2){$units=array("b","kb","mb","gb","tb");$bytes=max($bytes,0);$pow=floor(($bytes?log($bytes):0)/log(1024));$pow=min($pow,count($单位)-1);$字节/=(1<<(10*$pow));returnround($bytes,$precision)."".$units[$pow];}我们将在脚本函数的末尾使用这些,以便我们可以看到哪个脚本一次使用了最多的内存。我们有哪些选择?有很多方法可以有效地读取文件。但也有两种情况我们可能会用到它们。我们希望同时读取和处理所有数据,输出处理后的数据或根据读取的内容进行其他操作。我们可能还想在不实际访问数据的情况下转换数据流。假设对于第一种情况,我们想要读取一个文件并每10,000行创建一个独立排队的处理作业。我们需要在内存中保留至少10000行并将它们传递给排队的工作管理器(无论采用何种形式)。对于第二种情况,假设我们要压缩一个特别大的API响应的内容。我们不关心它的内容是什么,但我们需要确保它以压缩形式备份。在这两种情况下,如果我们需要读取大文件,首先,我们需要知道数据是什么。其次,我们并不真正关心数据是什么。让我们探索选项...逐行读取文件有很多操作文件的函数,我们将部分组合成一个简单的文件读取器(包装为方法)://frommemory.phpfunctionformatBytes($bytes,$precision=2){$units=array("b","kb","mb","gb","tb");$bytes=max($bytes,0);$pow=floor(($bytes?log($字节):0)/log(1024));$pow=min($pow,count($units)-1);$bytes/=(1<<(10*$pow));returnround($bytes,$precision).".$units[$pow];}printformatBytes(memory_get_peak_usage());//fromreading-files-line-by-line-1.phpfunctionreadTheFile($path){$lines=[];$handle=fopen($path,"r");while(!feof($handle)){$lines[]=trim(fgets($handle));}fclose($handle);返回$lines;}readTheFile("莎士比亚.txt");需要“memory.php”;我们阅读了莎士比亚全集的文本文件。文件大小为5.5MB,内存使用峰值为12.8MB。现在让我们使用一个生成器来读取每一行://fromreading-files-line-by-line-2.phpfunctionreadTheFile($path){$handle=fopen($path,"r");while(!feof($handle)){yieldtrim(fgets($handle));}fclose($handle);}readTheFile("shakespeare.txt");require"memory.php";文本文件大小没有变化,但内存使用峰值仅为393KB。即使我们可以对读取的数据做些什么,也没有任何意义。当我们看到两个空格时,也许我们可以将文档分成块,就像这样://fromreading-files-line-by-line-3.php$iterator=readTheFile("shakespeare.txt");$buffer="";foreach($iteratoras$iteration){preg_match("/\n{3}/",$buffer,$matches);if(count($matches)){print".";$buffer="";}否则{$buffer.=$iteration.PHP_EOL;}}需要“memory.php”;猜猜我们用了多少内存?我们将文档分成1216个块,但仍然只使用了459KB的内存。这让你感到惊讶吗??鉴于生成器的性质,我们使用的最多内存是使用我们需要在迭代期间存储的最大文本块。在这个例子中,最大的块是101985个字符。我已经编写了HintingPerformancewithGenerators和NikitaPopov的IteratorLibrary,如果您有兴趣,请查看!生成器还有其他用途,但最明显的好处是读取大文件的高性能。如果我们需要处理这些数据,生成器可能是最好的方式。管道之间的文件当我们不需要处理数据时,我们可以将文件数据传递给另一个文件。通常称为管道(大概是因为除了末端我们无法看到管道内部,当然末端也是不透明的),我们可以通过使用流方法来实现。让我们首先编写一个脚本来从一个文件传递到另一个文件。这样我们就可以测量内存使用情况://frompiping-files-1.phpfile_put_contents("piping-files-1.txt",file_get_contents("shakespeare.txt"));需要“memory.php”;正如预期的那样,此脚本使用更多内存来复制文本文件。这是因为它读取(并保存)内存中的文件内容,直到将其写入新文件。对于小文件,这种方法可能没问题。当涉及到较大的文件时,它会被拉伸......让我们尝试使用流式传输(管道)将一个文件传输到另一个文件://frompiping-files-2.php$handle1=fopen("shakespeare.txt","r");$handle2=fopen("piping-files-2.txt","w");stream_copy_to_stream($handle1,$handle2);fclose($handle1);fclose($handle2);require"memory.php";这段代码有点陌生。我们打开两个文件的句柄,第一个处于只读模式,第二个处于只写模式,然后我们从第一个复制到第二个。最后我们将其关闭,也许令您惊讶的是,只使用了393KB的内存,这看起来很熟悉。就像代码生成器存储它读取的每一行代码一样?这是因为fgets的第二个参数指定每行读取多少字节(默认为-1或直到下一行)。第三个参数stream_copy_to_stream和第二个参数是同一类型的参数(默认值相同),stream_copy_to_stream每次从一个数据流中读取一行,同时写入到另一个数据流中。它跳过了生成器只有一个值的部分(因为我们不需要这个值)。这篇文章对我们来说可能没用,所以让我们想一些我们可能会用到的例子。假设我们想从CDN导出图像作为一种重定向路由应用程序。我们可以参考下面的代码来实现://frompiping-files-3.phpfile_put_contents("piping-files-3.jpeg",file_get_contents("https://github.com/assertchris/uploads/raw/master/rick.jpg"));//...或者直接写出,如果我们不需要内存,则需要“memory.php”;想象一个路由应用程序让我们看到这段代码。但是,我们想从CDN中获取文件,而不是从本地文件系统中获取文件。我们可以用其他更好的东西(比如Guzzle)替换file_get_contents,尽管它们在内部几乎是一样的。图像内存约为581K。现在,让我们试试这个//frompiping-files-4.php$handle1=fopen("https://github.com/assertchris/uploads/raw/master/rick.jpg","r");$handle2=fopen("piping-files-4.jpeg","w");//...或者直接写到stdout,如果我们不需要内存信息流_copy_to_stream($handle1,$handle2);fclose($handle1);fclose($handle2);需要“内存。PHP";内存使用量明显减少(大约400K),但结果是一样的。如果我们不关心内存信息,我们仍然可以在标准模式下输出。事实上,PHP提供了一个简单的方法来做到这一点:$handle1=fopen("https://github.com/assertchris/uploads/raw/master/rick.jpg","r");$handle2=fopen("php://stdout","w");stream_copy_to_stream($handle1,$handle2);fclose($handle1);fclose($handle2);//require"memory.php";还有其他流,我们可以通过管道写入和读取(或只读/只写):php://stdin(只读)php://stderr(只写,如php://stdout)php://input(只读)this让我们访问原始请求主体php://output(只写)让我们写入输出缓冲区php://memory和php://temp(读写)是我们可以临时存储数据的地方.区别在于php://temp会在文件系统足够大时将数据存储在文件系统中,而php://memory会一直存储在内存中直到资源耗尽。还有一个我们可以在称为过滤器的流上使用的技巧。它们是一个中间步骤,可以在不将它们暴露给我们的情况下提供对流数据的一些控制。想象一下,我们将使用Zip扩展来压缩我们的shakespeare.txt文件。//fromfilters-1.php$zip=newZipArchive();$filename="filters-1.zip";$zip->open($filename,ZipArchive::CREATE);$zip->addFromString("shakespeare.txt",file_get_contents("莎士比亚.txt"));$zip->关闭();需要“memory.php”;这是一段整洁的小代码,但它测量的内存使用量约为10.75MB。使用过滤器,我们可以减少内存://fromfilters-2.php$handle1=fopen("php://filter/zlib.deflate/resource=shakespeare.txt","r");$handle2=fopen("filters-2.deflated","w");stream_copy_to_stream($handle1,$handle2);fclose($handle1);fclose($handle2);require"memory.php";这里可以看到php的A过滤器://filter/zlib.deflate读取并压缩资源的内容。之后我们可以将压缩数据导出到另一个文件中。这只使用了896KB。我知道它的格式不一样,或者制作zip存档有好处。你一定想知道:如果你可以选择不同的格式并节省大约12倍的内存,为什么不呢?要解压缩这些数据,我们可以通过执行另一个zlib过滤器来恢复压缩数据://fromfilters-2.phpfile_get_contents("php://filter/zlib.inflate/resource=filters-2.deflated");Streams已被广泛使用在《UnderstandingStreamsinPHP》和《UEffectivelyUsingStreamsinPHP》中的Stream中已经有完整的介绍。如果您喜欢完全不同的视角,请阅读它。
