PHP与百万数组宝贝

Posted

技术标签:

【中文标题】PHP与百万数组宝贝【英文标题】:PHP and the million array baby 【发布时间】:2012-07-26 23:33:07 【问题描述】:

假设您有以下整数数组:

array(1, 2, 1, 0, 0, 1, 2, 4, 3, 2, [...] );

整数最多可达一百万个条目;只是它们没有被硬编码,而是预先生成并存储在 JSON 格式的文件中(大小约为 2MB)。这些整数的顺序很重要,我不能每次都随机生成它,因为它应该是一致的,并且在相同的索引处始终具有相同的值。

如果之后在 php 中读回此文件(例如,使用 file_get_contents + json_decode),则需要 700 到 900 毫秒才能恢复数组 — “好吧”我想,“这可能是合理的,因为 json_decode必须解析大约 200 万个字符,让我们缓存它”。 APC 将其缓存在大约 68MB 的条目中,可能是正常的,zval 很大。 然而,从 APC 检索这个数组也需要一些不错的 600 毫秒,这在我看来仍然太多了。

编辑:APC 确实序列化/反序列化以存储和检索具有一百万个项目数组的内容是一个漫长而繁重的过程。

所以问题:

如果我打算在 PHP 中加载一百万个条目数组(无论是数据存储还是方法),我是否应该预料到这种延迟?据我了解,APC 存储 zval 本身,因此理论上从 APC 检索它应该尽可能快(无解析、无转换、无磁盘访问)

为什么 APC 对于看似简单的事情却这么慢?

是否有任何有效的方法可以使用 PHP 将一百万个条目数组完全加载到内存中?假设 RAM 使用没有问题。

1234563这样做,但我想知道所有方面),对于完整的阵列,最有效的数据存储系统是什么?显然不是 RDBM;我在考虑 redis,但我很乐意听到其他想法。

【问题讨论】:

你试过SplFixedArray吗? @Buddy 是的,差别不大,可能使用更少的内存,但 APC 需要同样长的时间。 如果数字很小并且数组是静态的,你不能用一个1Mb的字符串对象来代替吗? @6502 好主意。我会调查一下。 Redis lists 也值得一试。这里唯一的问题是您需要将整数存储为字符串,但我肯定会尝试这样做。 【参考方案1】:

APC 存储已序列化的数据,因此在从 APC 加载回数据时必须对其进行反序列化。这就是您的开销所在。

加载它的最有效方法是以 PHP 和 include() 的形式写入文件,但是对于包含一百万个元素的数组,你永远不会有任何效率水平......它需要大量的内存,加载需要时间。这就是发明数据库的原因,那么您对数据库有什么问题?

编辑

如果你想加快序列化/反序列化,看看igbinary扩展

【讨论】:

是的,我从未听说过数据库 :) 没有什么反对数据库的,我只是想,因为数据是静态的,所以像 APC 一样存在于内存中的简单的东西会做得更好;虽然很遗憾发现 APC 序列化了数据,但我认为情况并非如此。 您会发现大多数缓存(APC、memcache、redis 等)都需要序列化数据,因为它们被设计为跨平台工具,因此不是专门为 PHP 数据类型/zvals 设计的. @MarkBaker 但就是这样,APC 是专门作为 PHP 扩展创建的,理论上应该可以直接与 zvals 一起使用。 APC 是为 PHP 编写的,作为操作码缓存,具有存储序列化用户数据的附带好处,但用户数据存储不是特定于 PHP zvals,而是简单的字符串存储(因此是序列化的) @MarkBaker 感谢您提到 igbinary,我会检查一下。【参考方案2】:

我不能每次都随机生成它,因为它应该是一致的,并且总是在相同的索引处具有相同的值。

你读过伪随机数吗?有一个叫做种子的小东西可以解决这个问题。

还可以对您的选项和声明进行基准测试。您是否对 file_get_contents 与 json_decode 进行了计时?这里需要在存储和访问成本之间进行权衡。例如。如果您的数字是 0..9(或 0..255),那么将它们存储在 2Mb 字符串中并在此使用访问函数可能会更容易。无论是从 FS 还是 APC,2Mb 的加载速度都会更快。

