我努力探索在 PHP 中利用内存的最有效方法。这让我质疑这些方法是否有效。让我们将这些疑问转向 PHP 的内存使用,并通过一系列问题来探讨它们。

抱歉,我找不到第 9 题和第 10 题…

克隆对象是否可以防止在内存中加倍对象?

否,在 PHP 中克隆对象并不一定节省内存使用;实际上,它可能会消耗额外的内存。当你克隆一个对象时,你正在创建一个新的实例,它是原始对象的副本。这意味着原始对象和克隆对象都会在 PHP 运行时占用内存。

克隆可以在您需要创建对象的副本以单独处理或修改而不影响原始对象时非常有用。然而,重要的是要意识到克隆本身并不固有地减少内存使用。


<?php

function echoMemoryUsage()
{
    echo round(memory_get_usage(false) / 1024, 2) . " KB\n";
}

class Example {
    public $data;

    public function __construct($data) {
        $this->data = $data;
    }
}

echoMemoryUsage(); // 392.83 KB

$object = new Example(str_repeat('x', 1024 * 1024)); // Create an object with 1 MB of data
echoMemoryUsage(); // 1420.91 KB

$clonedObject = clone $object;
echoMemoryUsage(); // 1420.97 KB

$assignmentObject = $object;
echoMemoryUsage(); // 1420.97 KB

在 PHP 中,对象通常是通过引用分配的。当你将对象分配给变量或将它作为参数传递给函数时,你实际上是在内存中创建对该同一对象的引用,而不是创建对象数据的副本。这意味着如果你通过一个变量修改对象,这些更改将反映在所有对该对象的引用中。

生成器真的有魔法吗?

是的,生成器可以在 PHP 中节省内存使用。这是因为生成器不会为循环的每次迭代创建新的对象。相反,它们会为每次迭代重用相同的对象。

<?php

function echoMemoryUsage()
{
    echo round(memory_get_usage(false) / 1024, 2) . " KB\n";
}

echoMemoryUsage(); // 393.1 KB

function generateNumbersArray($limit) {
    $numbers = [];
    for ($i = 1; $i <= $limit; $i++) {
        $numbers[] = $i;
    }
    return $numbers;
}

$limit = 1000000;
$numbersArray = generateNumbersArray($limit);

echoMemoryUsage(); // 16781.21 KB
var_dump(is_iterable($numbersArray)); // bool(true)
unset($numbersArray);
echoMemoryUsage(); // 393.13 KB

function generateNumbersGenerator($limit) {
    for ($i = 1; $i <= $limit; $i++) {
        yield $i;
    }
}

$numbersGenerator = generateNumbersGenerator($limit);
echoMemoryUsage(); // 393.63 KB

var_dump(is_iterable($numbersGenerator)); // bool(true)

PHP 的可迭代类比数组更节省内存吗?

是的,PHP 的可迭代类在某些场景下可能比数组更节省内存,尤其是在处理大数据集时,因为它们允许你迭代地处理数据,而无需一次性将所有数据加载到内存中。可迭代类可以使用生成器,它们可以即时生成数据,与加载整个数组相比,可能减少内存消耗,但这是生成器的优势。

以下是一个示例,演示了通过比较使用生成器的可迭代类和传统数组在处理大量数据集时的内存使用情况:

<?php

class LargeDataSet implements IteratorAggregate {
    private $data = [];

    public function __construct($size) {
        for ($i = 1; $i <= $size; $i++) {
            $this->data[] = "Item " . $i;
        }
    }

    public function getIterator() {
        foreach ($this->data as $item) {
            yield $item;
        }
    }
}

$size = 1000000; // Number of items in the dataset

// Using an iterable class with generators
$iterableDataSet = new LargeDataSet($size);
$iterableMemory = memory_get_peak_usage(true);

// Using a traditional array
$arrayDataSet = [];
for ($i = 1; $i <= $size; $i++) {
    $arrayDataSet[] = "Item " . $i;
}
$arrayMemory = memory_get_peak_usage(true);

echo "Memory used by iterable class: " . formatBytes($iterableMemory) . "\n";
// Memory used by iterable class: 56 MB
echo "Memory used by array: " . formatBytes($arrayMemory) . "\n";
// Memory used by array: 110.01 MB

function formatBytes($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($units) - 1);

    $bytes /= (1 << (10 * $pow));

    return round($bytes, $precision) . ' ' . $units[$pow];
}

