PHP字符串拼接,性能。

77
在Java和C#等语言中,字符串是不可变的,每次逐个字符构建字符串的计算成本很高。在这些语言中,有一些库类可以降低这种成本,例如C#的System.Text.StringBuilder和Java的java.lang.StringBuilder
PHP(包括版本4和5)是否也存在这种限制?如果存在,是否有类似的解决方案可用?
12个回答

66
不,PHP中没有Stringbuilder类,因为字符串是可变的。
话虽如此,根据你要做的事情,有不同的构建字符串的方法。
例如,echo可以接受逗号分隔的标记进行输出。
// This...
echo 'one', 'two';

// Is the same as this
echo 'one';
echo 'two';

这意味着你可以输出一个复杂的字符串,而不必使用拼接,这样会更慢。
// This...
echo 'one', 'two';

// Is faster than this...
echo 'one' . 'two';

如果你需要将这个输出保存在一个变量中,你可以使用输出缓冲函数来实现。
另外,PHP的数组性能非常好。如果你想要做类似逗号分隔的值列表,只需使用implode()函数即可。
$values = array( 'one', 'two', 'three' );
$valueList = implode( ', ', $values );

最后,确保你熟悉PHP的字符串类型及其不同的分隔符,以及每个分隔符的影响。

30
尽可能使用单引号。 - Stephen
1
为什么不使用双引号? - Tebe
6
因为PHP会在双引号中解析变量以及转义序列,所以例子中的$x = 5; echo "x = $x";会输出x = 5,而$x = 5; echo 'x = $x';则会输出x = $x - samitny
有时需要将其扩展,有时则不需要,这取决于情况。 - Tebe
21
单引号性能的神话并不属实:http://nikic.github.io/2012/01/09/Disproving-the-Single-Quotes-Performance-Myth.html - alimack
显示剩余2条评论

36

我对此很好奇,所以我进行了一项测试。我使用了以下代码:

<?php
ini_set('memory_limit', '1024M');
define ('CORE_PATH', '/Users/foo');
define ('DS', DIRECTORY_SEPARATOR);

$numtests = 1000000;

function test1($numtests)
{
    $CORE_PATH = '/Users/foo';
    $DS = DIRECTORY_SEPARATOR;
    $a = array();

    $startmem = memory_get_usage();
    $a_start = microtime(true);
    for ($i = 0; $i < $numtests; $i++) {
        $a[] = sprintf('%s%sDesktop%sjunk.php', $CORE_PATH, $DS, $DS);
    }
    $a_end = microtime(true);
    $a_mem = memory_get_usage();

    $timeused = $a_end - $a_start;
    $memused = $a_mem - $startmem;

    echo "TEST 1: sprintf()\n";
    echo "TIME: {$timeused}\nMEMORY: $memused\n\n\n";
}

function test2($numtests)
{
    $CORE_PATH = '/Users/shigh';
    $DS = DIRECTORY_SEPARATOR;
    $a = array();

    $startmem = memory_get_usage();
    $a_start = microtime(true);
    for ($i = 0; $i < $numtests; $i++) {
        $a[] = $CORE_PATH . $DS . 'Desktop' . $DS . 'junk.php';
    }
    $a_end = microtime(true);
    $a_mem = memory_get_usage();

    $timeused = $a_end - $a_start;
    $memused = $a_mem - $startmem;

    echo "TEST 2: Concatenation\n";
    echo "TIME: {$timeused}\nMEMORY: $memused\n\n\n";
}

function test3($numtests)
{
    $CORE_PATH = '/Users/shigh';
    $DS = DIRECTORY_SEPARATOR;
    $a = array();

    $startmem = memory_get_usage();
    $a_start = microtime(true);
    for ($i = 0; $i < $numtests; $i++) {
        ob_start();
        echo $CORE_PATH,$DS,'Desktop',$DS,'junk.php';
        $aa = ob_get_contents();
        ob_end_clean();
        $a[] = $aa;
    }
    $a_end = microtime(true);
    $a_mem = memory_get_usage();

    $timeused = $a_end - $a_start;
    $memused = $a_mem - $startmem;

    echo "TEST 3: Buffering Method\n";
    echo "TIME: {$timeused}\nMEMORY: $memused\n\n\n";
}

function test4($numtests)
{
    $CORE_PATH = '/Users/shigh';
    $DS = DIRECTORY_SEPARATOR;
    $a = array();

    $startmem = memory_get_usage();
    $a_start = microtime(true);
    for ($i = 0; $i < $numtests; $i++) {
        $a[] = "{$CORE_PATH}{$DS}Desktop{$DS}junk.php";
    }
    $a_end = microtime(true);
    $a_mem = memory_get_usage();

    $timeused = $a_end - $a_start;
    $memused = $a_mem - $startmem;

    echo "TEST 4: Braced in-line variables\n";
    echo "TIME: {$timeused}\nMEMORY: $memused\n\n\n";
}

