php String Конкатенация, Производительность - PullRequest
69 голосов
/ 24 сентября 2008

В таких языках, как Java и C #, строки являются неизменяемыми, и может быть вычислительно дорого создавать строку по одному символу за раз. На указанных языках существуют библиотечные классы для снижения этой стоимости, такие как C # System.Text.StringBuilder и Java java.lang.StringBuilder.

Имеет ли php (4 или 5; меня интересуют оба) это ограничение? Если да, есть ли похожие решения проблемы?

Ответы [ 11 ]

60 голосов
/ 24 сентября 2008

Нет, в 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 , его различными разделителями и значениями каждого из них.

27 голосов
/ 03 мая 2013

Мне было любопытно, поэтому я провел тест. Я использовал следующий код:

<?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 является наименее эффективным способом сделать это, как с точки зрения времени и потребления памяти. РЕДАКТИРОВАТЬ: просматривать изображение на другой вкладке, если у вас нет зрения орла. enter image description here

12 голосов
/ 20 апреля 2013

Аналог StringBuilder не требуется в PHP.

Я сделал пару простых тестов:

в 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: ~ 17ms

12 голосов
/ 24 сентября 2008

Когда вы делаете сравнение по времени, различия настолько малы, что это не очень важно. С тех пор это сделало бы больше для того, чтобы сделать выбор, который делает ваш код более легким для чтения и понимания.

10 голосов
/ 17 августа 2010

Я знаю, о чем ты говоришь. Я только что создал этот простой класс для эмуляции класса Java StringBuilder.

class StringBuilder {

  private $str = array();

  public function __construct() { }

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

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

}
6 голосов
/ 24 сентября 2008

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';
2 голосов
/ 27 сентября 2016

Я написал код в конце этого поста, чтобы протестировать различные формы конкатенации строк, и они все практически одинаковы как по памяти, так и по времени.

Два основных метода, которые я использовал, - это сцепление строк друг с другом, заполнение массива строками и последующее их развертывание. В php 5.6 я добавил 500 строк с 1-мегабайтной строкой (в результате получается 500-мегабайтная строка). На каждой итерации теста все следы памяти и времени были очень и очень близки (~ $ IterationNumber * 1MB). Время выполнения обоих тестов составило 50,398 секунды и 50,843 секунды подряд, что, скорее всего, находится в допустимых пределах погрешности.

Сборка мусора строк, на которые больше не ссылаются, кажется довольно быстрой, даже не выходя из области видимости. Поскольку строки изменчивы, никакой дополнительной памяти на самом деле не требуется.

HOWEVER , Следующие тесты показали, что существует разное пиковое использование памяти WHILE строки объединяются.

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

Результат = 10 806 800 байт (~ 10 МБ без начального объема памяти PHP)

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

Результат = 6 613 320 байт (~ 6 МБ без начального объема памяти PHP)

Таким образом, на самом деле существует разница, которая может быть существенной в очень очень больших конкатенациях строк в памяти (я сталкивался с такими примерами при создании очень больших наборов данных или запросов SQL).

Но даже этот факт является спорным в зависимости от данных. Например, объединение 1 символа в строку для получения 50 миллионов байтов (то есть 50 миллионов итераций) заняло максимальный объем в 50 322 512 байтов (~ 48 МБ) за 5,97 секунды. В то время как выполнение метода массива закончилось использованием 7,337,107,176 байт (~ 6,8 ГБ) для создания массива за 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>';
}
?>
2 голосов
/ 24 сентября 2008

Да. Они делают. Например, если вы хотите отобразить пару строк вместе, используйте

echo str1,str2,str3 

вместо

echo str1.str2.str3 
чтобы получить немного быстрее.
1 голос
/ 24 сентября 2008

Во-первых, если вам не нужно объединять строки, не делайте этого: это всегда будет быстрее

echo $a,$b,$c;

чем

echo $a . $b . $c;

Однако, по крайней мере в PHP5, конкатенация строк действительно довольно быстрая, особенно если есть только одна ссылка на данную строку. Я полагаю, что интерпретатор использует StringBuilder -подобную технику внутри.

0 голосов
/ 24 сентября 2008

Если вы помещаете значения переменных в строки PHP, я понимаю, что использование встроенных переменных немного быстрее (это не официальное название - я не могу вспомнить, что это)

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

Должно быть внутри двойных кавычек, чтобы работать. Также работает для членов массива (т.е.

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

)

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...