php字符串串联,性能


71

在Java和C#之类的语言中,字符串是不可变的,并且一次构建一个字符的字符串在计算上会非常昂贵。在上述语言中,有一些库类可以降低这种成本,例如C#System.Text.StringBuilder和Javajava.lang.StringBuilder

php(4或5;我对两者都感兴趣)是否都共享此限制?如果是这样,是否有类似的解决方案?

Answers:


62

不,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的字符串类型,它的不同定界符以及每个定界符的含义。


26
并尽可能使用单引号。
斯蒂芬

1
为什么不使用双引号?
特贝

4
@gekannt因为PHP会在双引号内的字符串中扩展/解释变量以及其他转义序列。例如,$x = 5; echo "x = $x";x = 5同时$x = 5; echo 'x = $x';打印x = $x
2013年

人们可能需要对其进行扩展以及不对其进行扩展/解释,这取决于具体情况
Tebe

17
一个神话,单引号的东西:nikic.github.io/2012/01/09/…–
alimack

31

我对此感到很好奇,因此进行了测试。我使用以下代码:

<?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
非常有用,谢谢。字符串串联似乎是解决之道。他们会尝试并从中优化地狱,这是有道理的。
克里斯·米德尔顿

14

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,普通级联:〜6ms
2)PHP,使用StringBuilder:
〜5ms 3)C#,普通级联:〜520ms
4)C#,使用StringBuilder:

100000次迭代:
1)PHP,普通级联:〜63ms
2)PHP,使用StringBuilder:〜555ms
3)C#,普通级联:〜91000ms // !!!
4)C#,使用StringBuilder:〜17ms


Java在这方面与C#大致相同。尽管以后的版本在编译时做了一些优化,以缓解这种情况。过去(在1.4及更低版本中,甚至在1.6中)曾经有过这样的情况:如果要串联3个或更多元素,最好使用StringBuffer / Builder。尽管处于循环中,您仍然需要使用StringBuilder。
A.Grandt 2014年

换句话说,PHP是为那些不想担心低级注意事项的人设计的,它在字符串类型的内部进行字符串缓冲。这与PHP上的“可变”字符串无关。增大字符串的长度仍然需要将内存副本复制到更大的内存中,除非您为其保留一个缓冲区。
thomasrutter

顺便说一句,这应该是公认的答案。当前最重要的答案甚至没有真正回答问题。
thomasrutter

12

当您进行定时比较时,差异是如此之小,以至于它们并不是很相关。从那以后,选择可以使您的代码更易于阅读和理解的内容将会更多。


2
的确,当通常要担心许多更重要的问题(例如数据库设计,大型O()分析和正确的性能分析)时,担心此事完全是愚蠢的。
DGM

2
的确如此,但是我已经看到在Java和C#中使用可变字符串类(vs. s + =“ blah”)的情况确实大大提高了性能。
皮特·阿尔文,2010年

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");
贾巴2010年

7
这在PHP中完全没有必要。实际上,我敢打赌,这比进行常规串联要慢得多。
ryeguy 2011年

9
ryeguy:是的,因为在PHP中字符串是可变的,所以这种方法是“不必要的”,此人要求实现与Java的StringBuilder类似的实现,所以您可以...我不会说它“显着”慢一些,我想您有点戏剧性。实例化管理字符串构建的类的开销可能包括成本,但可以扩展StringBuilder类的用途以包括字符串上的其他方法。我将研究通过在类中实现类似内容实现的额外开销,然后尝试回发。
ossys 2011年

7
...而且再也没有听到他的消息。
Nigralbus 2013年

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

3

我在本文结尾处编写了代码,以测试字符串连接的不同形式,它们在内存和时间占用方面实际上几乎完全相同。

我使用的两种主要方法是将字符串彼此串联,并用字符串填充数组,然后内插它们。我在php 5.6中用1MB字符串做了500个字符串加法(所以结果是500MB字符串)。在测试的每次迭代中,所有内存和时间足迹都非常接近(〜$ IterationNumber * 1MB)。两项测试的运行时间连续为50.398秒和50.843秒,这很可能在可接受的误差范围内。

即使不再离开范围,也不再需要立即对垃圾字符串进行垃圾收集了。由于字符串是可变的,因此在此之后确实不需要额外的内存。

无论其,下面的测试表明,在峰值内存使用量的不同WHILE琴弦被连接起来。

$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个字符连接到字符串上以获取5000万个字节(因此进行了5000万次迭代),在5.97秒内最多花费了50,322,512字节(〜48MB)。在执行数组方法时,最终使用了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>';
}
?>

这次真是万分感谢。array_push比连接代码中的字符串快100倍。
尤玛

2

是。他们是这样。例如,如果您想一起回显几个字符串,请使用

回声str1,str2,str3 

代替

回声str1.str2.str3 
使其更快一点。


这个功能能这样工作吗?$ newstring = str1.srt2.str3; echo $ newstring;
JoshFinnie

1

首先,如果您不需要连接字符串,请不要这样做:这样做总是会更快

echo $a,$b,$c;

echo $a . $b . $c;

但是,至少在PHP5中,字符串连接确实非常快,尤其是在对给定字符串只有一个引用的情况下。我猜解释器StringBuilder内部使用了类似技术。


0

如果您要在PHP字符串中放置变量值,那么我知道使用内联变量包含会更快一些(不是正式名称-我不记得是什么)

$aString = 'oranges';
$compareString = "comparing apples to {$aString}!";
echo $compareString
   comparing apples to oranges!

必须在双引号内起作用。也适用于数组成员(即

echo "You requested page id {$_POST['id']}";


0

我刚遇到这个问题:

$ str。='字符串串联。';

$ 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));

有人可以确认吗?之所以遇到这种情况,是因为我是通过读取并仅将所需的行再次附加到字符串来从大文件中删除一些行的。

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


-4

php中没有此类限制,php可以将strng与dot(。)运算符连接

$a="hello ";
$b="world";
echo $a.$b;

输出“ hello world”


4
人们在这里很快就触发了..我在黑暗中打字..不小心按下了选项卡然后输入..
paan
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.