PHP中需要一个类似数组的结构,占用最少的内存。

66

在我的PHP脚本中,我需要创建一个包含600k个整数的数组。不幸的是,我的Web服务器的memory_limit设置为32M,所以在初始化数组时,脚本会因以下错误信息而终止:

致命错误:/home/www/myaccount/html/mem_test.php8行尝试分配71字节的内存时,已用尽33554432字节的内存限制。

我知道PHP将数组值存储为zvalue,而不是普通整数值(在我的64位系统上为8字节),因此每个数组条目使用的内存量要大得多。我编写了一个小脚本来估算每个数组条目使用的内存量,结果显示,它实际上占用了128字节。128!!!我需要>73M的内存才能存储该数组。不幸的是,Web服务器不在我的控制范围之内,因此我不能增加memory_limit

我的问题是,在PHP中是否有可能创建一个类似于数组的结构,它使用较少的内存。我并不需要该结构具有关联性(只需通过索引访问即可)。它也不需要具有动态调整大小的功能——我确切地知道数组的大小。此外,所有元素都将是相同类型的。就像一个老式的C数组。


编辑: 因此,deceze的解决方案适用于32位整数。但是,即使您在64位系统上,pack()似乎也不支持64位整数。为了在我的数组中使用64位整数,我应用了一些位操作。以下代码片段可能会对某些人有所帮助:

function push_back(&$storage, $value)
{
    // split the 64-bit value into two 32-bit chunks, then pass these to pack().
    $storage .= pack('ll', ($value>>32), $value);
}

function get(&$storage, $idx)
{
    // read two 32-bit chunks from $storage and glue them back together.
    return (current(unpack('l', substr($storage, $idx * 8, 4)))<<32 |
            current(unpack('l', substr($storage, $idx * 8+4, 4))));
}

尝试使用列表:http://uk3.php.net/list - Dave
1
@Dave:list 返回一个数组... 这有什么意义? - Alexander Tobias Bockstaller
1
这应该能为您解答为什么:http://www.stoimen.com/blog/2012/07/24/php-arrays-or-linked-lists/ - Dave
@AlexanderTobiasHeinrich:因为这不是一个问题,也不是一个论坛。 - DanMan
@AlexanderTobiasHeinrich 为什么不将你的修改编辑到deceze的答案中呢? - Dan
显示剩余5条评论
8个回答

60

你能得到的最节省内存的方式可能是将所有内容打包成二进制字符串,并使用手动索引来访问它。

$storage = '';

$storage .= pack('l', 42);

// ...

// get 10th entry
$int = current(unpack('l', substr($storage, 9 * 4, 4)));

如果可以一次性完成“数组”的初始化并且只需要从结构中读取,那么这是可行的。如果需要大量添加到字符串中,这将变得极其低效。即使如此,也可以使用资源句柄来实现:

$storage = fopen('php://memory', 'r+');
fwrite($storage, pack('l', 42));
...

这非常高效。然后您可以将此缓冲区读回变量并将其用作字符串,或者您可以继续使用资源和 fseek


1
600k条目会变得很混乱,不是吗? - Dave
1
为什么不呢?写一个小包装器,访问条目可以非常简单明了。只有初始化可能会变得混乱。 - deceze
@Alex 嗯,好问题,不确定。d 应该 可以做到,但实际上似乎并没有。也许值得开一个新的问题来讨论。 :) - deceze
1
好吧,我想我只需要进行一些位移操作,并将8字节整数存储为2个4字节整数...如果它有效,我稍后会编辑我的问题并分享代码。就此而言。 - Alexander Tobias Bockstaller
2
从PHP pack()手册中可以看到:“在整数类型具有64位大小的系统中,浮点数很可能没有足够大的尾数来保存值而不会失去精度。如果这些系统还具有本地64位C int类型(大多数类UNIX系统都没有),则在使用I包装格式的上限范围内使用唯一的方法是创建具有与所需无符号值相同的字节表示的整数负值。”因此,即使php是64位编译,它仍然只能正确支持32位整数进行打包。 - Dave
显示剩余2条评论

31

PHP Judy Array可以显著地减少内存占用,比标准PHP数组以及SplFixedArray更加节省。

