web-dev-qa-db-fra.com

php Concaténation de chaînes, performances

Dans des langages comme Java et C #, les chaînes sont immuables et il peut être coûteux en calcul de construire une chaîne un caractère à la fois. Dans ces langages, il existe des classes de bibliothèque pour réduire ce coût comme C # System.Text.StringBuilder et Java Java.lang.StringBuilder.

Php (4 ou 5; je suis intéressé par les deux) partage-t-il cette limitation? Si oui, existe-t-il des solutions similaires au problème?

69
Chris

Non, il n'y a pas de type de classe stringbuilder en PHP, car les chaînes sont mutables.

Cela étant dit, il existe différentes façons de créer une chaîne, selon ce que vous faites.

echo, par exemple, acceptera les jetons séparés par des virgules pour la sortie.

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

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

Cela signifie que vous pouvez sortir une chaîne complexe sans réellement utiliser la concaténation, ce qui serait plus lent

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

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

Si vous devez capturer cette sortie dans une variable, vous pouvez le faire avec les fonctions de tampon de sortie .

De plus, les performances des tableaux de PHP sont vraiment bonnes. Si vous voulez faire quelque chose comme une liste de valeurs séparées par des virgules, utilisez simplement implode ()

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

Enfin, assurez-vous de vous familiariser avec type de chaîne PHP et ses différents délimiteurs, et les implications de chacun.

60
Peter Bailey

J'étais curieux à ce sujet, j'ai donc fait un test. J'ai utilisé le code suivant:

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

... Et a obtenu les résultats suivants. Image jointe. De toute évidence, sprintf est le moyen le moins efficace de le faire, à la fois en termes de temps et de consommation de mémoire. EDIT: affichez l'image dans un autre onglet à moins que vous n'ayez une vision d'aigle. enter image description here

27
Evilnode

Lorsque vous effectuez une comparaison chronométrée, les différences sont si faibles qu'elles ne sont pas très pertinentes. Cela ferait plus puisque d'opter pour le choix qui rend votre code plus facile à lire et à comprendre.

12
SeanDowney

L'analogue StringBuilder n'est pas nécessaire en PHP.

J'ai fait quelques tests simples:

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

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

Résultats:

10000 itérations:
1) PHP, concaténation ordinaire: ~ 6 ms
2) PHP, en utilisant StringBuilder: ~ 5 ms
3) C #, concaténation ordinaire: ~ 520 ms
4) C #, en utilisant StringBuilder: ~ 1 ms

100000 itérations:
1) PHP, concaténation ordinaire: ~ 63 ms
2) PHP, en utilisant StringBuilder: ~ 555 ms
3) C #, concaténation ordinaire: ~ 91000 ms // !!!
4) C #, en utilisant StringBuilder: ~ 17 ms

12
nightcoder

Je sais de quoi tu parles. Je viens de créer cette classe simple pour émuler la classe Java StringBuilder.

class StringBuilder {

  private $str = array();

  public function __construct() { }

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

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

}
10
ossys

Les chaînes PHP sont mutables. Vous pouvez modifier des caractères spécifiques comme celui-ci:

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

Et vous pouvez ajouter des caractères à une chaîne comme celle-ci:

$string .= 'a';
6
Jeremy Ruten

J'ai écrit le code à la fin de cet article pour tester les différentes formes de concaténation de chaînes et elles sont toutes presque exactement égales en mémoire et en empreintes de temps.

Les deux principales méthodes que j'ai utilisées sont de concaténer des chaînes les unes sur les autres, de remplir un tableau de chaînes et de les imploser. J'ai fait 500 ajouts de chaînes avec une chaîne de 1 Mo en php 5.6 (donc le résultat est une chaîne de 500 Mo). À chaque itération du test, toutes les empreintes de mémoire et de temps étaient très très proches (à ~ $ IterationNumber * 1MB). La durée d'exécution des deux tests était de 50,398 secondes et 50,843 secondes consécutives, ce qui est très probablement dans des marges d'erreur acceptables.

La récupération de place des chaînes qui ne sont plus référencées semble être assez immédiate, même sans jamais quitter la portée. Étant donné que les chaînes sont mutables, aucune mémoire supplémentaire n'est vraiment requise après coup.

CEPENDANT , Les tests suivants ont montré qu'il y a une différence dans l'utilisation maximale de la mémoire [~ # ~] tandis que [~ # ~] les chaînes sont concaténées.

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

Résultat = 10 806 800 octets (~ 10 Mo sans la mémoire initiale PHP empreinte mémoire)

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

Résultat = 6613320 octets (~ 6 Mo sans la mémoire initiale PHP empreinte mémoire)

Il y a donc en fait une différence qui pourrait être significative dans les très grandes concaténations de chaînes en mémoire (j'ai rencontré de tels exemples lors de la création de très grands ensembles de données ou de requêtes SQL).

Mais même ce fait est contestable en fonction des données. Par exemple, concaténer 1 caractère sur une chaîne pour obtenir 50 millions d'octets (donc 50 millions d'itérations) a pris une quantité maximale de 50 322 512 octets (~ 48 Mo) en 5,97 secondes. Pendant ce temps, la méthode du tableau a fini par utiliser 7 337 107 176 octets (~ 6,8 Go) pour créer le tableau en 12,1 secondes, puis a pris 4,32 secondes supplémentaires pour combiner les chaînes du tableau.

Quoi qu'il en soit ... ce qui suit est le code de référence que j'ai mentionné au début qui montre que les méthodes sont à peu près égales. Il produit un joli tableau 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
Dakusan

Oui. Ils font. Par exemple, si vous souhaitez faire écho à deux chaînes ensemble, utilisez

 echo str1, str2, str3 

au lieu de

 echo str1.str2.str3 
2
mixdev

Premièrement, si vous n'avez pas besoin que les chaînes soient concaténées, ne le faites pas: ce sera toujours plus rapide à faire

echo $a,$b,$c;

que

echo $a . $b . $c;

Cependant, au moins en PHP5, la concaténation de chaînes est vraiment assez rapide, surtout s'il n'y a qu'une seule référence à une chaîne donnée. Je suppose que l'interpréteur utilise une technique semblable à StringBuilder en interne.

1
Anthony Williams

Si vous placez des valeurs de variable dans les chaînes PHP, je comprends qu'il est un peu plus rapide d'utiliser l'inclusion de variables en ligne (ce n'est pas son nom officiel - je ne me souviens pas de quoi il s'agit)

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

Doit être entre guillemets doubles pour fonctionner. Fonctionne également pour les membres du tableau (c.-à-d.

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

)

0
cori