Liste der Big-O für PHP-Funktionen

345

Nachdem ich PHP für eine Weile verwendet habe, habe ich festgestellt, dass nicht alle integrierten PHP-Funktionen so schnell sind wie erwartet. Betrachten Sie diese beiden möglichen Implementierungen einer Funktion, die mithilfe eines zwischengespeicherten Arrays von Primzahlen ermittelt, ob eine Zahl eine Primzahl ist.

//very slow for large $prime_array
$prime_array = array( 2, 3, 5, 7, 11, 13, .... 104729, ... );
$result_array = array();
foreach( $prime_array => $number ) {
    $result_array[$number] = in_array( $number, $large_prime_array );
}

//speed is much less dependent on size of $prime_array, and runs much faster.
$prime_array => array( 2 => NULL, 3 => NULL, 5 => NULL, 7 => NULL,
                       11 => NULL, 13 => NULL, .... 104729 => NULL, ... );
foreach( $prime_array => $number ) {
    $result_array[$number] = array_key_exists( $number, $large_prime_array );
}

Dies liegt daran in_array, dass eine lineare Suche O (n) implementiert wird, die sich mit zunehmendem Wachstum linear verlangsamt $prime_array. Wenn die array_key_existsFunktion mit einer Hash-Suche O (1) implementiert wird, die nicht verlangsamt wird, es sei denn, die Hash-Tabelle wird extrem gefüllt (in diesem Fall ist es nur O (n)).

Bisher musste ich die Big-O's durch Ausprobieren entdecken und gelegentlich den Quellcode betrachten . Nun zur Frage ...

Gibt es eine Liste der theoretischen (oder praktischen) großen O-Zeiten für alle * integrierten PHP-Funktionen?

* oder zumindest die interessanten

Zum Beispiel habe ich es sehr schwer , die große O Funktionen aufgelistet vorherzusagen , weil die mögliche Implementierung auf unbekannte Kerndatenstrukturen von PHP ab: array_merge, array_merge_recursive, array_reverse, array_intersect, array_combine, str_replace(mit Array - Eingänge), usw.

