【问题标题】:php String Concatenation, Performancephp 字符串连接,性能
【发布时间】:2010-09-12 13:12:30
【问题描述】:

在 Java 和 C# 等语言中,字符串是不可变的,一次构建一个字符串的计算成本可能很高。在上述语言中,有一些库类可以降低这种成本,例如 C# System.Text.StringBuilder 和 Java java.lang.StringBuilder

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

【问题讨论】:

    标签: php string concatenation


    【解决方案1】:

    不,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';
    

    如果您需要在变量中捕获此输出,您可以使用output buffering functions 来实现。

    另外,PHP 的数组性能非常好。如果你想做一个逗号分隔的值列表,只需使用 implode()

    $values = array( 'one', 'two', 'three' );
    $valueList = implode( ', ', $values );
    

    最后,确保您熟悉 PHP's string type,它是不同的分隔符,以及每个分隔符的含义。

    【讨论】:

    • 尽可能使用单引号。
    • 为什么不用双引号?
    • @gekannt 因为 PHP 扩展/解释变量以及用双引号括起来的字符串中的额外转义序列。例如,$x = 5; echo "x = $x"; 将打印 x = 5$x = 5; echo 'x = $x'; 将打印 x = $x
    • 可以需要扩展也可以不扩展/解释,视情况而定
    • 有点神话,单引号的事情:nikic.github.io/2012/01/09/…
    【解决方案2】:

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

    <?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,但将 . 替换为 ,(当然没有输出缓冲区)
    • 非常有用,谢谢。字符串连接似乎是要走的路。他们会尝试优化这一点是有道理的。
    【解决方案3】:

    在 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:~5 毫秒
    3) C#,普通串联:~520ms
    4) C#,使用 StringBuilder:~1ms

    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。
    • 换句话说,PHP 是为那些不想担心底层考虑的人设计的,它在字符串类型内部进行字符串缓冲。这与 PHP 上的字符串“可变”无关。增加字符串的长度仍然需要将内存复制到更大的内存,除非您维护一个缓冲区以使其增长。
    • 顺便说一句,这应该是公认的答案。当前的热门答案甚至没有真正回答这个问题。
    【解决方案4】:

    当您进行定时比较时,差异是如此之小以至于它不是很相关。选择使您的代码更易于阅读和理解的选择会带来更多收益。

    【讨论】:

    • 确实,担心这个完全是愚蠢的,因为通常有更重要的问题需要担心,比如数据库设计、大 O() 分析和正确的分析。
    • 确实如此,但我已经看到在 Java 和 C# 中使用可变字符串类(与 s += "blah" 相比)确实显着提高了性能的情况。
    • 这种性能优化很重要,当您必须在一个 while 循环中操作具有数十万个字符的字符串时,只有当 PHP 超出执行时间或内存时才会中断 - 我的情况
    【解决方案5】:

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

    class StringBuilder {
    
      private $str = array();
    
      public function __construct() { }
    
      public function append($str) {
        $this->str[] = $str;
      }
    
      public function toString() {
        return implode($this->str);
      }
    
    }
    

    【讨论】:

    • 不错的解决方案。在append 函数的末尾,您可以添加return $this; 以允许方法链接:$sb-&gt;append("one")-&gt;append("two");
    • 这在 PHP 中是完全没有必要的。事实上,我敢打赌,这比进行常规连接要慢得多。
    • ryeguy:是的,因为字符串在 PHP 中是可变的,所以这种方法是“不必要的”,该人要求提供与 Java 的 StringBuilder 类似的实现,所以你去吧......我不会说它是“显着”慢,我认为你有点戏剧化。实例化管理字符串构建的类的开销可能包括成本,但可以扩展 StringBuilder 类的有用性以包括字符串上的其他方法。我将研究通过在类中实现类似的东西并尝试回发来实现额外的开销。
    • ...他再也没有音讯了。
    【解决方案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 没有创建新字符串(也没有更改旧字符串)?
    • 是的,它是一个简短的形式。但是对于您的第二个问题,PHP 的内部行为实际上就像用一个字节长的字符串替换字符串一样。但在内部,它确实像 StringBuilder 一样缓冲。
    【解决方案7】:

    我在本文末尾编写了代码来测试不同形式的字符串连接,它们在内存和时间足迹上几乎完全相同。

    我使用的两种主要方法是将字符串相互连接,然后用字符串填充数组,然后将它们内爆。我在 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 个字符连接到一个字符串以获得 5000 万字节(即 5000 万次迭代)在 5.97 秒内最多需要 50,322,512 字节(~48MB)。在执行数组方法时,最终使用 7,337,107,176 字节(~6.8GB)在 12.1 秒内创建了数组,然后又花费了 4.32 秒来组合数组中的字符串。

    Anywho... 下面是我在开头提到的基准代码,它显示方法几乎相同。它会输出一个漂亮的 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 倍。
    【解决方案8】:

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

    回声 str1,str2,str3

    而不是

    echo str1.str2.str3 让它更快一点。

    【讨论】:

    • 这个功能是这样工作的吗? $newstring = str1.srt2.str3;回声 $newstring;
    【解决方案9】:

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

    echo $a,$b,$c;
    

    echo $a . $b . $c;
    

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

    【讨论】:

      【解决方案10】:

      我刚遇到这个问题:

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

      对比

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

      到目前为止,似乎没有人对此进行过比较。 50.000 次迭代和 php 7.4 的结果非常疯狂:

      字符串 1:0.0013918876647949

      字符串 2:1.1183910369873

      Faktor: 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));
      

      有人可以确认吗?我遇到了这个问题,因为我从一个大文件中删除了一些行,方法是读取并再次将想要的行附加到一个字符串中。

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

      【讨论】:

        【解决方案11】:

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

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

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

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

        )

        【讨论】:

          【解决方案12】:

          在 php 中没有这样的限制, php 可以用 dot(.) 操作符连接 strng

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

          输出“hello world”

          【讨论】:

          • 这里的人很快就触发了..我在黑暗中打字..不小心点击了标签然后输入..
          猜你喜欢
          • 2010-10-03
          • 1970-01-01
          • 1970-01-01
          • 2022-09-30
          • 2012-02-05
          • 2010-12-21
          • 1970-01-01
          • 1970-01-01
          • 2015-10-14
          相关资源
          最近更新 更多