【问题标题】:PHP – Slow String ManipulationPHP – 慢速字符串操作
【发布时间】:2012-12-04 10:55:20
【问题描述】:

我有一些非常大的数据文件,出于业务原因,我必须进行大量的字符串操作(替换字符和字符串)。这是不可避免的。替换的数量达到数十万。

花费的时间比我想的要长。 PHP 通常非常快,但我正在做很多这样的字符串操作,以至于它变慢了,脚本执行需要几分钟。这很痛苦,因为脚本经常运行。

我做了一些测试,发现 str_replace 最快,其次是 strstr,其次是 preg_replace。我还尝试了单独的 str_replace 语句以及构造模式和替换数组。

我正在考虑隔离字符串操作操作并用不同语言编写的想法,但我不想花时间在那个选项上,只是发现改进可以忽略不计。另外,我只知道 Perl、PHP 和 COBOL,所以对于任何其他语言,我都必须先学习它。

我想知道其他人是如何解决类似问题的?

我已经搜索过,我认为这不会重复任何现有问题。

【问题讨论】:

  • 这是个好问题。 +1 来自我。我看到你正在使用文件。你能以某种方式切换到数据库吗?如果没有,我们可以从文件中看到一些数据吗?
  • 如何读取和处理字符串?您是否对字符串替换与文件或流的实际打开进行了基准测试?平台?
  • 文件中没有什么花哨的东西,只是替换下划线、删除逗号、替换非UTF8字符等问题。
  • 是的,我已经对文件处理进行了基准测试。打开/关闭需要几毫秒。从文件中读取记录非常快,以至于 PHP 在记录时间时会恢复为科学计数法。
  • 是否必须在同一操作中处理每个文件?我猜你无权访问配置文件并增加执行时间?

标签: php string performance


【解决方案1】:

好吧,考虑到在 PHP 中某些字符串操作比数组操作要快,而您仍然对它的速度不满意,您可以按照您提到的那样编写外部程序,可能使用某种“低级”语言。我会推荐 C/C++。

【讨论】:

  • 在开始用 C 编写自己的字符串替换器之前,我先看看现有的工具,例如 sed
【解决方案2】:

有两种处理方式,IMO:

  • [easy] 在后台进程中预先计算一些通用替换并将它们存储在数据库/文件中(这个技巧来自一个游戏开发,其中所有的正弦和余弦都预先计算一次,然后存储在 RAM 中)。不过,您很容易在这里遇到维度灾难;
  • [不太容易] 用 C++ 或其他快速且可编译的编程语言实现替换工具,然后使用它。 Sphinx 是在 C++ 中实现的大型文本数据集的快速操作工具的一个很好的例子。

【讨论】:

  • 即使我预先计算了,我是否仍然需要进行某种运行时搜索来为每次搜索/替换找到我想要的适当的预先计算值?
  • @Simon Roberts,如果没有一些您需要应用的数据和替换示例,很难说。让我们假设你有你的 data.csv。然后你预先计算文件,如 data-no-underscore.csv、data-no-comma.csv、data-no-umlaut.csv、data-no-comma-underscores.csv、data-no-umlaut-underscores.csv 等。然后,对所需文件的搜索是将过滤器按字母顺序排序的内爆连接到所需的数据集名称的业务。
【解决方案3】:

如果您允许在多次执行中处理替换,您可以创建一个处理每个文件的脚本,临时创建具有重复内容的替换文件。这将允许您将数据从一个文件提取到另一个文件,处理副本 - 然后合并更改,或者如果您使用流缓冲区,您可能能够记住每一行,因此可以跳过复制/合并步骤。

但问题可能是您在处理文件时未完成文件,导致文件混合。因此,一个临时文件是合适的。

这将允许脚本运行多次,但仍需进行更改,您只需要一个临时文件来记住已处理的文件。

【讨论】:

  • 我明白你的意思,但我的流程是基本的:获取文件 -> 处理它 -> 将数据放入数据库。我认为这意味着我必须在继续下一步之前处理整个文件。即使我确实拆分了搜索/替换操作,我仍然需要等待所有部分完成才能继续(我认为)。
【解决方案4】:

限制因素是 PHP 重建字符串。考虑:

$out=str_replace('bad', 'good', 'this is a bad example');

在字符串中定位 'bad' 是一个相对低成本的操作,但是为了给替换腾出空间,PHP 必须向上移动,每个字符 e,l,p,m,a,x ,e,写入新值之前的空格。

为 needle 和 haystack 传递数组将提高性能,但效果不如预期。

AFAIK,PHP 没有低级内存访问功能,因此必须用不同的语言编写最佳解决方案,将数据分成“页面”,可以拉伸以适应变化。您可以尝试使用 chunk_split 将字符串分成更小的单元(因此每次替换需要更少的内存处理)。

另一种方法是将其转储到文件中并使用 sed(这仍然一次操作一个搜索/替换),例如

sed -i 's/good/bad/g;s/worse/better/g' file_containing_data

【讨论】:

  • 啊,这很好地解释了为什么搜索比替换要快得多,谢谢。很遗憾我无法预测必要的更改,或者我可以进行搜索和变量重新分配,我认为这会更快。
【解决方案5】:

如果您只需要执行一次此操作并且必须替换为静态内容,则可以使用 Dreamwaver 或其他编辑器,因此您不需要 PHP。它会快得多。

不过,如果您确实需要使用 PHP 动态执行此操作(您需要数据库记录或其他),您可以通过 exec - google search for search-replace 使用 shell 命令

【讨论】:

  • 这假设您在本地有可用的文件。
  • 我猜这意味着创建数据数组以传递给外部函数并解析返回的输入。这可能比一开始在 PHP 脚本中完成所有操作要快吗?
  • 您可以替换整个文件,而不是 str_replace("mike", "george", $string);这意味着很多
【解决方案6】:

您可能遇到了 PHP 的障碍。 PHP 很棒,但在某些领域它失败了,例如处理大量数据。您可以做一些事情:

  1. 使用多个 php 进程来完成任务(2 个进程可能需要一半的时间)。
  2. 安装更快的 CPU。
  3. 在多台机器上进行处理。
  4. 使用编译语言处理数据(Java、C、C++ 等)

【讨论】:

  • 我知道如何编写多线程 PERL,但不知道 PHP。该论坛上有关该主题的问题表明 PHP 不支持它,人们正在使用解决方法来拆分文件等。多台机器对我来说不是一个选择,但无论如何我最终都会遇到与试图躲避相同的问题我期望的多线程 PHP。对我来说,另一种语言似乎是最好的选择。
  • PHP 并不真正支持线程,因此,您要做的就是在 php 中执行:exec("php /myFile.php pram1 param2 > /dev/null 2>&1 &"); 之类的东西,然后您会在文件中像这样得到它们:$param1 = $arg[1];
【解决方案7】:

我认为问题是你为什么经常运行这个脚本?您是一遍又一遍地对相同的数据执行计算(字符串替换),还是每次都对不同的数据执行计算?

如果答案是前者,那么您在 PHP 端就无法提高性能了。您可以通过其他方式提高性能,例如使用更好的硬件(SSD 用于更快地读取/写入文件)、多核 CPU 以及将数据分解为同时运行多个脚本以同时处理数据的小块,以及更快的 RAM (即更高的公交车速度)。

如果答案是后者,那么您可能需要考虑使用诸如 memcached 或 reddis(键/值缓存存储)之类的东西来缓存结果,以便您只能执行一次计算,然后它只是从内存中线性读取,这非常便宜并且几乎不涉及 CPU 开销(您也可以在此级别使用 CPU 缓存)。

PHP 中的字符串操作已经很便宜了,因为 PHP 字符串本质上只是字节数组。在将文件读入内存并将其存储在字符串中时,PHP 几乎没有任何开销。如果您有一些示例代码来演示您在哪里看到性能问题和一些基准数字,我可能会有一些更好的建议,但现在看起来您需要根据您的潜在需求重构您的方法。

例如,当您在不同情况下处理数据时,需要单独考虑 CPU 和 I/O 成本。 I/O 涉及阻塞,因为它是一个系统调用。这意味着您的 CPU 必须等待更多数据通过线路(同时您的磁盘将数据传输到内存)才能继续处理或计算该数据。你的 CPU 总是比内存快得多,而内存总是比磁盘快得多。