function test5($numtests)
{
    $a = array();

    $startmem = memory_get_usage();
    $a_start = microtime(true);
    for ($i = 0; $i < $numtests; $i++) {
        $CORE_PATH = CORE_PATH;
        $DS = DIRECTORY_SEPARATOR;
        $a[] = "{$CORE_PATH}{$DS}Desktop{$DS}junk.php";
    }
    $a_end = microtime(true);
    $a_mem = memory_get_usage();

    $timeused = $a_end - $a_start;
    $memused = $a_mem - $startmem;

    echo "TEST 5: Braced inline variables with loop-level assignments\n";
    echo "TIME: {$timeused}\nMEMORY: $memused\n\n\n";
}

test1($numtests);
test2($numtests);
test3($numtests);
test4($numtests);
test5($numtests);

通过图表可以看出,使用sprintf来进行格式化输出是时间和内存消耗最高的方法。


1
应该再加一个测试:类似于test2,但将.替换为,(当然不使用输出缓冲区)。 - Raptor
1
非常有用,谢谢。字符串连接似乎是正确的方法。他们试图将其优化到极致是有道理的。 - Chris Middleton

16

在PHP中,不需要StringBuilder类的模拟。

我进行了几个简单的测试:

在PHP中:

$iterations = 10000;
$stringToAppend = 'TESTSTR';
$timer = new Timer(); // based on microtime()
$s = '';
for($i = 0; $i < $iterations; $i++)
{
    $s .= ($i . $stringToAppend);
}
$timer->VarDumpCurrentTimerValue();

$timer->Restart();

// Used purlogic's implementation.
// I tried other implementations, but they are not faster
$sb = new StringBuilder(); 

for($i = 0; $i < $iterations; $i++)
{
    $sb->append($i);
    $sb->append($stringToAppend);
}
$ss = $sb->toString();
$timer->VarDumpCurrentTimerValue();

在 C# (.NET 4.0) 中:

const int iterations = 10000;
const string stringToAppend = "TESTSTR";
string s = "";
var timer = new Timer(); // based on StopWatch

for(int i = 0; i < iterations; i++)
{
    s += (i + stringToAppend);
}

timer.ShowCurrentTimerValue();

timer.Restart();

var sb = new StringBuilder();

for(int i = 0; i < iterations; i++)
{
    sb.Append(i);
    sb.Append(stringToAppend);
}

string ss = sb.ToString();

timer.ShowCurrentTimerValue();

结果:

10000次迭代:
1)PHP,普通连接:约6毫秒
2)PHP,使用StringBuilder:约5毫秒
3)C#,普通连接:约520毫秒
4)C#,使用StringBuilder:约1毫秒

100000次迭代:
1)PHP,普通连接:约63毫秒
2)PHP,使用StringBuilder:约555毫秒
3)C#,普通连接:约91000毫秒 // !!!
4)C#,使用StringBuilder:约17毫秒


Java在这方面与C#差不多。尽管后来的版本在编译时进行了一些优化以帮助缓解这种情况。过去(在1.4及更早版本中,甚至可能在1.6中)如果您有3个或更多元素要连接,则最好使用StringBuffer/Builder。但是在循环中,仍然需要使用StringBuilder。 - A.Grandt
换句话说,PHP是为那些不想担心低级细节的人设计的,并且它在字符串类型上内部执行字符串缓冲。这与PHP中的字符串“可变”无关;增加字符串长度仍然需要将其复制到更大的内存块中,除非您维护一个缓冲区以供其扩展。 - thomasrutter
顺便说一句,这应该是被接受的答案。目前排名靠前的答案甚至没有真正回答问题。 - thomasrutter

12

当你进行定时比较时,差异非常小,这并不太相关。更明智的选择是采用使您的代码更易于阅读和理解的选项。


2
实际上,担心这个问题纯粹是愚蠢的,因为通常有更重要的问题需要关注,比如数据库设计、大O()分析和正确的性能分析。 - DGM
2
这很正确,但我确实见过在Java和C#中使用可变字符串类(而不是 s += "blah")的情况,确实可以极大地提高性能。 - Pete Alvin
这种性能优化在你需要在循环中操作包含数十万个字符的字符串时变得非常重要,而循环只有当 PHP 超出执行时间或内存时才会被中断 - 这也是我的情况。 - Lucas Bustamante

10

我知道你在说什么。我刚刚创建了这个简单的类来模拟Java的StringBuilder类。

class StringBuilder {

  private $str = array();

  public function __construct() { }

  public function append($str) {
    $this->str[] = $str;
  }

  public function toString() {
    return implode($this->str);
  }

}

