PHP'de FOREACH'e karşı performansı


134

Her şeyden önce, uygulamaların% 90'ında performans farkının tamamen alakasız olduğunu anlıyorum, ancak sadece hangisinin daha hızlı olduğunu bilmem gerekiyor. Bu ve ...

İnternette şu anda mevcut olan bilgiler kafa karıştırıcı. Pek çok insan foreach'in kötü olduğunu söylüyor, ancak teknik olarak daha hızlı olması gerekiyor çünkü yineleyiciler kullanarak bir dizi geçişi yazmayı basitleştirmesi gerekiyor. Yine daha hızlı olduğu varsayılan yineleyiciler, ancak PHP'de de görünüşe göre çok yavaşlar (veya bu bir PHP olayı değil mi?). Dizi işlevlerinden bahsediyorum: next () prev () reset () vb. İyi, eğer onlar işlevlerse ve işlevlere benzeyen PHP dili özelliklerinden biri değilse.

Bunu biraz daraltmak için : Dizileri 1'den fazla adımlarla dolaşmakla ilgilenmiyorum (negatif adımlar da yok, yani ters iterasyon). Ayrıca keyfi noktalardan ve noktalara geçişle de ilgilenmiyorum, sadece 0'dan uzunluğa. Düzenli olarak 1000'den fazla anahtar içeren dizilerin işlendiğini de görmüyorum, ancak bir dizinin bir uygulamanın mantığında birden çok kez geçtiğini görüyorum! Ayrıca işlemlere gelince, büyük ölçüde yalnızca dizge işleme ve yankılama.

İşte birkaç referans site:
http://www.phpbench.com/
http://www.php.lt/benchmark/phpbench.php

Her yerde duyduğum şey:

  • foreachyavaştır ve dolayısıyla for/ whiledaha hızlıdır
  • PHP foreach, üzerinde yinelediği diziyi kopyalar; daha hızlı hale getirmek için referanslar kullanmanız gerekir
  • bunun gibi kod: a'dan daha hızlı$key = array_keys($aHash); $size = sizeOf($key);
    for ($i=0; $i < $size; $i++)
    foreach

İşte benim sorunum. Bu test komut dosyasını yazdım: http://pastebin.com/1ZgK07US ve komut dosyasını kaç kez çalıştırsam da şuna benzer bir şey elde ediyorum:

foreach 1.1438131332397
foreach (using reference) 1.2919359207153
for 1.4262869358063
foreach (hash table) 1.5696921348572
for (hash table) 2.4778981208801

Kısacası:

  • foreachforeachreferansla olduğundan daha hızlı
  • foreach daha hızlı for
  • foreachforkarma tablodan daha hızlıdır

Birisi açıklayabilir mi?

  1. Yanlış bir şey mi yapıyorum?
  2. PHP foreach referansı gerçekten bir fark yaratıyor mu? Demek istediğim, referansla geçerseniz neden kopyalamasın?
  3. Foreach ifadesi için eşdeğer yineleyici kodu nedir; İnternette birkaç tane gördüm ama onları her test ettiğimde zamanlama yanlış; Ayrıca birkaç basit yineleyici yapısını da test ettim, ancak hiçbir zaman düzgün sonuçlar bile almıyor - PHP'deki dizi yineleyiciler korkunç mu?
  4. FOR / FOREACH (ve WHILE) dışında bir dizide yinelemenin daha hızlı yolları / yöntemleri / yapıları var mı?

PHP Sürüm 5.3.0