【讨论】:

是的,通过算法生成相同的基于固定种子的整数列表是我正在考虑的选项之一,并且可能是最优雅的;我会研究伪随机数,看看它是否适合我的需要。【参考方案3】:

正如 Mark 所说,这就是创建数据库的原因 - 允许您根据常规使用模式有效地搜索(和操作,但您可能不需要)数据。它也可能比使用数组实现自己的搜索更快。我猜我们说的是每次访问数组时都会有近 2-300MB 的数据(序列化之前)被序列化和反序列化。

如果您想加快速度,请尝试分别分配数组的每个元素 - 您可能会用函数调用开销换取序列化所花费的时间。你也可以用你自己的扩展来扩展它,将你的数据集包装在一个小的检索界面中。

我猜你不能直接存储 zval 的原因是因为它们包含内部状态,你根本不能将变量符号表指向前一个表。

【讨论】:

【参考方案4】:

假设整数都是 0-15。然后每个字节可以存储 2 个:

<?php
$data = '';
for ($i = 0; $i < 500000; ++$i)
  $data .= chr(mt_rand(0, 255));

echo serialize($data);

运行:php ints.php &gt; ints.ser

现在您有了一个包含 500000 字节字符串的文件,其中包含 1,000,000 个从 0 到 15 的随机整数。

加载:

<?php
$data = unserialize(file_get_contents('ints.ser'));

function get_data_at($data, $i)

  $data = ord($data[$i >> 1]);

  return ($i & 1) ? $data & 0xf : $data >> 4;


for ($i = 0; $i < 1000; ++$i)
  echo get_data_at($data, $i), "\n";

我机器上的加载时间约为 0.002 秒。

当然,这可能并不直接适用于您的情况,但它比包含一百万个条目的臃肿 PHP 数组要快得多。坦率地说,在 PHP 中拥有这么大的数组绝不是正确的解决方案。

我也不是说这是正确的解决方案,但如果它符合您的参数,它肯定是可行的。

请注意,如果您的数组包含 0-255 范围内的整数,您可以摆脱包装,只需以 ord($data[$i]) 访问数据。在这种情况下,您的字符串将是 1M 字节长。

最后,根据file_get_contents()的文档,php会对文件进行内存映射。如果是这样,您最好的表现是将原始字节转储到文件中,并像这样使用它:

$ints = file_get_contents('ints.raw');
echo ord($ints[25]);

这假设ints.raw 正好是一百万字节长。

【讨论】:

马修,感谢我的建议的速记版本。几点:是的f_g_s() 在大多数情况下都在文件上使用 mmap(例如,不用于 NFS 挂载的文件),但它仍然会将内容复制到本地分配的字符串中。是的,ord($ints[NNN])access 生成最有效的操作码序列。您的 2mS 是因为文件内容是 VFAT 缓存的。在生产服务器上可能不是这种情况。 这基本上是我在 6502 和 TerryE 建议时所想像的,但还是很高兴看到它写出来。那些 2ms 可能是因为文件像@TerryE 提到的那样被缓存,但是很有可能它可以很好地缓存在 APC 中而没有反序列化开销。我去看看。 确实,从 APC 存储和检索它可以轻而易举地完成。我会接受这个答案,因为在性能和内存方面,如果要将整个数组存储在内存中,它可能会尽可能好;我是否应该将整个内容存储在内存中是另一个我尚未决定的问题,但与此同时,这会做得更好。

以上是关于PHP与百万数组宝贝的主要内容,如果未能解决你的问题,请参考以下文章

百万程序员入门Python都在学的“笨办法”,进阶篇来喽!

如何在 PHP 中为数字(千、十万、百万、十亿)添加逗号分隔符

视频 | PHP协程,并发百万的协程场景使用及分析

Php导出百万数据的优化

Php导出百万数据的优化

PHP 中一个 False 引发的问题,差点让公司损失一百万