9
好的解决方案。在append函数的结尾处,您可以添加return $this;以允许方法链接:$sb->append("one")->append("two"); - Jabba
8
在 PHP 中这是完全不必要的。事实上,我敢打赌这比普通的连接操作慢得多。 - ryeguy
10
ryeguy: 确实,由于PHP中的字符串是可变的,因此这种方法是“不必要的”。该人要求一个类似于Java StringBuilder的实现,所以在这里提供了一个...我不会说它的速度“显著”较慢,我认为你有点夸张了。实例化一个管理字符串构建的类的开销可能会包括成本,但是StringBuilder类的有用性可以扩展到包括附加在字符串上的其他方法。我将研究在类中实现类似内容时产生的额外开销,并尝试发布回复。 - ossys
7
他再也没有音讯。 - Nigralbus

6

PHP字符串是可变的。您可以像这样更改特定字符:

$string = 'abc';
$string[2] = 'a'; // $string equals 'aba'
$string[3] = 'd'; // $string equals 'abad'
$string[5] = 'e'; // $string equals 'abad e' (fills character(s) in between with spaces)

您可以像这样将字符附加到字符串中:

$string .= 'a';

我不是php专家。"$string .= 'a'"不是"$string = $string . 'a'"的简写吗?此时php不会创建一个新的字符串(而不是改变旧的字符串)吗? - Wolfgang Adamec
是的,这是一个简短的形式。但是对于你的第二个问题,PHP的内部行为实际上类似于用一个字节更长的字符串替换该字符串。不过在内部,它像StringBuilder一样进行缓冲。 - thomasrutter

4
我编写了这篇文章底部的代码来测试不同形式的字符串连接,它们在内存和时间印记方面几乎完全相等。我使用了两种主要方法:将字符串连接到一起和用字符串填充数组,然后再合并它们。我在php 5.6中使用1MB的字符串进行了500次字符串添加操作(因此结果是一个500MB的字符串)。在测试的每个迭代中,所有的内存和时间印迹都非常接近(在~$IterationNumber*1MB的范围内)。两个测试的运行时间分别为50.398秒和50.843秒,很可能在可接受的误差范围内。 似乎对于不再被引用的字符串进行垃圾收集是相当即时的,即使从未离开作用域。由于字符串是可变的,在事实上之后并不需要额外的内存。 然而,以下测试表明,在字符串被连接时,峰值内存使用存在差异。
$OneMB=str_repeat('x', 1024*1024);
$Final=$OneMB.$OneMB.$OneMB.$OneMB.$OneMB;
print memory_get_peak_usage();

结果=10,806,800字节(约10MB,不包括初始PHP内存占用)

$OneMB=str_repeat('x', 1024*1024);
$Final=implode('', Array($OneMB, $OneMB, $OneMB, $OneMB, $OneMB));
print memory_get_peak_usage();

结果=6,613,320字节(~6MB,不包括PHP的初始内存占用)

因此,在非常大的字符串连接中,实际上存在可能相当显著的差异(我在创建非常大的数据集或SQL查询时遇到过这种情况)。

但是,即使是这个事实也是有争议的,具体取决于数据。例如,将1个字符连接到字符串上以获得50百万字节(因此需要5000万次迭代)最多需要50,322,512字节(约48MB),用时5.97秒。而使用数组方法则需要7,337,107,176字节(约6.8GB)来创建数组,用时12.1秒,然后再花费额外的4.32秒将字符串从数组中组合起来。

总之...以下是我在开头提到的基准代码,它显示了这些方法基本上是相等的。它输出一个漂亮的HTML表格。

<?
//Please note, for the recursion test to go beyond 256, xdebug.max_nesting_level needs to be raised. You also may need to update your memory_limit depending on the number of iterations

//Output the start memory
print 'Start: '.memory_get_usage()."B<br><br>Below test results are in MB<br>";

//Our 1MB string
global $OneMB, $NumIterations;
$OneMB=str_repeat('x', 1024*1024);
$NumIterations=500;

//Run the tests
$ConcatTest=RunTest('ConcatTest');
$ImplodeTest=RunTest('ImplodeTest');
$RecurseTest=RunTest('RecurseTest');

//Output the results in a table
OutputResults(
  Array('ConcatTest', 'ImplodeTest', 'RecurseTest'),
  Array($ConcatTest, $ImplodeTest, $RecurseTest)
);

//Start a test run by initializing the array that will hold the results and manipulating those results after the test is complete
function RunTest($TestName)
{
  $CurrentTestNums=Array();
  $TestStartMem=memory_get_usage();
  $StartTime=microtime(true);
  RunTestReal($TestName, $CurrentTestNums, $StrLen);
  $CurrentTestNums[]=memory_get_usage();

  //Subtract $TestStartMem from all other numbers
  foreach($CurrentTestNums as &$Num)
    $Num-=$TestStartMem;
  unset($Num);

  $CurrentTestNums[]=$StrLen;
  $CurrentTestNums[]=microtime(true)-$StartTime;

  return $CurrentTestNums;
}

