PHP String Verkettung, Leistung

71

In Sprachen wie Java und C # sind Zeichenfolgen unveränderlich und es kann rechenintensiv sein, eine Zeichenfolge zeichenweise zu erstellen. In diesen Sprachen gibt es Bibliotheksklassen wie C # System.Text.StringBuilderund Java , um diese Kosten zu senken java.lang.StringBuilder.

Teilt PHP (4 oder 5; ich interessiere mich für beide) diese Einschränkung? Wenn ja, gibt es ähnliche Lösungen für das Problem?

Chris
quelle

Antworten:

62

Nein, es gibt keinen Typ von Stringbuilder-Klasse in PHP, da Strings veränderlich sind.

Davon abgesehen gibt es verschiedene Möglichkeiten, einen String zu erstellen, je nachdem, was Sie tun.

Echo akzeptiert beispielsweise durch Kommas getrennte Token für die Ausgabe.

// This...
echo 'one', 'two';

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

Dies bedeutet, dass Sie eine komplexe Zeichenfolge ausgeben können, ohne tatsächlich eine Verkettung zu verwenden, die langsamer wäre

// This...
echo 'one', 'two';

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

Wenn Sie diese Ausgabe in einer Variablen erfassen müssen, können Sie dies mit den Ausgabepufferfunktionen tun .

Auch die Array-Leistung von PHP ist wirklich gut. Wenn Sie so etwas wie eine durch Kommas getrennte Liste von Werten erstellen möchten, verwenden Sie einfach implode ()

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

Stellen Sie schließlich sicher, dass Sie sich mit dem Zeichenfolgentyp von PHP und den verschiedenen Trennzeichen sowie deren Auswirkungen vertraut gemacht haben.

Peter Bailey
quelle
26
Und verwenden Sie nach Möglichkeit einfache Anführungszeichen.
Stephen
1
warum nicht doppelte Anführungszeichen?
Tebe
4
@gekannt Weil PHP Variablen sowie zusätzliche Escape-Sequenzen in Zeichenfolgen, die in doppelte Anführungszeichen eingeschlossen sind, erweitert / interpretiert. Zum Beispiel $x = 5; echo "x = $x";würde drucken, x = 5während $x = 5; echo 'x = $x';drucken würde x = $x.
Samitny
man kann es brauchen, um erweitert zu werden sowie nicht erweitert / interpretiert zu werden, es hängt von der Situation ab
Tebe
17
Ein bisschen wie ein Mythos, das einfache Zitat: nikic.github.io/2012/01/09/…
Alimack
31

Ich war neugierig und habe einen Test durchgeführt. Ich habe den folgenden Code verwendet:

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

... und bekam die folgenden Ergebnisse. Bild angehängt. Sprintf ist eindeutig der am wenigsten effiziente Weg, sowohl hinsichtlich des Zeit- als auch des Speicherverbrauchs. BEARBEITEN: Zeigen Sie das Bild auf einer anderen Registerkarte an, es sei denn, Sie haben Adlersicht. Geben Sie hier die Bildbeschreibung ein

Evilnode
quelle
1
sollte 1 weiteren Test haben: ähnlich, test2aber ersetzen .durch ,(natürlich ohne Ausgabepuffer)
Raptor
1
Sehr nützlich, danke. Die Verkettung von Zeichenfolgen scheint der richtige Weg zu sein. Es macht Sinn, dass sie versuchen würden, das zu optimieren.
Chris Middleton
14

StringBuilder analog wird in PHP nicht benötigt.

Ich habe ein paar einfache Tests gemacht:

in 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();

in 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();

Ergebnisse:

10000 Iterationen:
1) , PHP, gewöhnliche Verkettung: ~ 6 ms
2) PHP, unter Verwendung von String: ~ 5 ms
3) C #, gewöhnliche Verkettung: ~ 520ms
4) C #, unter Verwendung von String: ~ 1ms

100000 Iterationen:
1) PHP, normale Verkettung: ~ 63 ms
2) PHP mit StringBuilder: ~ 555 ms
3) C #, normale Verkettung: ~ 91000 ms // !!!
4) C # mit StringBuilder: ~ 17ms