引用一句话:“一个有100万条目的使用普通PHP数组数据结构的数组需要200MB的内存。使用SplFixedArray大约需要90兆字节。而使用Judy只需要8MB。其中的权衡在于性能,Judy需要的时间大约是普通PHP数组实现的两倍。”


12
但这是一种扩展,如果他没有增加 mem_limit 的访问权限,那么他将无法安装扩展,尽管这对其他人来说是一个不错的解决方案。 - Dave
9
对于其他可能查看我的问题的人来说,这仍然是一个很好的答案。 - Alexander Tobias Bockstaller

11
你可以尝试使用SplFixedArray,它更快且占用更少的内存(文档注释说减少了约30%)。在这里进行测试herehere

不过,似乎减少约30%还远远不够。 - deceze

11
如果可能的话,您可以使用对象。这些通常比数组使用更少的内存。 另外,SplFixedArray是一个不错的选择。
但这真的取决于您需要做的实现。如果您需要一个函数返回一个数组,并且正在使用PHP 5.5,您可以使用生成器yield来流式传输数组。

5

使用字符串-这是我会做的。将其存储在一个字符串中,使用固定的偏移量(16或20位数字应该可以做到?)并使用substr获取所需的内容。写入/读取速度非常快,超级容易,而且600,000个整数只需要~12M的存储空间。

base_convert() -如果您需要更紧凑但最少努力的内容,请将您的整数转换为基于36而不是基于10;在这种情况下,14位数字将存储在9个字母数字字符中。您需要制作两个64位int,但我确信这不是问题。(我会将它们拆分为9位数字块,其中的转换给您提供6个字符的版本。)

pack() / unpack() -二进制打包与一些更有效率的相同。如果没有其他办法,请使用它;将您的数字拆分成两个32位片段使其适合。


4

600K是很多元素。如果你愿意尝试替代方法,我个人会使用数据库来处理。然后使用标准的SQL / NoSQL选择语法来提取数据。也许可以使用像garantiadata.com这样的易于托管的内存缓存或Redis。也可以考虑使用APC。


+1 数据库是一个好的解决方案,在 Mac 和 iOS 开发中,他们使用本地数据库来维护应用程序数据。 - Khaled.K

2
我采用了@deceze的答案,并将其包装在一个可以处理32位整数的类中。它是只追加的,但您仍然可以将其用作简单的、内存优化的PHP数组、队列或堆。AppendItem和ItemAt都是O(1)的,而且没有内存开销。我添加了currentPosition/currentSize以避免不必要的fseek函数调用。如果需要限制内存使用并自动切换到临时文件,请使用php://temp
class MemoryOptimizedArray
{
    private $_storage;
    private $_currentPosition;
    private $_currentSize;
    const BYTES_PER_ENTRY = 4;
    function __construct()
    {
        $this->_storage = fopen('php://memory', 'rw+');
        $this->_currentPosition = 0;
        $this->_currentSize = 0;
    }
    function __destruct()
    {
        fclose($this->_storage);
    }
    function AppendItem($value)
    {
        if($this->_currentPosition != $this->_currentSize)
        {
            fseek($this->_storage, SEEK_END);
        }
        fwrite($this->_storage, pack('l', $value));
        $this->_currentSize += self::BYTES_PER_ENTRY;
        $this->_currentPosition = $this->_currentSize;
    }
    function ItemAt($index)
    {
        $itemPosition = $index * self::BYTES_PER_ENTRY;
        if($this->_currentPosition != $itemPosition)
        {
            fseek($this->_storage, $itemPosition);
        }
        $binaryData = fread($this->_storage, self::BYTES_PER_ENTRY);
        $this->_currentPosition = $itemPosition + self::BYTES_PER_ENTRY;
        $unpackedElements = unpack('l', $binaryData);
        return $unpackedElements[1];
    }
}

$arr = new MemoryOptimizedArray();
for($i = 0; $i < 3; $i++)
{
    $v = rand(-2000000000,2000000000);
    $arr->AddToEnd($v);
    print("added $v\n");
}
for($i = 0; $i < 3; $i++)
{
    print($arr->ItemAt($i)."\n");
}
for($i = 2; $i >=0; $i--)
{
    print($arr->ItemAt($i)."\n");
}

1
根据您生成整数的方式,您可能可以使用 PHP 的生成器,假设您正在遍历数组并对单个值执行某些操作。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接