2017-10-10 4 views
1

Donc j'écris une fonction qui prend un tuple en argument et fait plein de trucs dessus. Voici ce qui ressemble à:Python charge-t-il des arguments de fonction dans des registres ou les conserve-t-il dans la pile?

def swap(self, location): 
    if (location[0] < 0 or location[1] < 0 or 
     location[0] >= self.r or location[1] >= self.c): 
     return False 

    self.board[0][0] = self.board[location[0]][location[1]] 
    self.board[location[0]][location[1]] = 0 
    self.empty = (location[0],location[1]) 

Je suis en train de faire mon code aussi efficace que possible, de sorte que je ne suis pas modifier les valeurs de location, est-il logique de charger les variables dans les registres (loc0 = location[0]; loc1 = location[1]) pour des calculs plus rapides (lecture à cycle nul) ou location est-il déjà chargé dans les registres par le compilateur Python lorsqu'il est passé en argument de fonction?

Modifier: J'ai mordu la balle et j'ai fait quelques tests. Voici les résultats (en quelques secondes) pour cette fonction en cours d'exécution 10 millions de fois avec les entrées répétitives: "up", "down", "left", "right" (respectivement)

Code as is: 
    run#1: 19.39 
    run#2: 17.18 
    run#3: 16.85 
    run#4: 16.90 
    run#5: 16.74 
    run#6: 16.76 
    run#7: 16.94 

Code after defining location[0] and location[1] in the beginning of the function: 
    run#1: 14.83 
    run#2: 14.79 
    run#3: 14.88 
    run#4: 15.033 
    run#5: 14.77 
    run#6: 14.94 
    run#7: 14.67 

C'est une moyenne de 16% d'augmentation des performances. Certainement pas insignifiant pour mon cas. Bien sûr, ce n'est pas scientifique car j'ai besoin de faire plus de tests dans plus d'environnements avec plus d'entrées, mais assez pour mon cas d'utilisation simple!

Les temps mesurés en utilisant Python 2.7 sur un Macbook Pro (Early 2015), qui a un Broadwell i5-5257U CPU (2c4t max turbo 3.1GHz, 2.7GHz soutenu, 3 Mo de cache L3). IDE était: PyCharm Edu 3.5.1 JRE: 1.8.0_112-version-408-b6 x86_64 JVM: machine virtuelle serveur OpenJDK 64 bits.

Malheureusement, ceci est pour une classe qui se base sur la vitesse du code.

+0

Si vous faites 'loc0 = location [0]', vous définissez une variable, pas un "registre". – BrenBarn

+2

Quel compilateur JIT python utilisez-vous? Si vous utilisez un interpréteur, il est peu probable que les variables Python vivent dans des registres entre différentes expressions. En C, un avantage de l'utilisation de variables locales est quand il y a un alias possible. (par exemple si le compilateur pense modifier self.board [0] [0] pourrait avoir aussi modifié l'emplacement [0], le lire dans un local laisserait le compilateur le garder dans un registre.) IDK si c'est quelque chose en Python. (Je suis ici à cause de la balise [tag: assembly].) –

+0

@BrenBarn Oui, mais les compilateurs optimisent les variables définies qui sont fréquemment utilisées en leur allouant des registres. – QuantumHoneybees

Répondre

1

Si vous utilisez un interpréteur, il est peu probable que les variables Python vivent dans des registres entre différentes expressions. Vous pouvez voir comment la source Python a été compilée en byte-code.

