
C'était le soir, il n'y avait rien à faire. Il est temps d'organiser une petite analyse de la façon dont certaines méthodes de tri des tableaux en PHP diffèrent en interne.
Sources de la branche maître (c'est maintenant 7.4 entrecoupé de 8)
Le générateur d'opcode de php 7.3.0.
Des mesures ont été effectuées le 7.3.6.
Avertissement pour les nerds: mentionner quelques nanosecondes et cycles de processeur est une astuce polémique appelée "hyperbole".
Peut-être, en fait, il y a des dizaines ou des centaines de nanosecondes et des milliers de mesures, mais il est encore si petit que la nécessité de les enregistrer suggère que quelque chose dans votre code est incorrect.
Étape de compilation
for ,
foreach ,
do et
while sont des mots clés du langage, tandis que les fonctions
array_ * sont des fonctions de la bibliothèque standard. Par conséquent, ceteris paribus, selon le premier, l'analyseur exécutera quelques nanosecondes plus rapidement.
Analyseur
Jusqu'à ce que le jeton d'
instruction , le chemin sera le même pour tous
start -> top_statement_list -> top_statement -> statement
Les boucles sont définies au niveau de l'
instruction :
->T_FOR '(' for_exprs ';' for_exprs ';' for_exprs ')' for_statement ->T_FOREACH '(' expr T_AS foreach_variable ')' foreach_statement ->T_FOREACH '(' expr T_AS foreach_variable T_DOUBLE_ARROW foreach_variable ')' foreach_statement ->T_DO statement T_WHILE '(' expr ')' ';' ->T_WHILE '(' expr ')' while_statement
La différence entre
for_exprs et just
expr est seulement que pour le premier, écrire plusieurs
expr est autorisé, séparés par des virgules.
foreach_variable est une construction qui, en plus de la
variable , suit également la décompression à l'aide de
list ou
[] .
for_statement ,
foreach_statement ,
while_statement diffèrent de l'
instruction standard en ce qu'ils ajoutent la possibilité d'analyser une syntaxe alternative:
foreach($array as $element):
L'appel de fonction est enterré beaucoup plus profondément:
-> expr -> variable -> callable_variable -> function_call -> name argument_list
callable_variable , hmm ... Drôle, n'est-ce pas? :)
Passons aux opcodes.
À titre d'exemple, prenons une simple itération d'un tableau indexé avec impression de chaque clé et valeur. Il est clair que l'utilisation de
for ,
while et
do pour une telle tâche n'est pas justifiée, mais notre objectif est simplement de montrer le périphérique interne.
foreach
$arr = ['a', 'b', 'c'];
line #* EIO op fetch ext return operands --------------------------------------------------------------------------- 2 0 E > ASSIGN !0, <array> 4 1 > FE_RESET_R $4 !0, ->7 2 > > FE_FETCH_R ~5 $4, !1, ->7 3 > ASSIGN !2, ~5 4 CONCAT ~7 !2, !1 5 ECHO ~7 6 > JMP ->2 7 > FE_FREE $4 5 8 > RETURN 1
Que se passe-t-il ici:
FE_RESET_R : crée un itérateur de
4 $ pour le tableau
! 0 . Ou, si le tableau est vide, il passe immédiatement à l'instruction
7 .
FE_FETCH_R : effectue une étape d'itération, récupère la clé actuelle à
~ 5 et la valeur à
! 1 . Ou, si la fin du tableau est atteinte, il passe à l'instruction
7 .
Les instructions
3-6 ne
sont pas particulièrement intéressantes. Ici, il y a une conclusion et retour à
FE_FETCH_R .
FE_FREE : détruit l'itérateur.
car pendant que ...
$length = count($arr); for($i = 0; $i < $length; $i++) echo $i.$arr[$i];
Il s'agit en fait d'un cas particulier de
$i = 0; while($i < $length) { echo $i.$arr[$i]; $i++; }
Il s'agit en fait d'un cas spécial de
if + goto $i = 0; goto check; body: echo $i.$arr[$i]; $i++; check: if($i < $length) goto body;
Les opcodes pour les trois cas seront presque identiques. Sauf dans le cas de
if ,
JMPNZ passera à une paire de
JMPZ + JMP en raison de l'entrée dans le corps de
if .
Pour la boucle
do , les opcodes varieront légèrement en raison de leur nature post-vérification.
line #* EIO op fetch ext return operands --------------------------------------------------------------------------- 3 0 E > ASSIGN !0, <array> 5 1 COUNT ~4 !0 2 ASSIGN !1, ~4 6 3 ASSIGN !2, 0 4 > JMP ->10 5 > FETCH_DIM_R ~7 !0, !2 6 CONCAT ~8 !2, ~7 7 ECHO ~8 8 POST_INC ~9 !2 9 FREE ~9 10 > IS_SMALLER ~10 !2, !1 11 > JMPNZ ~10, ->5 12 > > RETURN 1
Comme prévu, il y a plus d'opcodes.
0-2 : calcul
de la longueur $ .
3: $ i = 04, 10, 11 : contrôle initial de la condition de sortie et, en fait, soit la sortie soit la transition vers le corps du cycle.
5 (FETCH_DIM_R): $ arr [$ i]6-7 : conclusion
8-9: $ i ++ (notez que l'opcode increment retourne dans tous les cas une valeur, même si elle n'est pas nécessaire. Par conséquent, une instruction
FREE supplémentaire est nécessaire pour l'effacer).
10-11 : vérification de la condition de sortie après l'incrément.
Ou vous pouvez toujours répéter
$value = reset($arr); $key = key($arr); while($key !== null) { echo $key.$value; $value = next($arr); $key = key($arr); }
Drap de lit donc sous le spoiler line #* EIO op fetch ext return operands --------------------------------------------------------------------------- 3 0 E > ASSIGN !0, <array> 5 1 INIT_FCALL 'reset' 2 SEND_REF !0 3 DO_ICALL $4 4 ASSIGN !1, $4 6 5 INIT_FCALL 'key' 6 SEND_VAR !0 7 DO_ICALL $6 8 ASSIGN !2, $6 7 9 > JMP ->20 8 10 > CONCAT ~8 !2, !1 11 ECHO ~8 9 12 INIT_FCALL 'next' 13 SEND_REF !0 14 DO_ICALL $9 15 ASSIGN !1, $9 10 16 INIT_FCALL 'key' 17 SEND_VAR !0 18 DO_ICALL $11 19 ASSIGN !2, $11 7 20 > IS_NOT_IDENTICAL ~13 !2, null 21 > JMPNZ ~13, ->10 11 22 > > RETURN 1
Cette option est bonne car elle convient pour itérer sur un tableau avec n'importe quelle clé, et pas seulement avec des entiers à augmentation monotone.
Les fonctions
reset ,
next et
key sont assez légères, mais il y a encore des frais généraux pour les appeler. Et, comme nous le verrons plus loin, ces coûts sont élevés.
Bien que cette approche soit très similaire au principe de
foreach , il existe deux différences fondamentales entre elles.
1) Alors que
reset ,
next et
key (et
current aussi) fonctionnent directement avec le pointeur interne d'un tableau,
foreach utilise son propre itérateur et ne change pas l'état du pointeur interne.
C'est-à-dire
foreach($arr as $v) echo $v.' - '.current($arr).PHP_EOL;
2) Lorsque vous utilisez
foreach pour itérer par valeur, quoi que vous fassiez avec le tableau à l'intérieur de la boucle, l'ensemble de données d'origine sera itéré
foreach($arr as $v) { $arr = ['X','Y','Z']; echo $v.PHP_EOL; } var_dump($arr);
Ce qui se passera lors de l'itération sur le lien peut être lu dans
ce RFC . Tout n'y est pas très simple.
array_walk avec fonction anonyme
array_walk($arr, function($value, $key){ echo $key.$value;});
Puisqu'une fonction personnalisée est utilisée, il y aura un ensemble supplémentaire d'opcodes.
Fonction
line #* EIO op fetch ext return operands --------------------------------------------------------------------------- 0 E > RECV !0 1 RECV !1 2 CONCAT ~2 !1, !0 3 ECHO ~2 4 > RETURN null
Code principal
line #* EIO op fetch ext return operands --------------------------------------------------------------------------- 2 0 E > ASSIGN !0, <array> 4 1 INIT_FCALL 'array_walk' 2 SEND_REF !0 3 DECLARE_LAMBDA_FUNCTION '%00%7Bclosure%7D%2Fin%2FHuNEK0x7f9fa62d105a' 4 SEND_VAL ~2 5 DO_ICALL 6 > RETURN 1
Comme
array_walk , comme le reste des fonctions de la bibliothèque standard, est intrinsèque, il n'y a pas de mécanisme d'itération dans les opcodes compilés.
INIT_FCALL : initialisez l'appel à
array_walkSEND_REF : mettre une référence de tableau sur la pile d'appels
DECLARE_LAMBDA_FUNCTION : déclarer une fonction anonyme
SEND_VAL : Nous avons mis une fonction anonyme sur la pile des appels
DO_ICALL : exécutez
array_walk pour exécuter
Ensuite, la magie se produit avec l'appel de notre lambda pour chaque élément du tableau.
array_walk à l' aide d'une fonction prédéfinie
Pas très différent d'appeler avec un anonyme, sauf peut-être un peu moins que la surcharge de création d'un lambda au moment de l'exécution.
line #* EIO op fetch ext return operands --------------------------------------------------------------------------- 3 0 E > ASSIGN !0, <array> 9 2 INIT_FCALL 'array_walk' 3 SEND_REF !0 4 SEND_VAL 'my_echo' 5 DO_ICALL 6 > RETURN 1
Les conclusions sont banales.
foreach est conçu pour itérer les tableaux, tandis que le reste des boucles n'est qu'un wrapper sur
if + goto .
Les fonctions de la bibliothèque standard fonctionnent sur le principe d'une boîte noire.
Plonger un peu plus profondément
Considérons d'abord le cas avec
for et son opcode
FETCH_DIM_R , qui est utilisé pour extraire la valeur par clé. L'élément est extrait via une recherche dans la table de hachage (
ZEND_HASH_INDEX_FIND ). Dans notre cas, l'extraction provient d'un
tableau emballé (les clés sont une séquence numérique continue) - c'est une opération assez facile et rapide. Pour les baies non emballées, ce sera légèrement plus cher.
Passons maintenant à
chacun (
FE_FETCH_R ). Tout est banal ici:
- Vérification si le pointeur de l'itérateur est en dehors du tableau
- Récupérer la clé et la valeur actuelles
- Pointeur d'incrément
Eh bien,
qu'advient- il des fonctions comme
array_walk ? Bien qu'ils fassent tous des choses différentes, ils ont tous un seul et même principe.
S'il est complètement simplifié, alors (pseudo-code):
reset_pointer() do { value = get_current_value() if (value == NULL) break
En fait, tout est plus compliqué à l'intérieur, mais l'essentiel est le même - il y a une recherche assez rapide de la table de hachage sans la participation de la machine virtuelle PHP (sans tenir compte de l'appel à la fonction utilisateur).
Eh bien, quelques mesures
Et puis après tout, quel est l'article sans mesures (de mémoire, il s'est avéré si égal qu'il a supprimé sa mesure).
En tant que tableau, selon la tradition, nous prenons
zend_vm_execute.h pour 70,108 lignes.
Script de mesure du spoiler <?php $array = explode("\n", file_get_contents('/Users/rjhdby/CLionProjects/php-src/Zend/zend_vm_execute.h')); function myEcho($key, $value){ echo $key . $value; } ob_start(); $startTime = microtime(true); for ($i = 0; $i < 10; $i++) {
Chaque mesure a été exécutée 10 fois, en choisissant la plus fréquente sur les 4 premiers chiffres.
foreach time: 0.12159085273743 foreach(reference) time: 0.11191201210022 for, while time: 0.13130807876587 array_walk(lambda) time: 0.87953400611877 array_walk(static lambda) time: 0.87544417497211 array_walk(name) time: 0.50753092765808 next,key time: 1.06258893013
Pour résumer
En analysant les résultats, n'oubliez pas de prendre en compte qu'ils ont été obtenus en 10 passages à travers un tableau de 70 000 éléments.
L'anti-héros absolu était «l'émulation» de
foreach avec
la touche suivante / . Ne le faites que si cela est absolument nécessaire.
array_walk avec un lambda respire dans le dos, mais il y a une nuance. Le prochain JIT peut radicalement changer la situation. Ou peut-être ne pas changer. Ce sera intéressant à voir.
array_walk utilisant une fonction prête à l'emploi est un moyen fort.
Étant donné que
foreach fonctionne un peu différemment lors de l'itération sur un lien (il utilise l'opcode
FE_FETCH_RW au lieu de
FE_FETCH_R ), il a fait une mesure distincte pour lui. Il s'est vraiment avéré un peu plus vite.
Il s'est avéré que la création d'un lambda à la volée n'est pas l'opération la moins chère. Il semblerait qu'il ne soit créé que 10 fois. Il faudra étudier.
Toutes les autres méthodes ont montré approximativement les mêmes résultats, avec un très léger écart.
Merci de votre attention!
Si vous avez des suggestions, que pouvez-vous "choisir" d'autre - écrivez dans les commentaires. Je pense toujours aux lambdas - une telle baisse des performances est très étrange.
UPDAjout de la mesure pour
array_walk avec lambda statique. La différence n'est pas visible.