Düzenleme: Cevap Buradaki insanların yardımıyla tüm soruların cevaplarını bir araya getirebildim. Onları burada özetleyeceğim:

  1. "Yanlış bir şey mi yapıyorum?" Fikir birliği şöyle görünüyor: evet, yankıyı kıyaslamalarda kullanamıyorum. Şahsen, yankının rastgele çalıştırma zamanına sahip bir işlev olduğunu ya da başka herhangi bir işlevin nasıl farklı olduğunu hala anlamıyorum - bu ve bu komut dosyasının her şeyden daha iyi foreach için aynı sonuçları üretme yeteneği zordur. "yankı kullanıyorsunuz" olsa da açıklamak için (ne kullanmalıydım). Ancak, testin daha iyi bir şeyle yapılması gerektiğini kabul ediyorum; akla ideal bir uzlaşma gelmiyor.
  2. "PHP foreach referansı gerçekten bir fark yaratıyor mu? Yani, referansla geçerseniz neden kopyalamasın?" ircmaxell gösteriyor ki evet öyle, ileri testler çoğu durumda referansın daha hızlı olması gerektiğini kanıtlıyor gibi görünüyor - yukarıdaki kod parçacığıma bakıldığında, kesinlikle hepsi anlamına gelmiyor. Sorunun muhtemelen böyle bir düzeyde uğraşmak için fazla sezgisel olmadığını ve her durum için hangisinin daha iyi olduğunu gerçekten belirlemek için derlemeyi çözme gibi aşırı bir şey gerektireceğini kabul ediyorum.
  3. "Foreach ifadesi için eşdeğer yineleyici kodu nedir; İnternette birkaç tane gördüm, ancak onları her test ettiğimde zamanlama çok yanlış; ayrıca birkaç basit yineleyici yapısını da test ettim ama hiçbir zaman düzgün sonuçlar bile alamadı - PHP'deki dizi yineleyiciler korkunç mu? " ircmaxell aşağıdaki cevabı verdi; ancak kod yalnızca PHP sürümü> = 5 için geçerli olabilir
  4. "FOR / FOREACH (ve WHILE) dışında bir dizide yinelemenin daha hızlı yolları / yöntemleri / yapıları var mı?" Cevap için Gordon'a teşekkürler. PHP5'te yeni veri türlerinin kullanılması, ya performans artışı ya da bellek artışı sağlamalıdır (durumunuza bağlı olarak bunlardan biri istenebilir). Hız açısından pek çok yeni dizi türü array () 'den daha iyi görünmese de splpriorityqueue ve splobjectstorage önemli ölçüde daha hızlı görünmektedir. Gordon tarafından sağlanan bağlantı: http://matthewturland.com/2010/05/20/new-spl-features-in-php-5-3/

Yardım etmeye çalışan herkese teşekkür ederim.

Herhangi bir basit geçiş için foreach'e (referans olmayan versiyon) bağlı kalacağım.


7
Kıyaslamanın 2.71 Kuralı: Kıyaslama için yankılanmayın.
Mchl

1
referans ile foreach, referans için karşı işaretlenmelidir. orada hatalı bir sonucunuz var. Bir referansın herhangi bir kullanımı, bir do-while döngüsünde bile, bir referans olmadan kullanımdan açıkça daha yavaş olacaktır.
bcosca

2
Bu php 5.3 için olduğundan, yeni Spl Veri Türlerini Dizilere karşı test etmeyi de düşünebilirsiniz. Ya da şuraya bakın: matthewturland.com/2010/05/20/new-spl-features-in-php-5-3
Gordon

@ Mchl: Birkaç kez çalıştırdım ve aynı sonuçları aldım - eğer yankı kıyaslamayı bozarsa tamamen rastgele sonuçlar almam gerekmez mi? ayrıca bir şeyi yinelemek ve onu çıktı olarak vermek isterdim, böylece yankı benim için gerçekten önemli; yankılanırken foreach daha hızlıysa, bu foreach'i kullanmam gereken büyük bir kod yığınıdır. @ stillstanding: duyduğum şey temelde "foreach'de referans daha hızlı (her zaman) yapar, her zaman referansla yaz" satırları boyuncadır, bu yüzden böyle test ettim - diğer referans döngüleri ile karşılaştırıldığında gerçekten ilgilenmiyorum
srcspider

2
bu boş sorular doğal olarak yasaklanmalıdır. yanı sıra aldatıcı phpbench sitesi
Ortak

Yanıtlar:


110

Benim kişisel fikrim, bağlam içinde anlamlı olanı kullanmaktır. Şahsen ben neredeyse hiç fordizi geçişi için kullanmam . Onu diğer yineleme türleri için kullanıyorum, ama foreachçok kolay ... Çoğu durumda zaman farkı minimum olacak.