Nachtcodierer
quelle
Java ist in dieser Hinsicht mehr oder weniger dasselbe wie C #. Obwohl die späteren Versionen zur Kompilierungszeit einige Optimierungen vorgenommen haben, um dies zu verringern. Früher war es so (in 1.4 und früher, vielleicht sogar in 1.6), dass Sie besser mit einem StringBuffer / Builder umgehen sollten, wenn Sie 3 oder mehr Elemente verketten müssen. Obwohl in einer Schleife, müssen Sie immer noch den StringBuilder verwenden.
A. Grandt
Mit anderen Worten, PHP wurde für Personen entwickelt, die sich nicht um Überlegungen auf niedriger Ebene kümmern müssen, und es führt eine interne Zeichenfolgenpufferung für den Zeichenfolgentyp durch. Dies hat nichts damit zu tun, dass Strings auf PHP "veränderlich" sind. Um die Länge eines Strings zu vergrößern, ist weiterhin eine Speicherkopie auf einen größeren Speicher erforderlich, es sei denn, Sie verwalten einen Puffer, in den er hineinwachsen kann.
Thomasrutter
Übrigens sollte dies die akzeptierte Antwort sein. Die aktuellen Top-Antworten beantworten die Frage nicht einmal.
Thomasrutter
12

Wenn Sie einen zeitgesteuerten Vergleich durchführen, sind die Unterschiede so gering, dass sie nicht sehr relevant sind. Es würde mehr bedeuten, sich für die Wahl zu entscheiden, die das Lesen und Verstehen Ihres Codes erleichtert.

SeanDowney
quelle
2
In der Tat ist es geradezu albern, sich darüber Sorgen zu machen, wenn es normalerweise weitaus wichtigere Probleme gibt, über die man sich Sorgen machen muss, wie z. B. Datenbankdesign, Big O () -Analyse und ordnungsgemäße Profilerstellung.
DGM
2
Das ist sehr richtig, aber ich habe Situationen in Java und C # gesehen, in denen die Verwendung einer veränderlichen Zeichenfolgenklasse (vs. s + = "blah") die Leistung tatsächlich dramatisch gesteigert hat.
Pete Alvin
10

Ich weiß wovon du sprichst. Ich habe gerade diese einfache Klasse erstellt, um die Java StringBuilder-Klasse zu emulieren.

class StringBuilder {

  private $str = array();

  public function __construct() { }

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

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

}
Ossys
quelle
9
Schöne Lösung. Am Ende der appendFunktion können Sie hinzufügen return $this;, um die Methodenverkettung zu ermöglichen : $sb->append("one")->append("two");.
Jabba
7
Dies ist in PHP völlig unnötig. Tatsächlich bin ich bereit zu wetten, dass dies erheblich langsamer ist als die reguläre Verkettung.
Ryeguy
9
ryeguy: stimmt, da Strings in PHP veränderbar sind, ist diese Methode "unnötig". Die Person hat nach einer ähnlichen Implementierung wie Javas StringBuilder gefragt. Also los ... ich würde nicht sagen, dass sie "wesentlich" langsamer ist, denke ich Ich bin ein bisschen dramatisch. Der Aufwand für die Instanziierung einer Klasse, die die Zeichenfolgenerstellung verwaltet, kann Kosten beinhalten, aber die Nützlichkeit der StringBuilder-Klasse kann um zusätzliche Methoden für die Zeichenfolge erweitert werden. Ich werde untersuchen, welcher zusätzliche Aufwand durch die Implementierung von so etwas in einer Klasse realisiert wird, und versuchen, einen Beitrag zu veröffentlichen.
Ossys
7
... und er wurde nie wieder gehört.
Nigralbus
6

PHP-Strings sind veränderbar. Sie können bestimmte Zeichen wie folgt ändern:

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

Und Sie können Zeichen wie folgt an eine Zeichenfolge anhängen:

$string .= 'a';
Jeremy Ruten
quelle
Ich bin kein Experte für PHP. Ist "$ string. = 'A'" keine Kurzform von "$ string = $ string. 'A'" und erstellt PHP keinen neuen String (und ändert den alten nicht)?
Wolfgang Adamec
Ja, es ist eine Kurzform. Bei Ihrer zweiten Frage ist das interne Verhalten von PHP so, dass es effektiv so ist, als würde man den String durch einen um ein Byte längeren ersetzen. Intern wird jedoch wie StringBuilder gepuffert.
Thomasrutter
3

Ich habe den Code am Ende dieses Beitrags geschrieben, um die verschiedenen Formen der Zeichenfolgenverkettung zu testen, und sie sind wirklich alle in Bezug auf Speicher- und Zeitabdruck fast genau gleich.

Die beiden wichtigsten Methoden, die ich verwendet habe, bestehen darin, Zeichenfolgen miteinander zu verknüpfen, ein Array mit Zeichenfolgen zu füllen und diese dann zu implodieren. Ich habe 500 Zeichenfolgen mit einer 1-MB-Zeichenfolge in PHP 5.6 hinzugefügt (das Ergebnis ist also eine 500-MB-Zeichenfolge). Bei jeder Iteration des Tests waren alle Speicher- und Zeitabdrücke sehr, sehr eng (bei ~ $ IterationNumber * 1MB). Die Laufzeit beider Tests betrug nacheinander 50,398 Sekunden und 50,843 Sekunden, was höchstwahrscheinlich innerhalb akzeptabler Fehlergrenzen liegt.