Kendall Hopkins
quelle
31
Völlig unangebracht, aber 1 ist nicht prim.
Jason Punyon
24
Arrays in PHP sind Hashtabellen. Das sollte dir alles sagen, was du wissen musst. Die Suche nach einem Schlüssel in einer Hashtabelle ist O (1). Die Suche nach einem Wert ist O (n) - was Sie bei einem unsortierten Satz nicht schlagen können. Die meisten Funktionen, auf die Sie neugierig sind, sind wahrscheinlich O (n). Wenn Sie es wirklich wissen möchten, können Sie natürlich die Quelle lesen: cvs.php.net/viewvc.cgi/php-src/ext/standard/…
Frank Farmer
11
Für den Datensatz besteht die schnellste Implementierung dessen, was Sie versuchen, darin, (anstatt NULL für Ihre Werte zu verwenden) zu verwenden trueund dann die Anwesenheit mit zu testen isset($large_prime_array[$number]). Wenn ich mich richtig erinnere, ist es in der Größenordnung hunderte Male schneller als die in_arrayFunktion.
Mattbasta
3
In der Big O-Notation geht es nicht um Geschwindigkeit. Es geht darum, das Verhalten einzuschränken.
Gumbo
3
@ Kendall Ich vergleiche nicht mit array_key_exists, ich vergleiche mit in_array. in_arrayiteriert jedes Element im Array und vergleicht den Wert mit der Nadel, die Sie an das Array übergeben. Wenn Sie die Werte auf den Schlüssel drehen (und einfach jeden der Werte durch einen Dummy-Wert wie ersetzen true, ist die Verwendung issetum ein Vielfaches schneller. Dies liegt daran, dass die Schlüssel eines Arrays von PHP indiziert werden (wie eine Hashtabelle). Folglich wird gesucht Ein Array auf diese Weise kann eine signifikante Verbesserung der Geschwindigkeit haben.
Mattbasta

Antworten:

649

Da es so aussieht, als hätte dies noch niemand getan, dachte ich, es wäre eine gute Idee, es irgendwo als Referenz zu haben. Ich bin jedoch gegangen und entweder über Benchmark oder Code-Skimming, um die array_*Funktionen zu charakterisieren . Ich habe versucht, das interessantere Big-O ganz oben zu platzieren. Diese Liste ist nicht vollständig.

Hinweis: Alle Big-O-Werte, die unter der Annahme einer Hash-Suche berechnet wurden, sind O (1), obwohl es sich tatsächlich um O (n) handelt. Der Koeffizient von n ist so niedrig, dass der RAM-Overhead beim Speichern eines ausreichend großen Arrays Sie verletzen würde, bevor die Eigenschaften von Lookup Big-O wirksam werden. Zum Beispiel array_key_existsbeträgt die Differenz zwischen einem Anruf bei N = 1 und N = 1.000.000 ~ 50% Zeitzunahme.

Interessante Punkte :

  1. isset/ array_key_existsIst viel schneller als in_arrayundarray_search
  2. +(union) ist etwas schneller als array_merge(und sieht besser aus). Aber es funktioniert anders, denken Sie daran.
  3. shuffle ist auf der gleichen Big-O-Ebene wie array_rand
  4. array_pop/ array_pushist schneller als array_shift/ array_unshiftaufgrund einer Neuindizierungsstrafe

Lookups :

array_key_existsO (n), aber sehr nahe an O (1) - dies liegt an der linearen Abfrage bei Kollisionen, aber da die Wahrscheinlichkeit von Kollisionen sehr gering ist, ist der Koeffizient auch sehr klein. Ich finde, Sie behandeln Hash-Lookups als O (1), um ein realistischeres Big-O zu erhalten. Zum Beispiel ist der Unterschied zwischen N = 1000 und N = 100000 nur etwa 50% langsamer.

isset( $array[$index] )O (n), aber sehr nahe an O (1) - es verwendet dieselbe Suche wie array_key_exists. Da es sich um ein Sprachkonstrukt handelt, wird die Suche zwischengespeichert, wenn der Schlüssel fest codiert ist, was in Fällen, in denen derselbe Schlüssel wiederholt verwendet wird, zu einer Beschleunigung führt.

in_array O (n) - Dies liegt daran, dass eine lineare Suche durch das Array durchgeführt wird, bis der Wert gefunden wird.

array_search O (n) - Es verwendet dieselbe Kernfunktion wie in_array, gibt jedoch einen Wert zurück.

Warteschlangenfunktionen :

array_push O (∑ var_i, für alle i)

array_pop O (1)

array_shift O (n) - Es müssen alle Schlüssel neu indiziert werden

array_unshift O (n + ∑ var_i, für alle i) - es müssen alle Schlüssel neu indiziert werden

Array-Schnittpunkt, Union, Subtraktion :

array_intersect_key Wenn der Schnittpunkt 100% O (Max (param_i_size) * ∑param_i_count für alle i), wenn der Schnittpunkt 0% O schneidet (∑param_i_size für alle i)

array_intersect wenn der Schnittpunkt 100% O (n ^ 2 * ∑param_i_count, für alle i), wenn der Schnittpunkt 0% O schneidet (n ^ 2)

array_intersect_assoc Wenn der Schnittpunkt 100% O (Max (param_i_size) * ∑param_i_count für alle i), wenn der Schnittpunkt 0% O schneidet (∑param_i_size für alle i)

array_diff O (π param_i_size, für alle i) - Das ist das Produkt aller param_sizes

array_diff_key O (∑ param_i_size, für i! = 1) - Dies liegt daran, dass wir nicht über das erste Array iterieren müssen.

array_merge O (∑ array_i, i! = 1) - muss nicht über das erste Array iterieren

+ (union) O (n), wobei n die Größe des 2. Arrays ist (dh array_first + array_second) - weniger Overhead als array_merge, da es nicht neu nummeriert werden muss

array_replace O (∑ array_i, für alle i)

Zufall :

shuffle Auf)

array_rand O (n) - Erfordert eine lineare Abfrage.

Offensichtlicher Big-O :

array_fill Auf)

array_fill_keys Auf)

range Auf)

array_splice O (Versatz + Länge)

array_slice O (Versatz + Länge) oder O (n), wenn Länge = NULL

array_keys Auf)

array_values Auf)

array_reverse Auf)

array_pad O (pad_size)

array_flip Auf)

array_sum Auf)

array_product Auf)

array_reduce Auf)

array_filter Auf)

array_map Auf)

array_chunk Auf)

array_combine Auf)

Ich möchte Eureqa dafür danken, dass es einfach ist, das Big-O der Funktionen zu finden. Es ist ein erstaunliches kostenloses Programm, das die beste Anpassungsfunktion für beliebige Daten finden kann.