İzlenmesi gereken en büyük şey:

for ($i = 0; $i < count($array); $i++) {

Bu pahalı bir döngü, çünkü her bir yinelemede sayma çağırıyor. Bunu yapmadığın sürece, gerçekten önemli olduğunu sanmıyorum ...

Fark yaratan referansa gelince, PHP yazılırken kopyala kullanır, bu nedenle diziye yazmazsanız, döngü sırasında nispeten az ek yük olacaktır. Bununla birlikte, dizi içindeki diziyi değiştirmeye başlarsanız, aralarındaki farklılıkları görmeye başlayacağınız yer burasıdır (çünkü bir kişinin tüm diziyi kopyalaması gerekecek ve referans sadece satır içi değiştirebilir) ...

Yineleyicilere gelince foreach, eşdeğerdir:

$it->rewind();
while ($it->valid()) {
    $key = $it->key();     // If using the $key => $value syntax
    $value = $it->current();

    // Contents of loop in here

    $it->next();
}

Yinelemenin daha hızlı yolları olduğu sürece, bu gerçekten soruna bağlıdır. Ama gerçekten sormam gerekiyor, neden? İşleri daha verimli hale getirmek istemeyi anlıyorum ama sanırım mikro optimizasyon için zamanınızı boşa harcıyorsunuz. Unutma Premature Optimization Is The Root Of All Evil...

Düzenleme: Yoruma dayanarak, hızlı bir kıyaslama çalışması yapmaya karar verdim ...

$a = array();
for ($i = 0; $i < 10000; $i++) {
    $a[] = $i;
}

$start = microtime(true);
foreach ($a as $k => $v) {
    $a[$k] = $v + 1;
}
echo "Completed in ", microtime(true) - $start, " Seconds\n";

$start = microtime(true);
foreach ($a as $k => &$v) {
    $v = $v + 1;
}
echo "Completed in ", microtime(true) - $start, " Seconds\n";

$start = microtime(true);
foreach ($a as $k => $v) {}
echo "Completed in ", microtime(true) - $start, " Seconds\n";

$start = microtime(true);
foreach ($a as $k => &$v) {}    
echo "Completed in ", microtime(true) - $start, " Seconds\n";

Ve sonuçlar:

Completed in 0.0073502063751221 Seconds
Completed in 0.0019769668579102 Seconds
Completed in 0.0011849403381348 Seconds
Completed in 0.00111985206604 Seconds

Yani döngüdeki diziyi değiştiriyorsanız, referansları kullanmak birkaç kat daha hızlıdır ...

Ve sadece referans için ek yük, aslında diziyi kopyalamaktan daha azdır (bu, 5.3.2'de) ... Yani, (en azından 5.3.2'de) referanslar önemli ölçüde daha hızlıymış gibi görünür ...


1
"Planlanmamış] optimizasyon tüm kötülüklerin köküdür" demek istemiyor musunuz? ;) Şey, hepsi aynı şeyi yapıyor, bu yüzden o kadar çok bir optimizasyon değil: "benimsemenin daha iyi standart yolu". Ayrıca cevaplanmamış birkaç soru daha: Kopyalamak zorunda olmadığı için söylüyorsunuz, ancak referans kullanımı da bir yük değil mi? Soruma hala devam eden yorum sizin varsayımlarınızla uyuşmuyor gibi görünüyor. Ayrıca, kod burada referans için neden daha yavaş üretiliyor? 5.3.0'daki foreach, herhangi bir diziyi () nesneye (örn. SplFixedArray) dönüştürmek için değişti mi?
srcspider

@srcspider: Kıyaslama kodlu yanıt düzenlenmiş ve referansları gösteren sonuçlar gerçekten de referans olmayanlardan çok daha hızlıdır ...
ircmaxell

1
@srcspider "the better standard way to adopt." performansı, neyi benimseyeceğinizi seçmek için tek kriter değildir. özellikle böylesine abartılı bir durumda. Açıkçası, sadece zamanınızı boşa harcıyorsunuz
Sağduyu