Die Speicherbereinigung von Zeichenfolgen, auf die nicht mehr verwiesen wird, scheint ziemlich unmittelbar zu sein, auch ohne jemals den Gültigkeitsbereich zu verlassen. Da die Zeichenfolgen veränderbar sind, wird nachträglich kein zusätzlicher Speicher benötigt.

Die folgenden Tests haben jedoch gezeigt, dass sich die maximale Speichernutzung unterscheidet, während die Zeichenfolgen verkettet werden.

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

Ergebnis = 10.806.800 Bytes (~ 10 MB ohne den anfänglichen PHP-Speicherbedarf)

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

Ergebnis = 6.613.320 Bytes (~ 6 MB ohne den anfänglichen PHP-Speicherbedarf)

Es gibt also tatsächlich einen Unterschied, der bei sehr sehr großen Zeichenfolgenverkettungen in Bezug auf den Speicher von Bedeutung sein kann (ich habe solche Beispiele beim Erstellen sehr großer Datenmengen oder SQL-Abfragen gefunden).

Aber auch diese Tatsache ist je nach Daten umstritten. Das Verketten eines Zeichens mit einer Zeichenfolge, um 50 Millionen Bytes (also 50 Millionen Iterationen) zu erhalten, dauerte beispielsweise in 5,97 Sekunden maximal 50.322.512 Bytes (~ 48 MB). Dabei wurden für die Array-Methode 7.337.107.176 Byte (~ 6,8 GB) verwendet, um das Array in 12,1 Sekunden zu erstellen. Anschließend wurden zusätzliche 4,32 Sekunden benötigt, um die Zeichenfolgen aus dem Array zu kombinieren.

Egal ... das Folgende ist der Benchmark-Code, den ich am Anfang erwähnt habe und der zeigt, dass die Methoden ziemlich gleich sind. Es gibt eine hübsche HTML-Tabelle aus.

<?
//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>';
}
?>
Dakusan
quelle
Danke dafür. array_push war 100x schneller als das Verketten von Zeichenfolgen in meinem Code.
Kyouma
2

Ja. Tun sie. Wenn Sie beispielsweise mehrere Zeichenfolgen zusammen wiedergeben möchten, verwenden Sie

Echo str1, str2, str3 

Anstatt von

echo str1.str2.str3 
um es etwas schneller zu bekommen.

mixdev
quelle
funktioniert diese Funktion so? $ newstring = str1.srt2.str3; echo $ newstring;
JoshFinnie
1

Erstens, wenn Sie nicht möchten, dass die Zeichenfolgen verkettet werden, tun Sie es nicht: Es wird immer schneller gehen

echo $a,$b,$c;

als

echo $a . $b . $c;

Zumindest in PHP5 ist die Verkettung von Zeichenfolgen jedoch sehr schnell, insbesondere wenn nur ein Verweis auf eine bestimmte Zeichenfolge vorhanden ist. Ich denke, der Dolmetscher verwendet StringBuilderintern eine ähnliche Technik.

Anthony Williams
quelle
0

Wenn Sie Variablenwerte in PHP-Zeichenfolgen einfügen, ist die Inline-Variableneinbeziehung meines Erachtens etwas schneller (das ist nicht der offizielle Name - ich kann mich nicht erinnern, was ist).

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

Muss in doppelten Anführungszeichen stehen, um zu funktionieren. Funktioniert auch für Array-Mitglieder (dh

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

)

cori
quelle
0

Ich bin gerade auf dieses Problem gestoßen:

$ str. = 'String-Verkettung. ';

vs.

$ str = $ str. 'String-Verkettung. ';

Hier scheint bisher niemand dies verglichen zu haben. Und die Ergebnisse sind mit 50.000 Iterationen und PHP 7.4 ziemlich verrückt:

Zeichenfolge 1: 0,0013918876647949

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

Kann jemand das bestätigen? Ich bin darauf gestoßen, weil ich einige Zeilen aus einer großen Datei gelöscht habe, indem ich die gewünschten Zeilen gelesen und nur wieder an eine Zeichenfolge angehängt habe.

Die Verwendung von. = Hat hier alle meine Probleme gelöst. Bevor ich eine Auszeit bekam!

mdempfle
quelle
-4

Keine solche Einschränkung in PHP, PHP kann Strng mit dem Punkt (.) Operator verketten

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

gibt "Hallo Welt" aus

paan
quelle
4
Leute hier sind schnell am Abzug .. ich habe im Dunkeln getippt .. versehentlich Tab gedrückt und dann eingeben ..
paan