BEARBEITEN:

Für diejenigen, die daran zweifeln, dass es sich um PHP-Array-Lookups handelt O(N), habe ich einen Benchmark geschrieben, um dies zu testen (sie sind immer noch effektiv O(1)für die realistischsten Werte).

PHP Array Lookup Graph

$tests = 1000000;
$max = 5000001;


for( $i = 1; $i <= $max; $i += 10000 ) {
    //create lookup array
    $array = array_fill( 0, $i, NULL );

    //build test indexes
    $test_indexes = array();
    for( $j = 0; $j < $tests; $j++ ) {
        $test_indexes[] = rand( 0, $i-1 );
    }

    //benchmark array lookups
    $start = microtime( TRUE );
    foreach( $test_indexes as $test_index ) {
        $value = $array[ $test_index ];
        unset( $value );
    }
    $stop = microtime( TRUE );
    unset( $array, $test_indexes, $test_index );

    printf( "%d,%1.15f\n", $i, $stop - $start ); //time per 1mil lookups
    unset( $stop, $start );
}
Kendall Hopkins
quelle
5
@ Kendall: Danke! Ich habe ein bisschen gelesen und es stellte sich heraus, dass PHP 'verschachtelte' Hashtabellen für Kollisionen verwendet. Das heißt, anstelle einer Logn-Struktur für Kollisionen wird einfach eine andere Hashtabelle verwendet. Und ich verstehe, dass PHP-Hashtabellen praktisch O (1) oder zumindest O (1) im Durchschnitt bieten - dafür sind Hashtabellen gedacht. Ich war nur neugierig, warum Sie sagten, sie seien "wirklich O (n)" und nicht "wirklich O (logn)". Toller Beitrag übrigens!
Cam
10
Zeitliche Komplexität sollte in die Dokumentation aufgenommen werden! Durch die Auswahl der richtigen Funktion können Sie so viel Zeit sparen oder vermeiden, das zu tun, was Sie geplant haben: p Vielen Dank für diese Liste bereits!
Samuel
41
Ich weiß, das ist alt ... aber was? Diese Kurve zeigt überhaupt nicht O (n), sondern O (log n), en.wikipedia.org/wiki/Logarithm . Das stimmt auch mit dem überein, was Sie für verschachtelte Hash-Maps erwarten würden.
Andreas
5
Was ist das Big-O von Unset für ein Element eines Arrays?
Chandrew
12
Während Hashtabellen tatsächlich die Komplexität der O (n) -Nachfrage im ungünstigsten Fall aufweisen, ist der durchschnittliche Fall O (1), und der spezielle Fall, den Ihr Benchmark testet, ist sogar O (1) garantiert , da es sich um eine nullbasierte, kontinuierliche, numerisch indizierte handelt Array, das niemals Hash-Kollisionen haben wird. Der Grund, warum immer noch eine Abhängigkeit von der Arraygröße auftritt, hat nichts mit der algorithmischen Komplexität zu tun. Sie wird durch CPU-Cache-Effekte verursacht. Je größer das Array ist, desto wahrscheinlicher ist es, dass Suchvorgänge mit wahlfreiem Zugriff zu Cache-Fehlern führen (und Cache-Fehler höher in der Hierarchie).
NikiC
5

Die Erklärung für den Fall, den Sie speziell beschreiben, ist, dass assoziative Arrays als Hash-Tabellen implementiert sind. Die Suche nach Schlüssel (und entsprechend array_key_exists) ist also O (1). Arrays werden jedoch nicht nach Wert indiziert. Daher ist die einzige Möglichkeit, im allgemeinen Fall festzustellen, ob ein Wert im Array vorhanden ist, eine lineare Suche. Da gibt es keine Überraschung.

Ich glaube nicht, dass es eine spezifische umfassende Dokumentation der algorithmischen Komplexität von PHP-Methoden gibt. Wenn es jedoch groß genug ist, um den Aufwand zu rechtfertigen, können Sie jederzeit den Quellcode durchsehen .