@Col. Şarapnel% 100 katılıyorum. Okunabilirlik ve sürdürülebilirlik, bu özel durumda performansı büyük bir farkla gölgede bırakıyor ... Bir standart seçmek ve ona bağlı kalmak konusunda hemfikirim, ancak bu standardı diğer --daha önemli-- faktörlere
dayandırıyorum

@ircmaxell: betiğinizi hızlı bir şekilde çalıştırmak amacınızı kanıtlıyor gibi görünüyor, ancak biraz daha derinlemesine incelemek istiyorum; Orijinal sorumu yeni 5.3 özelliklerinden bazılarını içerecek şekilde daha fazla testle düzenleyebilirim. @Col. Şarapnel: FOR neredeyse evrensel programlama-anaokulu seviyesidir, FOREACH daha basit sözdizimidir. Okunabilirlik açısından eşit zeminde görünüyorlar. Bunların hepsi o kadar düşük seviyeli ki bazı yüksek seviyeli modellerde olduğu gibi bakımın bir sorun olduğunu düşünmüyorum. Ve bu "temel yapı" yazacağım birçok kodu açıklayacağı için zamanımı boşa harcadığımı da sanmıyorum. :)
srcspider

54

Bunun bu kadar şaşırtıcı olduğundan emin değilim. PHP'de kodlama yapan çoğu insan PHP'nin aslında çıplak metalde ne yaptığını bilmiyor. Çoğu zaman doğru olacak birkaç şey söyleyeceğim:

  1. Değişkeni değiştirmiyorsanız, by-value PHP'de daha hızlıdır. Bunun nedeni, referansın yine de sayılması ve değerine göre daha az iş yapmasıdır. ZVAL'ı (çoğu tür için PHP'nin dahili veri yapısı) değiştirdiğiniz an, onu basit bir şekilde kesmesi gerektiğini bilir (kopyalayın ve diğer ZVAL'ı unutun). Ama onu asla değiştirmezsin, bu yüzden önemli değil. Referanslar, daha fazla defter tutma ile bunu daha karmaşık hale getirir , değişkeni değiştirdiğinizde ne yapılacağını bilmek zorunda kalır. Yani eğer salt okunursanız, paradoksal olarak & ile bunu belirtmemek daha iyidir. Biliyorum, sezgisel değil ama aynı zamanda doğru.

  2. Foreach yavaş değil. Ve basit yineleme için, test ettiği koşul - "bu dizinin sonunda mıyım?" - PHP işlem kodları yerine yerel kod kullanılarak yapılır. APC önbelleğe alınmış işlem kodları olsa bile, çıplak metalde yapılan bir dizi yerel işlemden daha yavaştır.

  3. For döngüsü "for ($ i = 0; $ i <count ($ x); $ i ++) kullanmak, count () ve PHP'nin ayrıştırma sırasında değerlendirme yeteneğinin (veya gerçekten herhangi bir yorumlanmış dilin) ​​olmaması nedeniyle yavaştır herhangi bir şeyin diziyi değiştirip değiştirmediğini zaman. Bu, sayıyı bir kez değerlendirmesini engeller.

  4. Ama bunu "$ c = count ($ x); for ($ i = 0; $ i <$ c; $ i ++) ile düzelttikten sonra bile $ i <$ c, olduğu gibi en iyi ihtimalle bir grup Zend işlem kodudur $ i ++. 100000 yineleme sırasında bu önemli olabilir. Foreach yerel düzeyde ne yapacağını bilir. "Bu dizinin sonunda mıyım" koşulunu test etmek için hiçbir PHP işlem koduna gerek yoktur.

  5. Peki ya eski okul "while (list (" şeyler? Her bir (), current (), vb. Kullanımı yavaş olmayan ancak ücretsiz olmayan en az 1 işlev çağrısı içerecek. Evet, bunlar Yine PHP işlem kodları var! Yani + list + her birinin maliyeti vardır.

Bu nedenlerden dolayı foreach anlaşılır bir şekilde basit yineleme için en iyi seçenektir.

Ve unutma, aynı zamanda okuması en kolay olanı, yani kazan-kazan.


Bu tam da aradığım açıklama, teşekkürler.
hardsetting