Le bytecode Python (le type stocké dans les fichiers en dehors de l'interpréteur) est basé sur une pile (http://security.coverity.com/blog/2014/Nov/understanding-python-bytecode.html). Ce code-octet est ensuite interprété ou compilé par JIT en code machine natif. Python régulier interprète uniquement, donc il n'est pas plausible pour lui de conserver les variables python dans les registres de machines sur plusieurs instructions.

Un interpréteur écrit en C peut conserver le sommet de la pile de bytecode dans une variable locale à l'intérieur d'une boucle d'interprétation, et le compilateur C peut conserver cette variable C dans un registre. Donc, l'utilisation répétée de la même variable Python pourrait finir par ne pas avoir trop d'allers-retours magasin/rechargement.

Notez que la latence de réacheminement de mémoire sur votre CPU Broadwell est d'environ 4 ou 5 cycles d'horloge, loin des centaines de cycles pour un aller-retour vers DRAM. Un magasin/rechargement n'a même pas à attendre que le magasin se retire et s'engage dans le cache L1D; il est transmis directement à partir du tampon du magasin. Connexes: http://blog.stuffedcow.net/2014/01/x86-memory-disambiguation/ et http://agner.org/optimize/, et d'autres liens dans le wiki des tags ). La latence d'utilisation de la charge est également limitée à 5 cycles d'horloge pour un appel de cache L1D (la latence de l'adresse étant prête pour les données.) Vous pouvez la mesurer en pointant sur une liste chaînée (dans asm). nombre d'instructions qu'il exécute pour savoir quoi faire ensuite) que ce n'est probablement même pas le goulot d'étranglement. Conserver une variable python spécifique dans un registre n'est pas du tout plausible pour un interpréteur. Même si vous avez écrit un interpréteur dans asm, le problème fondamental est que les registres ne sont pas adressables. Une instruction x86 add r14d, eax doit avoir les deux registres codés en dur dans le code machine de l'instruction. (Tous les autres ISA fonctionnent de la même manière: les numéros de registre font partie du code machine pour l'instruction, sans indirection basée sur des données). Même si l'interpréteur a fait le travail pour comprendre qu'il devait ajouter "reg-var # 3 à reg-var # 2" (ie décoder les opérations de pile de bytecode en variables de registre pour une représentation interne qu'il interprète), avoir à utiliser une fonction différente de toute autre combinaison de registres.

Étant donné un entier, les seules façons d'obtenir la valeur du Nième registre sont de se brancher sur une instruction qui utilise ce registre, ou de stocker tous les registres en mémoire et d'indexer le tableau résultant. (Ou peut-être une sorte de comparaison et de masquage sans branchette). Quoi qu'il en soit, essayer de faire quelque chose de spécifique n'est pas rentable, c'est pourquoi les gens écrivent simplement l'interpréteur en C et permettent au compilateur C d'optimiser (espérons-le) le code machine qui fonctionnera réellement. Ou vous écrivez un compilateur JIT comme Sun l'a fait pour Java (la machine virtuelle HotSpot). IDK s'il y en a pour Python. Voir Does the Python 3 interpreter have a JIT feature?.

Un compilateur JIT transforme réellement le code Python en code machine, où l'état de registre contient principalement des variables Python plutôt que des données d'interpréteur. Encore une fois, sans compilateur JIT (ou compilateur d'avance), "garder les variables dans les registres" n'est pas une chose.


Il est probablement plus rapide car il évite l'opérateur [] et autres frais généraux (voir la réponse de Bren, que vous avez accepté)


Note: un ISAs couple ont des registres mappés en mémoire. par exemple. AVR (microcontrôleurs RISC 8 bits), où la puce a également intégré SRAM contenant la gamme basse d'adresses de mémoire qui comprend les registres. Vous pouvez donc faire une charge indexée et obtenir le contenu du registre, mais vous auriez tout aussi bien pu le faire dans la mémoire qui ne contenait pas le contenu du registre architectural.

1

Le Python VM ne utilise une pile pour exécuter son bytecode, et cette pile est complètement indépendant de la pile de matériel. Vous pouvez utiliser dis pour démonter votre code pour voir comment vos modifications affectent le bytecode généré.

+0

Cela peut être vrai pour CPython, mais il est plus exact de dire que ce comportement n'est pas spécifié du tout. – BrenBarn

1

Ce sera un peu plus vite si vous stockez ces variables deux:

loc0 = location[0] 
loc1 = location[1] 

Parce qu'il y aura seulement deux look-up au lieu de quatre. Btw, si vous voulez utiliser python, vous ne devriez pas faire attention aux performances de ce bas niveau.

+1

Malheureusement, ceci est pour une classe qui se base sur la vitesse du code. Ridicule, je sais mais je ne peux pas faire grand chose malheureusement. – QuantumHoneybees

1

Ces types de détails ne font pas partie du comportement spécifié de Python. Comme le dit la réponse d'Ignacio, CPython le fait d'une façon, mais cela n'est pas garanti par le langage lui-même. La description de Python de ce qu'il fait est très éloignée des notions de bas niveau comme les registres, et la plupart du temps, il n'est pas utile de s'inquiéter de savoir comment Python fait la correspondance avec ces détails. Python est un langage de haut niveau dont le comportement est défini en termes d'abstractions de haut niveau, comme une API.

Dans tous les cas, faire quelque chose comme loc0 = language[0] en code Python n'a rien à voir avec les registres de réglage. C'est juste créer un nouveau nom Python pointant un objet Python existant.

Cela dit, il y a une différence de performance, parce que si vous utilisez location[0] partout, la recherche réelle (ou au moins peut - en théorie une implémentation de Python intelligente pourrait optimiser ce) se encore et encore à chaque fois que l'expression location[0] est évalué.Mais si vous faites loc0 = location[0] puis utilisez loc0 partout, vous savez que la recherche ne se produit qu'une seule fois. Dans des situations typiques (par exemple, location est une liste Python ou dict, vous n'exécutez pas ce gazillions de code de fois dans une boucle serrée) cette différence sera minime.

+0

Y a-t-il un coût d'assignation à une variable locale dans la plupart/certains interpréteurs/compilateurs JIT? En C vous obtiendrez le même asm pour 'int tmp = a [i]; b [i] = tmp; 'comme vous le feriez pour' b [i] = a [i]; '(en supposant que' tmp' ne soit pas utilisé plus tard, et que vous compiliez avec l'optimisation activée bien sûr). S'il y a un coût, c'est probablement une victoire de perf à assigner à un local si vous utilisez même la valeur deux fois, vous dites? Même si une fois est une perte de perf? (En moyenne, je veux dire, dans une boucle de petite à moyenne avec beaucoup d'itérations, je suis sûr que vous pourriez construire des cas où ce n'est pas vrai.) –

+0

@PeterCordes: Eh bien, des bytecodes supplémentaires seront générés pour faire l'assignation. Je serais choqué de voir une implémentation Python où l'attribution à un local a un coût non négligeable. Je ne fais pas ce que font les compilateurs JIT; en théorie, ils pourraient optimiser certaines missions, mais je ne suis pas sûr de leur intelligence à ce sujet.En théorie ce serait un gain de perf à assigner à un local si vous utilisez la valeur plus d'une fois, mais c'est plus compliqué si vous avez besoin de modifier la valeur pendant le calcul. L'avantage de la performance est susceptible d'être indétectablement minuscule dans de nombreuses situations cependant. – BrenBarn

+1

Vous pouvez mesurer des différences de performances assez petites avec les compteurs de performance du processeur, comme une partie sur 100 est assez facile à détecter pour les petites boucles ASM (même si un ralentissement de 2% peut être difficile à expliquer ...) (https://stackoverflow.com/questions/44169342/can-x86s-mov-really-be-free-why-cant-i-reproduce-this-at-all/44193770#44193770)). Avec une mesure minutieuse, vous obtenez un rapport signal sur bruit supérieur à une partie dans 10k pour les microbenchmarks. J'ai déjà compris que si vous vous souciez vraiment autant de perf, python est généralement le mauvais langage: P –