Liste des fonctions Big-O pour PHP

345

Après avoir utilisé PHP pendant un certain temps maintenant, j'ai remarqué que toutes les fonctions PHP intégrées ne sont pas aussi rapides que prévu. Considérez ces deux implémentations possibles d'une fonction qui trouve si un nombre est premier en utilisant un tableau de nombres premiers mis en cache.

//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 );
}

En effet, il in_arrayest implémenté avec une recherche linéaire O (n) qui ralentira linéairement à mesure qu'il $prime_arraygrandit. Où la array_key_existsfonction est implémentée avec une recherche de hachage O (1) qui ne ralentira pas à moins que la table de hachage ne soit extrêmement remplie (auquel cas ce n'est que O (n)).

Jusqu'à présent, j'ai dû découvrir les big-O via des essais et des erreurs, et parfois regarder le code source . Maintenant pour la question ...

Existe-t-il une liste des grands temps O théoriques (ou pratiques) pour toutes * les fonctions PHP intégrées?

* ou du moins les plus intéressants

Par exemple, je trouve qu'il est très difficile de prédire le grand O des fonctions énumérées parce que la mise en œuvre possible dépend des structures de données de base inconnue de PHP: array_merge, array_merge_recursive, array_reverse, array_intersect, array_combine, str_replace(avec des entrées de tableau), etc.

Kendall Hopkins
la source
31
Totalement hors sujet mais, 1 n'est pas premier.
Jason Punyon
24
Les tableaux en PHP sont des tables de hachage. Cela devrait vous dire tout ce que vous devez savoir. La recherche d'une clé dans une table de hachage est O (1). La recherche d'une valeur est O (n) - que vous ne pouvez pas battre sur un ensemble non trié. La plupart des fonctions qui vous intéressent sont probablement O (n). Bien sûr, si vous voulez vraiment savoir, vous pouvez lire la source: cvs.php.net/viewvc.cgi/php-src/ext/standard/…
Frank Farmer
11
Pour mémoire, l'implémentation la plus rapide de ce que vous essayez de faire serait (au lieu d'utiliser NULL pour vos valeurs) d'utiliser true, puis de tester la présence en utilisant isset($large_prime_array[$number]). Si je me souviens bien, c'est dans l'ordre d'être des centaines de fois plus rapide que la in_arrayfonction.
mattbasta
3
La notation Big O n'est pas une question de vitesse. Il s'agit de limiter les comportements.
Gumbo
3
@Kendall, je ne compare pas à array_key_exists, je compare à in_array. in_arrayitère chaque élément du tableau et compare la valeur à l'aiguille que vous lui passez. Si vous retournez les valeurs à la clé (et remplacez simplement chacune des valeurs par une valeur fictive comme true, l'utilisation issetest beaucoup plus rapide. En effet, les clés d'un tableau sont indexées par PHP (comme une table de hachage). Par conséquent, la recherche une matrice de cette manière peut avoir une amélioration significative de la vitesse.
mattbasta

Réponses:

649

Comme il ne semble pas que quelqu'un l'ait fait avant, je pensais que ce serait une bonne idée de l'avoir pour référence quelque part. Je suis allé cependant et soit par référence ou par écrémage de code pour caractériser les array_*fonctions. J'ai essayé de placer le Big-O le plus intéressant près du sommet. Cette liste n'est pas complète.

Remarque: Tous les Big-O sont calculés en supposant qu'une recherche de hachage est O (1) même si c'est vraiment O (n). Le coefficient du n est si bas que le surcoût de stockage d'un stockage suffisamment grand vous nuirait avant que les caractéristiques de la recherche Big-O ne commencent à prendre effet. Par exemple, la différence entre un appel array_key_existsà N = 1 et N = 1 000 000 est une augmentation de temps de ~ 50%.

Points intéressants :

  1. isset/ array_key_existsest beaucoup plus rapide que in_arrayetarray_search
  2. +(union) est un peu plus rapide que array_merge(et semble plus joli). Mais cela fonctionne différemment, gardez cela à l'esprit.
  3. shuffle est sur le même niveau Big-O que array_rand
  4. array_pop/ array_pushest plus rapide que array_shift/ en array_unshiftraison d'une pénalité de réindexation

Recherches :

array_key_existsO (n) mais très proche de O (1) - c'est à cause de l'interrogation linéaire dans les collisions, mais parce que le risque de collisions est très faible, le coefficient est également très faible. Je trouve que vous traitez les recherches de hachage comme O (1) pour donner un big-O plus réaliste. Par exemple, la différence entre N = 1000 et N = 100000 n'est que d'environ 50% de ralentissement.

