Chaîne de transcripteur Python → 11l → C ++ [pour accélérer le code Python et plus]




Cet article décrit les conversions les plus intéressantes qu'une chaîne de deux transpilateurs effectue (le premier traduit le code Python en code dans le nouveau langage de programmation 11l , et le second traduit le code en 11l en C ++), et compare également les performances avec d'autres outils d'accélération / Exécution de code Python (PyPy, Cython, Nuitka).

Remplacement des tranches \ tranches par des plages

Python11l
s[-1] s[-2] s[:-1] s[1:] s[:2] s[1:2] s[::2] s[1::2] s[3:10:2] 
 s.last s[(len)-2] s[0..<(len)-1] s[1..] s[0..<2] s[1..<2] s[(0..).step(2)] s[(1..).step(2)] s[(3..<10).step(2)] 
L'indication explicite pour l'indexation à partir de la fin du tableau s[(len)-2] au lieu de simplement s[-2] nécessaire pour éliminer les erreurs suivantes:
  1. Lorsque, par exemple, il est nécessaire d'obtenir le caractère précédent par s[i-1] , mais pour i = 0 tel / cet enregistrement au lieu d'une erreur retournera silencieusement le dernier caractère de la chaîne [ et en pratique j'ai rencontré une telle erreur - commit ] .
  2. L'expression s[i:] après i = s.find(":") ne fonctionnera pas correctement lorsque le caractère n'est pas trouvé dans la chaîne [ au lieu de '' une partie de la chaîne commençant par le premier caractère : puis '' le dernier caractère de la chaîne sera pris ] (et généralement , Je pense que retourner -1 avec la fonction find() en Python est également incorrect [ devrait retourner null / None [ et si -1 est requis, il devrait être écrit explicitement: i = s.find(":") ?? -1 ] ] )
  3. L'écriture de s[-n:] pour obtenir les n derniers caractères d'une chaîne ne fonctionnera pas correctement lorsque n = 0.

Chaînes d'opérateurs de comparaison


À première vue, c'est une caractéristique exceptionnelle du langage Python, mais en pratique, il peut être facilement abandonné / distribué en utilisant l'opérateur in et les plages:
a < b < cb in a<..<c
a <= b < cb in a..<c
a < b <= cb in a<..c
0 <= b <= 9b in 0..9

Compréhension des listes


De même, comme il s'est avéré, vous pouvez refuser une autre fonctionnalité intéressante de Python: les listes de compréhension.
Alors que certains glorifient la compréhension des listes et suggèrent même d'abandonner `filter ()` et `map ()` , j'ai trouvé que:
  1. Dans tous les endroits où j'ai vu la compréhension de la liste de Python, vous pouvez facilement vous débrouiller avec les fonctions `filter ()` et `map ()`.
     dirs[:] = [d for d in dirs if d[0] != '.' and d != exclude_dir] dirs[:] = filter(lambda d: d[0] != '.' and d != exclude_dir, dirs) '[' + ', '.join(python_types_to_11l[ty] for ty in self.type_args) + ']' '[' + ', '.join(map(lambda ty: python_types_to_11l[ty], self.type_args)) + ']' # Nested list comprehension: matrix = [ [1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12], ] [[row[i] for row in matrix] for i in range(4)] list(map(lambda i: list(map(lambda row: row[i], matrix)), range(4))) 
  2. `filter ()` et `map ()` en 11l sont plus jolis qu'en Python
     dirs[:] = filter(lambda d: d[0] != '.' and d != exclude_dir, dirs) dirs = dirs.filter(d -> d[0] != '.' & d != @exclude_dir) '[' + ', '.join(map(lambda ty: python_types_to_11l[ty], self.type_args)) + ']' '['(.type_args.map(ty -> :python_types_to_11l[ty]).join(', '))']' outfile.write("\n".join(x[1] for x in fileslist if x[0])) outfile.write("\n".join(map(lambda x: x[1], filter(lambda x: x[0], fileslist)))) outfile.write(fileslist.filter(x -> x[0]).map(x -> x[1]).join("\n")) 
    et par conséquent, le besoin de compréhension de liste en 11l disparaît réellement [le remplacement de la compréhension de liste par filter() et / ou map() est effectué pendant la conversion du code Python en 11l automatiquement ] .

Convertir la chaîne if-elif-else en commutateur


Bien que Python ne contienne pas d'instruction switch, c'est l'une des plus belles constructions de 11l, et j'ai donc décidé d'insérer automatiquement switch:
Python11l
 ch = instr[i] if ch == "[": nesting_level += 1 elif ch == "]": nesting_level -= 1 if nesting_level == 0: break elif ch == "'": ending_tags.append(''') # '' elif ch == "'": assert(ending_tags.pop() == ''') 
 switch instr[i] '[' nesting_level++ ']' if --nesting_level == 0 loop.break "'" ending_tags.append("'") // '' "'" assert(ending_tags.pop() == "'") 
Pour être complet, voici le code C ++ généré
 switch (instr[i]) { case u'[': nesting_level++; break; case u']': if (--nesting_level == 0) goto break_; break; case u''': ending_tags.append(u"'"_S); break; // '' case u''': assert(ending_tags.pop() == u'''); break; } 


Convertir de petits dictionnaires en code natif


Considérez cette ligne de code Python:
 tag = {'*':'b', '_':'u', '-':'s', '~':'i'}[prev_char()] 
Très probablement, cette forme d'enregistrement n'est pas très efficace [ en termes de performances ] , mais elle est très pratique.

En 11l, l'entrée correspondant à cette ligne [ et obtenue par le transporteur Python → 11l ] est non seulement pratique [ cependant, pas aussi élégante qu'en Python ] , mais aussi rapide:
 var tag = switch prev_char() {'*' {'b'}; '_' {'u'}; '-' {'s'}; '~' {'i'}} 

La ligne ci-dessus est traduite en:
 auto tag = [&](const auto &a){return a == u'*' ? u'b'_C : a == u'_' ? u'u'_C : a == u'-' ? u's'_C : a == u'~' ? u'i'_C : throw KeyError(a);}(prev_char()); 
[ L'appel de fonction lambda sera compilé par le compilateur C ++ \ inline pendant le processus d'optimisation et seule la chaîne d'opérateurs restera ?/: ]

Dans le cas où une variable est affectée, le dictionnaire est laissé tel quel:
Python
 rn = {'I': 1, 'V': 5, 'X': 10, 'L': 50, ...} 
11l
 var rn = ['I' = 1, 'V' = 5, 'X' = 10, 'L' = 50, ...] 
C ++
 auto rn = create_dict(dict_of(u'I'_C, 1)(u'V'_C, 5)(u'X'_C, 10)(u'L'_C, 50)...); 

Capture \ Ca pture variables externes


En Python, pour indiquer qu'une variable n'est pas locale, mais doit être prise en dehors de [ de la fonction actuelle ] , le mot-clé non local est utilisé [ sinon, par exemple, found = True sera traité comme créant une nouvelle variable locale found , plutôt que d'attribuer une valeur déjà variable externe existante ] .
En 11l, le préfixe @ est utilisé pour cela:
Python11l
 writepos = 0 def write_to_pos(pos, npos): nonlocal writepos outfile.write(...) writepos = npos 
 var writepos = 0 fn write_to_pos(pos, npos) @outfile.write(...) @writepos = npos 
C ++:
 auto writepos = 0; auto write_to_pos = [..., &outfile, &writepos](const auto &pos, const auto &npos) { outfile.write(...); writepos = npos; }; 

Variables globales


Semblable aux variables externes, si vous oubliez de déclarer une variable globale en Python [en utilisant le mot-clé global ] , vous obtenez un bug invisible:
 break_label_index = -1 ... def parse(tokens, source_): global source, tokeni, token, scope source = source_ tokeni = -1 token = None break_label_index = -1 scope = Scope(None) ... 
 var break_label_index = -1 ... fn parse(tokens, source_) :source = source_ :tokeni = -1 :token = null break_label_index = -1 :scope = Scope(null) ... 
Le code 11l droite ] , contrairement à Python gauche ], break_label_index erreur "variable break_label_index non break_label_index " au break_label_index compilation.

Index / numéro de l'élément de conteneur actuel


J'oublie toujours l'ordre des variables que la fonction enumerate Python renvoie {la valeur vient en premier, puis l'index, ou vice versa}. Le comportement analogique dans Ruby - each.with_index - est beaucoup plus facile à retenir: avec index signifie que l'index vient après la valeur, pas avant. Mais en 11l, la logique est encore plus facile à retenir:
Python11l
 items = ['A', 'B', 'C'] for index, item in enumerate(items): print(str(index) + ' = ' + item) 
 var items = ['A', 'B', 'C'] loop(item) items print(loop.index' = 'item) 

Performances


Le programme de conversion du balisage PC en HTML est utilisé comme programme de test, et le code source de l' article sur le balisage PC est utilisé comme données source [ car cet article est actuellement le plus grand de ceux écrits sur le balisage PC ] , et est répété 10 fois, c'est-à-dire obtenu à partir d'une taille de fichier d'article de 48,8 kilo-octets de 488 Ko.

Voici un diagramme montrant combien de fois la manière correspondante d'exécuter du code Python est plus rapide que l'implémentation d'origine [ CPython ] :

Et maintenant, ajoutez au diagramme l'implémentation générée par le transpilateur Python → 11l → C ++:

Le temps d'exécution [ temps de conversion du fichier de 488 Ko ] était de 868 ms pour CPython et de 38 ms pour le code C ++ généré [ cette fois-ci inclut à part entière [ c. -à- d. pas seulement travailler avec des données en RAM ] exécutant le programme par le système d'exploitation et toutes les entrées / sorties [ lecture du fichier source [ .pq ] et sauvegarde du nouveau fichier [ .html ] sur le disque ] ] .

Je voulais également essayer Shed Skin , mais il ne prend pas en charge les fonctions locales.
Numba n'a pas pu être utilisé non plus (il génère une erreur «Utilisation de l'opcode inconnu LOAD_BUILD_CLASS»).
Voici l'archive avec le programme utilisé pour comparer les performances [ sous Windows ] (nécessite Python 3.6 ou supérieur et les packages Python suivants: pywin32, cython).

Code source en Python et sortie des transpilateurs Python -> 11l et 11l -> C ++:
PythonGénéré 11l
(avec des mots clés au lieu de lettres)
11l
(avec lettres)
C ++ généré

Source: https://habr.com/ru/post/fr431318/


All Articles