//Initialize the test and store the memory allocated at the end of the test, with the result
function RunTestReal($TestName, &$CurrentTestNums, &$StrLen)
{
  $R=$TestName($CurrentTestNums);
  $CurrentTestNums[]=memory_get_usage();
  $StrLen=strlen($R);
}

//Concatenate 1MB string over and over onto a single string
function ConcatTest(&$CurrentTestNums)
{
  global $OneMB, $NumIterations;
  $Result='';
  for($i=0;$i<$NumIterations;$i++)
  {
    $Result.=$OneMB;
    $CurrentTestNums[]=memory_get_usage();
  }
  return $Result;
}

//Create an array of 1MB strings and then join w/ an implode
function ImplodeTest(&$CurrentTestNums)
{
  global $OneMB, $NumIterations;
  $Result=Array();
  for($i=0;$i<$NumIterations;$i++)
  {
    $Result[]=$OneMB;
    $CurrentTestNums[]=memory_get_usage();
  }
  return implode('', $Result);
}

//Recursively add strings onto each other
function RecurseTest(&$CurrentTestNums, $TestNum=0)
{
  Global $OneMB, $NumIterations;
  if($TestNum==$NumIterations)
    return '';

  $NewStr=RecurseTest($CurrentTestNums, $TestNum+1).$OneMB;
  $CurrentTestNums[]=memory_get_usage();
  return $NewStr;
}

//Output the results in a table
function OutputResults($TestNames, $TestResults)
{
  global $NumIterations;
  print '<table border=1 cellspacing=0 cellpadding=2><tr><th>Test Name</th><th>'.implode('</th><th>', $TestNames).'</th></tr>';
  $FinalNames=Array('Final Result', 'Clean');
  for($i=0;$i<$NumIterations+2;$i++)
  {
    $TestName=($i<$NumIterations ? $i : $FinalNames[$i-$NumIterations]);
    print "<tr><th>$TestName</th>";
    foreach($TestResults as $TR)
      printf('<td>%07.4f</td>', $TR[$i]/1024/1024);
    print '</tr>';
  }

  //Other result numbers
  print '<tr><th>Final String Size</th>';
  foreach($TestResults as $TR)
    printf('<td>%d</td>', $TR[$NumIterations+2]);
  print '</tr><tr><th>Runtime</th>';
    foreach($TestResults as $TR)
      printf('<td>%s</td>', $TR[$NumIterations+3]);
  print '</tr></table>';
}
?>

1
谢谢您。在我的代码中,array_push 比字符串连接快了 100 倍。 - Kyouma

4

我刚遇到了这个问题:

$str .= '字符串连接。';

vs.

$str = $str . '字符串连接。';

迄今为止似乎没有人在这里进行过比较。使用50,000次迭代和PHP 7.4,结果相当惊人:

字符串1:0.0013918876647949

字符串2:1.1183910369873

倍数:803 !!!

$currentTime = microtime(true);
$str = '';
for ($i = 50000; $i > 0; $i--) {
    $str .= 'String concatenation. ';
}
$currentTime2 = microtime(true);
echo "String 1: " . ( $currentTime2 - $currentTime);

$str = '';
for ($i = 50000; $i > 0; $i--) {
    $str = $str . 'String concatenation. ';
}
$currentTime3 = microtime(true);
echo "<br>String 2: " . ($currentTime3 - $currentTime2);

echo "<br><br>Faktor: " . (($currentTime3 - $currentTime2) / ( $currentTime2 - $currentTime));

有人能确认这个吗?我遇到了这个问题,因为我在读取大文件时删除了一些行,只将想要的行附加到一个字符串中。

使用“.=”解决了我这里所有的问题。之前我会超时!


确认了,我注意到在嵌入式设备上将40k行SQL连接成字符串的某些事情。差别很大! - Geoffrey VL
如果您启用了Opcache,并且将opcache.optimization_level设置为非零值(我的值是0x7FFEBFFF),那么这两个测试的速度几乎相同。 - Mike Richardson

2
是的。它们确实可以。例如,如果您想将几个字符串一起输出,请使用

echo str1,str2,str3 

而不是

echo str1.str2.str3 
以获得更快的速度。


这个函数是这样工作的吗?$newstring = str1.srt2.str3; echo $newstring; - JoshFinnie

1
首先,如果您不需要将字符串连接起来,请不要这样做:这样做总是比较快的。
echo $a,$b,$c;

echo $a . $b . $c;

然而,在PHP5中,字符串连接非常快,特别是如果对于给定的字符串只有一个引用。我猜解释器在内部使用了类似于StringBuilder的技术。


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