isset( $array[$index] )O (n) mais très proche de O (1) - il utilise la même recherche que array_key_exists. Étant donné que c'est une construction de langage, mettra en cache la recherche si la clé est codée en dur, ce qui entraîne une accélération dans les cas où la même clé est utilisée à plusieurs reprises.

in_array O (n) - c'est parce qu'il effectue une recherche linéaire dans le tableau jusqu'à ce qu'il trouve la valeur.

array_search O (n) - il utilise la même fonction principale que in_array mais renvoie une valeur.

Fonctions de file d'attente :

array_push O (∑ var_i, pour tout i)

array_pop O (1)

array_shift O (n) - il doit réindexer toutes les clés

array_unshift O (n + ∑ var_i, pour tout i) - il doit réindexer toutes les clés

Intersection, union, soustraction de tableau :

array_intersect_key si l'intersection 100% fait O (Max (param_i_size) * ∑param_i_count, pour tout i), si l'intersection 0% intersecte O (∑param_i_size, pour tout i)

array_intersect si l'intersection 100% fait O (n ^ 2 * ∑param_i_count, pour tout i), si l'intersection 0% intersecte O (n ^ 2)

array_intersect_assoc si l'intersection 100% fait O (Max (param_i_size) * ∑param_i_count, pour tout i), si l'intersection 0% intersecte O (∑param_i_size, pour tout i)

array_diff O (π param_i_size, pour tout i) - C'est le produit de toutes les param_sizes

array_diff_key O (∑ param_i_size, pour i! = 1) - c'est parce que nous n'avons pas besoin d'itérer sur le premier tableau.

array_merge O (∑ array_i, i! = 1) - n'a pas besoin d'itérer sur le premier tableau

+ (union) O (n), où n est la taille du 2e tableau (c'est-à-dire array_first + array_second) - moins de surcharge que array_merge car il n'a pas besoin de renuméroter

array_replace O (∑ array_i, pour tout i)

Aléatoire :

shuffle Sur)

array_rand O (n) - Nécessite un sondage linéaire.

Big-O évident :

array_fill Sur)

array_fill_keys Sur)

range Sur)

array_splice O (décalage + longueur)

array_slice O (décalage + longueur) ou O (n) si longueur = NULL

array_keys Sur)

array_values Sur)

array_reverse Sur)

array_pad O (pad_size)

array_flip Sur)

array_sum Sur)

array_product Sur)

array_reduce Sur)

array_filter Sur)

array_map Sur)

array_chunk Sur)

array_combine Sur)

Je tiens à remercier Eureqa pour avoir facilité la recherche du Big-O des fonctions. C'est un programme gratuit incroyable qui peut trouver la meilleure fonction d'ajustement pour des données arbitraires.

ÉDITER:

Pour ceux qui doutent que les recherches de tableaux PHP le soient O(N), j'ai écrit un benchmark pour le tester (elles sont toujours efficaces O(1)pour les valeurs les plus réalistes).

graphique de recherche de tableau php

$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
la source
5
@Kendall: Merci! J'ai fait un peu de lecture et il s'avère que PHP utilise des tables de hachage «imbriquées» pour les collisions. Autrement dit, au lieu d'une structure de connexion pour les collisions, il utilise simplement une autre table de hachage. Et je comprends que pratiquement les tables de hachage PHP donnent des performances O (1), ou au moins O (1) en moyenne - c'est à cela que servent les tables de hachage. J'étais simplement curieux de savoir pourquoi vous avez dit qu'ils étaient "vraiment O (n)" et non "vraiment O (logn)". Chouette post au fait!
Cam
10
Les complexités temporelles doivent être incluses dans la documentation! Choisir la bonne fonction peut vous faire gagner beaucoup de temps ou vous éviter de faire ce que vous aviez prévu: p Merci pour cette liste déjà!
Samuel
41
Je sais que c'est vieux ... mais quoi? Cette courbe ne montre pas du tout O (n), elle montre O (log n), en.wikipedia.org/wiki/Logarithm . Ce qui est également exact avec ce que vous attendez des cartes de hachage imbriquées.
Andreas
5
Quel est le Big-O de unset sur un élément d'un tableau?
Chandrew
12
Alors que les tables de hachage ont en effet la complexité de recherche O (n) dans le pire des cas, le cas moyen est O (1) et le cas particulier que votre benchmark teste est même garanti O (1), car il s'agit d'une base zéro, continue, indexée numériquement tableau, qui n'aura jamais de collisions de hachage. La raison pour laquelle vous voyez toujours une dépendance à la taille du tableau n'a rien à voir avec la complexité algorithmique, elle est causée par des effets de cache CPU. Plus le tableau est grand, plus il est probable que les recherches à accès aléatoire entraînent des échecs de cache (et des échecs de cache plus élevés dans la hiérarchie).
NikiC
5