Dathan
quelle
Das ist keine wirkliche Antwort. Wie ich in der Frage angegeben habe, habe ich bereits versucht, den PHP-Quellcode zu untersuchen. Da PHP implementiert ist, wird es in C unter Verwendung komplexer Makros geschrieben, was es manchmal schwierig machen kann, das zugrunde liegende große O für Funktionen zu "sehen".
Kendall Hopkins
@ Kendall Ich habe Ihren Hinweis auf das Eintauchen in den Quellcode übersehen. In meiner Antwort gibt es jedoch eine Antwort: "Ich glaube nicht, dass es eine spezifische umfassende Dokumentation der algorithmischen Komplexität von PHP-Methoden gibt." "Nein" ist eine absolut gültige Antwort. (c:
Dathan
4

Sie möchten fast immer issetanstelle von verwenden array_key_exists. Ich schaue nicht auf die Interna, aber ich bin mir ziemlich sicher, dass dies array_key_existsO (N) ist, da es über jeden einzelnen Schlüssel des Arrays iteriert, während issetversucht wird, mit demselben Hash-Algorithmus auf das Element zuzugreifen, der beim Zugriff verwendet wird ein Array-Index. Das sollte O (1) sein.

Ein "Gotcha", auf das Sie achten sollten, ist Folgendes:

$search_array = array('first' => null, 'second' => 4);

// returns false
isset($search_array['first']);

// returns true
array_key_exists('first', $search_array);

Ich war neugierig und habe den Unterschied gemessen:

<?php

$bigArray = range(1,100000);

$iterations = 1000000;
$start = microtime(true);
while ($iterations--)
{
    isset($bigArray[50000]);
}

echo 'is_set:', microtime(true) - $start, ' seconds', '<br>';

$iterations = 1000000;
$start = microtime(true);
while ($iterations--)
{
    array_key_exists(50000, $bigArray);
}

echo 'array_key_exists:', microtime(true) - $start, ' seconds';
?>

is_set:0,132308959961 Sekunden
array_key_exists:2,33202195168 Sekunden

Dies zeigt natürlich keine zeitliche Komplexität, aber es zeigt, wie die beiden Funktionen miteinander verglichen werden.

Vergleichen Sie zum Testen der Zeitkomplexität die Zeit, die zum Ausführen einer dieser Funktionen auf der ersten und der letzten Taste benötigt wird.

Ryeguy
quelle
9
Das ist falsch. Ich bin zu 100% sicher, dass array_key_exists nicht über jeden Schlüssel iterieren muss. Wenn Sie nicht glauben, schauen Sie sich den Link unten an. Der Grund, warum isset so viel schneller ist, ist, dass es sich um ein Sprachkonstrukt handelt. Dies bedeutet, dass es nicht den Aufwand hat, einen Funktionsaufruf auszuführen. Ich denke auch, dass es aus diesem Grund die Suche zwischenspeichern könnte. Dies ist auch keine Antwort auf DIE FRAGE! Ich möchte eine Liste von Big (O) für PHP-Funktionen (wie in der Frage angegeben). Kein einziger Maßstab meiner Beispiele. svn.php.net/repository/php/php-src/branches/PHP_5_3/ext/…
Kendall Hopkins
Wenn Sie mir immer noch nicht glauben, habe ich einen kleinen Benchmark erstellt, um den Punkt zu demonstrieren. pastebin.com/BdKpNvkE
Kendall Hopkins
Was an Ihrem Benchmark falsch ist, ist, dass Sie xdebug deaktivieren müssen. =)
Guilherme Blanco
3
Es gibt zwei kritische Gründe, warum Sie isset über array_key_exists verwenden möchten. Erstens ist isset ein Sprachkonstrukt, das die Kosten eines Funktionsaufrufs senkt. Dies ist vergleichbar mit dem Argument $arrray[] = $appendvs. array_push($array, $append)Zweitens unterscheidet array_key_exists auch zwischen nicht gesetzten und Nullwerten. Denn $a = array('fred' => null); array_key_exists('fred', $a)wird true zurückgeben, während isset($['fred'])false zurückgegeben wird. Dieser zusätzliche Schritt ist nicht trivial und verlängert die Ausführungszeit erheblich.
Orca
0

Wenn Menschen in der Praxis Probleme mit Schlüsselkollisionen haben, implementieren sie Container mit einer sekundären Hash-Suche oder einem ausgeglichenen Baum. Der ausgeglichene Baum würde O (log n) Worst-Case-Verhalten und O (1) Durchschnitt ergeben. Fall (der Hash selbst). Der Overhead lohnt sich in den meisten praktischen Anwendungen in Speicheranwendungen nicht, aber vielleicht gibt es Datenbanken, die diese Form der gemischten Strategie als Standardfall implementieren.

Josh Stern
quelle