这里有一个简单的基准来向您展示差异:

/* First, let's create a simple test file to benchmark */
file_put_contents('in.txt', str_repeat(implode(" ",range('a','z')),10000));

/* Now let's write two different tests that replace all vowels with asterisks */

// The first test reads the entire file into memory and performs the computation all at once

function test1($filename, $newfile) {
    $start = microtime(true);
    $data = file_get_contents($filename);
    $changes = str_replace(array('a','e','i','o','u'),array('*'),$data);
    file_put_contents($newfile,$changes);
    return sprintf("%.6f", microtime(true) - $start);
}

// The second test reads only 8KB chunks at a time and performs the computation on each chunk

function test2($filename, $newfile) {
    $start = microtime(true);
    $fp = fopen($filename,"r");
    $changes = '';
    while(!feof($fp)) {
        $changes .= str_replace(array('a','e','i','o','u'),array('*'),fread($fp, 8192));
    }
    file_put_contents($newfile, $changes);
    return sprintf("%.6f", microtime(true) - $start);
}

上述两个测试做同样的事情,但是当我使用少量数据时,Test2 对我来说明显更快(在这个测试中大约 500KB) .

这是您可以运行的基准...

// Conduct 100 iterations of each test and average the results
for ($i = 0; $i < 100; $i++) {
    $test1[] = test1('in.txt','out.txt');
    $test2[] = test2('in.txt','out.txt');
}
echo "Test1 average: ", sprintf("%.6f",array_sum($test1) / count($test1)), "\n",
     "Test2 average: ", sprintf("%.6f\n",array_sum($test2) / count($test2));

对我来说,上面的基准测试给出了Test1 average: 0.440795Test2 average: 0.052054,这是一个数量级的差异,这只是对 500KB 的数据进行测试。现在,如果我将此文件的大小增加到大约 50MB,Test1 实际上会更快,因为每次迭代的系统 I/O 调用更少(即 我们只是线性地从内存中读取在 Test1 中),但更多的 CPU 成本(即我们在每次迭代中执行更大的计算)。事实证明,CPU 一次能够处理比您的 I/O 设备通过总线发送的数据量大得多的数据。

因此,在大多数情况下,这不是一种万能的解决方案。

【讨论】:

  • 每次取值相同还是不同尚不清楚,所以每次都需要测试。在阅读 symcbean 的答案后,我尝试在替换之前进行评估,但它实际上稍微增加了处理时间——大概是因为 str_replace 本身包含一个匹配测试,所以我有效地测试了两次。文件访问时间不是问题(毫秒)。在处理之前,完整的文件会被放入数组中。 str_replace 的主题是数组元素,而不是直接从文件中读取的字符串。感谢您的详细回复。
【解决方案8】:

由于您了解 Perl,我建议您使用正则表达式在 perl 中进行字符串操作,并在 PHP 网页中使用最终结果。

这似乎更好,原因如下

  1. 你已经知道 Perl
  2. Perl 更好地处理字符串

您可以仅在必要时使用 PHP。

【讨论】:

  • 是的,我认为这对我来说是一个不错的选择。我必须进行一些测试,看看 PERL 的速度有多快。
【解决方案9】:

这种操作必须即时进行吗?如果没有,我可以建议预处理...也许通过 cron 作业。

定义你将要使用的规则。 它只是一个 str_replace 还是几个不同的? 你必须一次性完成整个文件吗?或者你能把它分成多批吗? (例如一次半个文件)

一旦定义了规则,就决定何时进行处理。 (例如,早上 6 点,大家开始工作之前)

然后您可以设置作业队列。我已经使用 apache 的 cron 作业在给定的时间安排上运行我的 php 脚本。

对于我不久前从事的一个项目,我有这样的设置:

7:00 - pull 10,000 records from mysql and write them to 3 separate files.
7:15 - run a complex regex on file one.
7:20 - run a complex regex on file two.
7:25 - run a complex regex on file three.
7:30 - combine all three files into one.
8:00 - walk into the metting with the formatted file you boss wants. *profit*

希望这能帮助你思考......

【讨论】:

    猜你喜欢
    • 2012-01-07
    • 1970-01-01
    • 2013-08-09
    • 1970-01-01
    • 2011-03-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多