L'explication du cas que vous décrivez spécifiquement est que les tableaux associatifs sont implémentés comme des tables de hachage - donc la recherche par clé (et en conséquence, array_key_exists) est O (1). Cependant, les tableaux ne sont pas indexés par valeur, donc la seule façon dans le cas général de découvrir si une valeur existe dans le tableau est une recherche linéaire. Il n'y a pas de surprise là-bas.

Je ne pense pas qu'il existe une documentation complète et spécifique de la complexité algorithmique des méthodes PHP. Cependant, si c'est une préoccupation suffisamment importante pour justifier l'effort, vous pouvez toujours parcourir le code source .

Dathan
la source
Ce n'est pas vraiment une réponse. Comme je l'ai dit dans la question, j'ai déjà essayé de chercher dans le code source PHP. Étant donné que PHP est implémenté, il est écrit en C en utilisant des macros complexes, ce qui peut parfois rendre difficile la "visualisation" du grand O sous-jacent des fonctions.
Kendall Hopkins du
@Kendall J'ai ignoré votre référence à la plongée dans le code source. Cependant, il y a une réponse dans ma réponse: "Je ne pense pas qu'il existe une documentation complète spécifique de la complexité algorithmique des méthodes PHP." "Non" est une réponse parfaitement valable. (c:
Dathan
4

Vous voulez presque toujours utiliser issetau lieu de array_key_exists. Je ne regarde pas les internes, mais je suis sûr que array_key_existsc'est O (N) car il itère sur chaque clé du tableau, tout en issetessayant d'accéder à l'élément en utilisant le même algorithme de hachage qui est utilisé lorsque vous accédez un index de tableau. Cela devrait être O (1).

Un "piège" à surveiller est le suivant:

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

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

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

J'étais curieux, j'ai donc comparé la différence:

<?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 secondes
array_key_exists:2.33202195168 secondes

Bien sûr, cela ne montre pas la complexité temporelle, mais cela montre comment les 2 fonctions se comparent.

Pour tester la complexité temporelle, comparez le temps nécessaire à l'exécution de l'une de ces fonctions sur la première clé et la dernière clé.

ryeguy
la source
9
C'est faux. Je suis sûr à 100% que array_key_exists n'a pas à itérer sur chaque clé. Si vous ne croyez pas, jetez un œil au lien ci-dessous. La raison pour laquelle isset est tellement plus rapide est qu'il s'agit d'une construction de langage. Ce qui signifie qu'il n'a pas la surcharge de faire un appel de fonction. De plus, je pense que cela pourrait mettre la recherche en cache, à cause de cela. De plus, ce n'est pas une réponse à LA QUESTION! Je voudrais une liste de Big (O) pour les fonctions PHP (comme l'indique la question). Pas une seule référence de mes exemples. svn.php.net/repository/php/php-src/branches/PHP_5_3/ext/…
Kendall Hopkins
Si vous ne me croyez toujours pas, j'ai créé une petite référence pour démontrer le point. pastebin.com/BdKpNvkE
Kendall Hopkins
Ce qui ne va pas avec votre benchmark, c'est que vous devez désactiver xdebug. =)
Guilherme Blanco
3
Il existe deux raisons essentielles pour lesquelles vous souhaitez utiliser isset sur array_key_exists. Tout d'abord, isset est une construction de langage qui atténue le coût d'un appel de fonction. Cela s'apparente à l' argument $arrray[] = $appendvs. array_push($array, $append)Deuxièmement, array_key_exists différencie également les valeurs non définies et nulles. Pour $a = array('fred' => null); array_key_exists('fred', $a)retournera vrai tandis que isset($['fred'])retournera faux. Cette étape supplémentaire n'est pas anodine et augmentera considérablement le temps d'exécution.
orque
0

Si les gens rencontraient des problèmes dans la pratique avec des collisions clés, ils implémenteraient des conteneurs avec une recherche de hachage secondaire ou un arbre équilibré. L'arbre équilibré donnerait le comportement O (log n) dans le pire des cas et O (1) moy. cas (le hachage lui-même). Le surcoût n'en vaut pas la peine dans la plupart des applications de mémoire, mais il existe peut-être des bases de données qui implémentent cette forme de stratégie mixte comme cas par défaut.

Josh Stern
la source