两个大约占用 56Mb 存储数据,但 IteratorAggregate 可以使用生成器处理数据,而不需要将数据数组加倍到内存中,这是优势。

传递参数通过引用是否可以防止在内存中重复参数?

是的。这是因为传递的是参数的引用,而不是参数的实际值。

<?php

function echoMemoryUsage()
{
    echo round(memory_get_usage(false) / 1024, 2) . "\n";
}

function testMemoryRegular($array)
{
    // $array will be copied, because it is modified
    $array[0] += 1;
    echoMemoryUsage();
}

function testMemoryReference(&$array)
{
    // $array will not be copied, even it is modified
    $array[0] += 1;
    echoMemoryUsage();
}

$array = [];
for ($i = 0; $i < 100000; $i++) {
    $array[$i] = $i;
}

echoMemoryUsage(); // 2444.67
testMemoryRegular($array); // 4496.78
testMemoryReference($array); // 2444.73

使用函数代替循环是否可以节省内存?

是的。这是因为函数只加载一次到内存中,而循环每次执行时都会加载到内存中。

<?php

function echoMemoryUsage()
{
    echo "\n" . memory_get_usage(false) . " \n";
}

$numbers = range(0, 100, 1);
echoMemoryUsage(); //403560

foreach ($numbers as $number) {
    echo "\n" . $number;
}

function printNumber($number) {
    echo "\n" . $number;
}

echoMemoryUsage(); //403592

foreach ($numbers as $number) {
    printNumber($number);
}

echoMemoryUsage(); //403592

在第一种情况下,对于每个 foreach() 循环的迭代,都会创建一个新的循环。这意味着 PHP 需要将 foreach() 循环加载到内存中 100 次。

第二个情况只会创建一个函数,该函数将被用于每个 foreach() 循环的迭代中。

在此代码中, printNumber() 函数只加载到内存中一次。这意味着即使使用了 100 次,PHP 也只需将 printNumber() 函数加载到内存中一次。

一般来说,在 PHP 中使用函数而不是循环可以节省内存。然而,需要注意的是,这并不总是如此。如果函数非常复杂,它可能比循环使用更多的内存。重要的是要对您的代码进行基准测试,以查看在您特定情况下使用函数而不是循环是否真的节省内存。

单例模式是创建对象的万能药吗?

否,单例模式是一种设计模式,可以确保只创建一个对象实例。这可以用于需要全局访问的对象或创建成本较高的对象。然而,不应将单例模式视为创建对象的万能药。在某些情况下,创建多个对象实例可能更有效率。

可以使用流式响应来最小化 PHP 中的内存使用吗?

是的,流式响应是一种可以将内容以更小的块发送而不是将整个内容加载到内存中的技术。这可以在生成用于下载的大文件时最小化内存使用。

以下是一个如何使用流式响应以最小化内存使用的示例:

<?php

$file = fopen("large_file.txt", "r");

while (!feof($file)) {
    $chunk = fread($file, 1024);

    // Send the chunk to the client.
    echo $chunk;
}

fclose($file);

SplFixedArray 是否比常规数组更节省内存?

是的,SplFixedArray 是一个固定大小的数组,这意味着它不能被调整大小。这在某些情况下可能是一个缺点,但也意味着 SplFixedArray 比常规 PHP 数组使用更少的内存。

如果您知道您将需要使用固定大小的数组,那么 SplFixedArray 可以是一个减少内存使用的不错选择。然而,如果您需要能够动态地向数组中添加或删除元素,那么 SplFixedArray 不是一个好的选择。

以下是使用 SplFixedArray 最小化内存使用的示例:

<?php

$beforeArrayInit = memory_get_usage(false);

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

$afterArrayInit = memory_get_usage(false);
echo "SplFixedArray memory usage:" . ($afterArrayInit - $beforeArrayInit) . " \n";
// SplFixedArray memory usage:1872


$beforeArrayInit = memory_get_usage(false);

$regularArray = array();
for ($i = 0; $i < 100; $i++) {
    $regularArray[] = $i;
}

$afterArrayInit = memory_get_usage(false);
echo "Array memory usage:" . ($afterArrayInit - $beforeArrayInit) . " \n";
// Array memory usage:2616

所有测试都是在 PHP -v 8.1 下进行的。

我希望您在这里找到了一些有趣且新鲜的东西!