Bu cevap, işaretli cevaba gerçekten bir destek veya özet olmalıdır. Okuduğuma sevindim, iyi iş.
doz87

30

Kıyaslamalarda (özellikle phpbench.com) dikkat edilmesi gereken bir şey, sayılar sağlam olsa da testler değildir. Phpbench.com'daki testlerin birçoğu önemsiz şeyler yapıyor ve PHP'nin dizi aramalarını karşılaştırmaları çarpıtmak için önbelleğe alma yeteneğini kötüye kullanıyor veya bir dizi üzerinde yineleme olması durumunda, onu gerçek dünyadaki durumlarda test etmiyor (kimse için boş yazmıyor) ) döngüler. Gerçek dünya sonuçlarını oldukça yansıtan bulduğum kendi kriterlerimi yaptım ve her zaman dilin yerel yinelenen sözdizimini foreachen üstte gösteriyor (sürpriz, sürpriz).

//make a nicely random array
$aHash1 = range( 0, 999999 );
$aHash2 = range( 0, 999999 );
shuffle( $aHash1 );
shuffle( $aHash2 );
$aHash = array_combine( $aHash1, $aHash2 );


$start1 = microtime(true);
foreach($aHash as $key=>$val) $aHash[$key]++;
$end1 = microtime(true);

$start2 = microtime(true);
while(list($key) = each($aHash)) $aHash[$key]++;
$end2 = microtime(true);


$start3 = microtime(true);
$key = array_keys($aHash);
$size = sizeOf($key);
for ($i=0; $i<$size; $i++) $aHash[$key[$i]]++;
$end3 = microtime(true);

$start4 = microtime(true);
foreach($aHash as &$val) $val++;
$end4 = microtime(true);

echo "foreach ".($end1 - $start1)."\n"; //foreach 0.947947025299
echo "while ".($end2 - $start2)."\n"; //while 0.847212076187
echo "for ".($end3 - $start3)."\n"; //for 0.439476966858
echo "foreach ref ".($end4 - $start4)."\n"; //foreach ref 0.0886030197144

//For these tests we MUST do an array lookup,
//since that is normally the *point* of iteration
//i'm also calling noop on it so that PHP doesn't
//optimize out the loopup.
function noop( $value ) {}

//Create an array of increasing indexes, w/ random values
$bHash = range( 0, 999999 );
shuffle( $bHash );

$bstart1 = microtime(true);
for($i = 0; $i < 1000000; ++$i) noop( $bHash[$i] );
$bend1 = microtime(true);

$bstart2 = microtime(true);
$i = 0; while($i < 1000000) { noop( $bHash[$i] ); ++$i; }
$bend2 = microtime(true);


$bstart3 = microtime(true);
foreach( $bHash as $value ) { noop( $value ); }
$bend3 = microtime(true);

echo "for ".($bend1 - $bstart1)."\n"; //for 0.397135972977
echo "while ".($bend2 - $bstart2)."\n"; //while 0.364789962769
echo "foreach ".($bend3 - $bstart3)."\n"; //foreach 0.346374034882

3

Yıl 2020 ve eşyalar php 7.4 ve opcache ile büyük ölçüde gelişti .

İşte echo ve html parçaları olmadan unix CLI olarak çalıştırılan OP ^ kıyaslaması .

Test, normal bir bilgisayarda yerel olarak çalıştırıldı.

php -v

PHP 7.4.6 (cli) (built: May 14 2020 10:02:44) ( NTS )

Değiştirilmiş karşılaştırma komut dosyası:

<?php 
 ## preperations; just a simple environment state

  $test_iterations = 100;
  $test_arr_size = 1000;

  // a shared function that makes use of the loop; this should
  // ensure no funny business is happening to fool the test
  function test($input)
  {
    //echo '<!-- '.trim($input).' -->';
  }

  // for each test we create a array this should avoid any of the
  // arrays internal representation or optimizations from getting
  // in the way.

  // normal array
  $test_arr1 = array();
  $test_arr2 = array();
  $test_arr3 = array();
  // hash tables
  $test_arr4 = array();
  $test_arr5 = array();

  for ($i = 0; $i < $test_arr_size; ++$i)
  {
    mt_srand();
    $hash = md5(mt_rand());
    $key = substr($hash, 0, 5).$i;

    $test_arr1[$i] = $test_arr2[$i] = $test_arr3[$i] = $test_arr4[$key] = $test_arr5[$key]
      = $hash;
  }

  ## foreach

  $start = microtime(true);
  for ($j = 0; $j < $test_iterations; ++$j)
  {
    foreach ($test_arr1 as $k => $v)
    {
      test($v);
    }
  }
  echo 'foreach '.(microtime(true) - $start)."\n";  

  ## foreach (using reference)

  $start = microtime(true);
  for ($j = 0; $j < $test_iterations; ++$j)
  {
    foreach ($test_arr2 as &$value)
    {
      test($value);
    }
  }
  echo 'foreach (using reference) '.(microtime(true) - $start)."\n";

  ## for

  $start = microtime(true);
  for ($j = 0; $j < $test_iterations; ++$j)
  {
    $size = count($test_arr3);
    for ($i = 0; $i < $size; ++$i)
    {
      test($test_arr3[$i]);
    }
  }
  echo 'for '.(microtime(true) - $start)."\n";  

  ## foreach (hash table)

  $start = microtime(true);
  for ($j = 0; $j < $test_iterations; ++$j)
  {
    foreach ($test_arr4 as $k => $v)
    {
      test($v);
    }
  }
  echo 'foreach (hash table) '.(microtime(true) - $start)."\n";

  ## for (hash table)

  $start = microtime(true);
  for ($j = 0; $j < $test_iterations; ++$j)
  {
    $keys = array_keys($test_arr5);
    $size = sizeOf($test_arr5);
    for ($i = 0; $i < $size; ++$i)
    {
      test($test_arr5[$keys[$i]]);
    }
  }
  echo 'for (hash table) '.(microtime(true) - $start)."\n";

Çıktı:

foreach 0.0032877922058105
foreach (using reference) 0.0029420852661133
for 0.0025191307067871
foreach (hash table) 0.0035080909729004
for (hash table) 0.0061779022216797

Gördüğünüz gibi evrim çılgınca, 2012'de bildirilenden yaklaşık 560 kat daha hızlı .

Makinelerimde ve sunucularımda, çok sayıda deneyimin ardından, döngülerin temelleri en hızlı olanıdır. Bu, iç içe döngüler kullanıldığında daha da nettir ( $ i $ j $ k ..)

Aynı zamanda kullanımda en esnektir ve benim görüşüme göre daha iyi okunabilirliğe sahiptir.


0

Sanırım ama emin değilim: fordöngü, değerleri kontrol etmek ve artırmak için iki işlem gerektirir. foreachveriyi belleğe yükler ve ardından her değeri yineler.


7
Herkesin bir fikri vardır, insanlar cevapları bulmak için Stack Overflow'a gelir. Ne belirttiğinizden emin değilseniz, kaynak kodunu, belgeleri kontrol edin, bir google araması yapın vb.
Sebastien F.

Performans araştırma ve teste dayandığından, bazı kanıtlar sunmalısınız. Lütfen referanslarınızı buna göre sağlayın. Umarım cevabınızı geliştirebilirsiniz.
Marwan Salim

Sanırım bu aynı zamanda sunucunun gerçek yüküne ve döngüde ne yapmak istediğinize de bağlı. Sanırım bu aynı zamanda sunucunun gerçek yüküne ve döngüde ne yapmak istediğinize de bağlı. Numaralandırılmış dizi üzerinden yineleme yapıp yapmadığımı öğrenmek istedim, foreach veya for-döngüsü kullanmalıyım, bu yüzden sandbox.onlinephpfunctions.com'da PHP 7.4 ile bir kıyaslama yaptım . Aynı betiği birden çok kez çalıştırıyorum ve her çalıştırma bana farklı sonuç veriyor. Bir seferinde for-loop daha hızlıydı, başka bir sefer foreach döngüsü ve başka bir sefer eşitti.
Alexander Behling
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.