Maison >développement back-end >Tutoriel Python >Optimisation des performances Python du point de vue du compilateur
"La vie est courte, il faut du python" !
Les codeurs vétérans aiment vraiment l'élégance de Python. Cependant, dans un environnement de production, les fonctionnalités de langage dynamiques comme Python qui ne sont pas construites avec la performance comme priorité peuvent être dangereuses. Par conséquent, les bibliothèques hautes performances populaires telles que TensorFlow ou PyTorch sont principalement utilisées. python En tant que langage d'interface, utilisé pour interagir avec des bibliothèques C/C++ optimisées.
Il existe de nombreuses façons d'optimiser les performances des programmes Python. Du point de vue du compilateur, les hautes performances peuvent être compilées dans un langage de niveau inférieur analysable statiquement, tel que C ou C++, avec une surcharge d'exécution inférieure du code machine natif. lui permettant d'être comparable en performances à C/C++.
Codon peut être considéré comme un tel compilateur, tirant parti d'une compilation anticipée, d'une vérification de type bidirectionnelle spécialisée et d'une nouvelle représentation intermédiaire bidirectionnelle pour activer des extensions facultatives spécifiques à un domaine dans la syntaxe du langage et des optimisations du compilateur. Il permet aux programmeurs professionnels d'écrire du code hautes performances de manière intuitive, de haut niveau et familière.
Contrairement à d'autres implémentations Python orientées performances (telles que PyPy ou Numba), Codon est construit dès le départ en tant que système autonome, compilé à l'avance dans un exécutable statique, sans avoir besoin d'un runtime Python existant (par exemple, CPython ). Par conséquent, en principe, Codon peut obtenir de meilleures performances et surmonter les problèmes spécifiques à l'exécution de Python tels que les verrous globaux de l'interpréteur. En pratique, Codon compile les scripts Python (comme un compilateur C) en code natif, s'exécutant 10 à 100 fois plus rapidement que l'exécution interprétée.
Codon est modélisé sur la base du langage Seq, qui est un DSL bioinformatique. Seq a été conçu à l'origine comme un DSL de style pyramidal présentant de nombreux avantages, tels qu'une facilité d'adoption, d'excellentes performances et de puissantes capacités d'expression. Cependant, en raison de règles de type strictes, Seq ne prend pas en charge de nombreuses constructions courantes du langage Python et ne fournit pas non plus de mécanisme permettant d'implémenter facilement de nouvelles optimisations du compilateur. En appliquant l'IR bidirectionnel et des vérificateurs de type améliorés, Codon fournit une solution générale à ces problèmes basée sur Seq.
Codon couvre la plupart des fonctionnalités de Python et fournit un cadre pour réaliser une optimisation dans des domaines spécifiques. De plus, un système de typage flexible est fourni pour mieux gérer diverses fonctionnalités linguistiques. Le système de types est similaire à RPython et PyPy et au système de types statiques. Ces idées ont également été appliquées dans le contexte d'autres langages dynamiques, tels que PRuby. L'approche utilisée par l'IR bidirectionnel présente des similitudes avec les systèmes de type enfichable direct, tels que le cadre d'inspection de Java.
Bien que l'expression intermédiaire de Codon ne soit pas le premier IR personnalisable, elle ne prend pas en charge la personnalisation de tout le contenu, optant plutôt pour une personnalisation simple et bien définie qui peut être combinée avec la bidirectionnalité pour implémenter des fonctionnalités plus complexes. En termes de structure, CIR s'inspire de LLVM et de l'IR de Rust. Ces IR bénéficient d’un ensemble de nœuds et d’une structure considérablement simplifiés, ce qui simplifie la mise en œuvre des canaux IR. Structurellement, cependant, ces implémentations restructurent fondamentalement le code source, éliminant les informations sémantiques qui doivent être refactorisées pour effectuer la transformation. Pour remédier à cette lacune, Taichi adopte une structure hiérarchique qui maintient le flux de contrôle et les informations sémantiques au détriment d'une complexité accrue. Cependant, contrairement à Codon, ces IR sont largement indépendants du front-end de leur langage, ce qui rend le maintien de l'exactitude du type et la génération de nouveau code quelque peu peu pratique, voire impossible. Par conséquent, CIR exploite la hiérarchie simplifiée de ces méthodes, en conservant les nœuds de flux de contrôle du code source et un sous-ensemble complètement réduit de nœuds internes. Surtout, il améliore cette structure grâce à la bidirectionnalité, ce qui rend les nouveaux IR faciles à générer et à manipuler.
Codon utilise la vérification de type statique et compile dans LLVM IR sans utiliser d'informations de type d'exécution, similaire à ce qui était fait auparavant dans le contexte de langages dynamiques tels que Python end. -la vérification de type de bout en bout fonctionne. À cette fin, Codon est livré avec un système de types statiques bidirectionnels, appelé LTS-DI, qui utilise l'inférence de style HindleyMilner pour déduire les types dans le programme sans nécessiter que l'utilisateur annote manuellement les types (cette approche, bien que prise en charge, n'est pas pris en charge en Python Pas courant parmi les développeurs).
En raison de la nature de la syntaxe de Python et des idiomes pythoniques courants, LTS-DI adapte le raisonnement standard de type hm pour prendre en charge les constructions Python notables telles que les compréhensions, les itérateurs, les générateurs, les opérations de fonctions complexes, les paramètres de variables, la vérification de type statique, etc. Pour gérer ces structures et bien d’autres, LTS-DI s’appuie sur :
De nombreuses constructions Python nécessitent également des expressions de compilation (similaires aux expressions pr compressées de C++), qui sont prises en charge par codon. Bien que ces approches ne soient pas rares dans la pratique (par exemple, les modèles C++ utilisent des singletons) et que l'instanciation paresseuse soit déjà utilisée dans le système de types HMF, nous ne connaissons pas leur utilisation combinée dans le contexte de programmes Python à vérification de type. Enfin, veuillez noter que le système de types de Codon dans son implémentation actuelle est complètement statique et n'effectue aucune inférence de type d'exécution. Par conséquent, certaines fonctionnalités de Python, telles que le polymorphisme d'exécution ou la réflexion d'exécution, ne sont pas prises en charge. Dans le contexte du calcul scientifique, la suppression de ces fonctionnalités s’est avérée représenter un compromis raisonnable entre utilité et performances.
De nombreux langages se compilent de manière relativement simple : le code source est analysé dans un arbre de syntaxe abstraite (AST), optimisé et converti en code machine, souvent à l'aide d'un framework tel que LLVM. . Bien que cette approche soit relativement facile à mettre en œuvre, l'AST contient souvent beaucoup plus de types de nœuds que nécessaire pour représenter un programme donné. Cette complexité peut rendre la mise en œuvre de l’optimisation, de la transformation et de l’analyse difficile, voire peu pratique. Une autre approche consiste à convertir l'AST en une représentation intermédiaire (IR) avant d'effectuer la passe d'optimisation. La représentation intermédiaire contient généralement un ensemble simplifié de nœuds avec une sémantique bien définie, ce qui les rend plus propices à la conversion et à l'optimisation.
Codon implémente cette approche dans son IR, qui se situe entre les phases de vérification de type et d'optimisation, comme le montre l'image ci-dessus. La représentation intermédiaire de Codon (CIR) est beaucoup plus simple que l'AST, avec une structure plus simple et moins de types de nœuds. Malgré leur simplicité, les représentations intermédiaires de Codon conservent une grande partie des informations sémantiques du code source et facilitent la « réduction progressive », permettant une optimisation à plusieurs niveaux d'abstraction.
CIR est en partie inspiré de l'IR de LLVM. Dans LLVM, une structure similaire à la forme d'allocation statique unique (SSA) est adoptée, distinguant les valeurs et les variables allouées dans un emplacement, qui sont conceptuellement similaires aux emplacements mémoire. La compilation se déroule d'abord de manière linéaire, où le code source. est analysé dans un arbre de syntaxe abstraite sur lequel une vérification de type est effectuée pour générer des expressions intermédiaires. Cependant, contrairement à d'autres frameworks de compilation, Codon est bidirectionnel et l'optimisation IR peut revenir à l'étape de vérification de type pour générer de nouveaux nœuds qui n'étaient pas dans le programme d'origine. Le framework est « extensible au domaine » et un « plug-in DSL » se compose de modules de bibliothèque, de syntaxe et d'optimisations spécifiques au domaine.
Afin de réaliser le mappage de la structure du code source, une valeur peut être imbriquée dans un arbre arbitrairement grand. Cette structure permet de réduire facilement un CIR à un graphe de flux de contrôle, par exemple. Cependant, contrairement à LLVM, CIR utilisait à l'origine des nœuds explicites appelés flux pour représenter le flux de contrôle, permettant une correspondance structurelle étroite avec le code source. Représenter explicitement la hiérarchie des flux de contrôle est similaire à l'approche adoptée par Taichi. Surtout, cela facilite la mise en œuvre des optimisations et des transformations qui reposent sur des concepts précis de flux de contrôle. Un exemple simple est celui des flux, qui conserve des boucles explicites dans CIR et permet au codon d'identifier facilement des modèles de boucles communs, plutôt que de déchiffrer un labyrinthe de branches comme c'est le cas dans LLVM IR.
CIR ne représente pas explicitement les opérateurs comme "+", mais les convertit en appels de fonction correspondants. Cela permet une surcharge transparente des opérateurs de types arbitraires, avec la même sémantique que celle de Python. Par exemple, l'opérateur + se résout en un appel d'ajout.
Une question naturelle qui découle de cette approche est de savoir comment implémenter les opérateurs pour les types primitifs tels que les ints et les floats. Codon résout ce problème en autorisant LLVM IR en ligne via les annotations de fonction @llvm, ce qui permet à tous les opérateurs primitifs d'être écrits dans le code source du codon. Les informations sur les propriétés des opérateurs telles que la commutativité et l'associativité peuvent être transmises sous forme d'annotations dans IR.
Le pipeline de compilation traditionnel est linéaire dans son flux de données : le code source est analysé en AST, généralement converti en IR, optimisé et finalement converti en code machine. Codon a introduit le concept d'IR bidirectionnel, dans lequel le canal IR peut revenir à l'étape de vérification de type, générant de nouveaux nœuds IR et des nœuds spécialisés qui n'existent pas dans le programme source. Ses avantages incluent :
De même, le canal IR lui-même peut être générique, en utilisant le système de types d'expression de Codon pour fonctionner sur différents types. Les types IR n'ont pas de génériques associés (contrairement aux types AST). Cependant, chaque type CIR comporte une référence au type AST utilisé pour le générer, ainsi qu'à tous les paramètres de type générique AST. Ces types AST associés sont utilisés lors de la réinvocation du vérificateur de type et permettent d'interroger les types CIR pour leurs génériques sous-jacents. Notez que les types CIR correspondent à des abstractions de haut niveau ; les types IR LLVM sont inférieurs et ne correspondent pas directement aux types Codon.
En fait, la capacité d'instancier de nouveaux types lors de la livraison CIR est essentielle pour de nombreuses opérations CIR. Par exemple, créer un tuple (x,y) à partir des valeurs CIR données x et y nécessite d'instancier un nouveau type de tuple tuple[X,Y] (où l'identifiant en majuscule est le type d'expression), ce qui nécessite à son tour d'instancier un nouveau tuple. Opérateurs de tuples pour la vérification de l'égalité et des inégalités, l'itération, le hachage, etc. Cependant, le rappel du vérificateur de type rend le processus transparent.
L'image ci-dessus est un exemple d'une simple fonction Fibonacci vers le mappage du code source CIR. La fonction fib correspond à un CIR BodiedFunc avec un seul argument entier. Le corps contient un flux de contrôle If, qui renvoie une constante ou appelle la fonction de manière récursive pour obtenir le résultat. Notez que les opérateurs comme + sont traduits en appels de fonction (par exemple, add), mais IR est mappé au code source brut dans sa structure, permettant une correspondance et une conversion simples de modèles. Dans ce cas, surchargez simplement le gestionnaire d'appel, vérifiez si la fonction remplit les conditions de remplacement et effectuez l'action si elle correspond. Les utilisateurs peuvent également définir leur propre schéma de parcours et modifier la structure IR à volonté.
CIR fournit une infrastructure complète d'analyse et de transformation : les utilisateurs écrivent des passes à l'aide de diverses classes d'application intégrées au CIR et les enregistrent auprès du gestionnaire de mots de passe, où des canaux plus complexes peuvent exploiter la bidirectionnalité du CIR et réinvoquer le type. vérificateur pour obtenir de nouveaux types, fonctions et méthodes CIR, dont des exemples sont présentés dans la figure ci-dessous.
Dans cet exemple, les appels à la fonction foo sont recherchés et après chaque appel, un appel est inséré qui valide les paramètres de foo et sa sortie. Étant donné que les deux fonctions sont génériques, le vérificateur de type est réinvoqué pour générer trois nouvelles instanciations de validation uniques. L'instanciation de nouveaux types et fonctions nécessite la gestion des spécialisations possibles et l'implémentation de nœuds supplémentaires (par exemple, la méthode de l'opérateur == __eq__ doit être implémentée dans l'exemple pour implémenter la validation), ainsi que la mise en cache de l'implémentation pour une utilisation ultérieure.
Codon utilise LLVM pour générer du code natif. La conversion de Codon IR en LLVM IR est généralement un processus simple. La plupart des types Codon peuvent également être intuitivement convertis en types IR LLVM : int devient i64, float devient double, bool devient int8, et ainsi de suite - ces conversions permettent également l'interopérabilité C/C++. Les types de tuples sont convertis en types struct contenant les types d'éléments appropriés, qui sont transmis par valeur (remarque, les tuples sont immuables en Python) ; cette façon de gérer les tuples permet d'optimiser entièrement LLVM dans la plupart des cas. Les types de référence, tels que les listes, Dict, etc., sont implémentés en tant qu'objets alloués dynamiquement et sont transmis par référence. Ceux-ci suivent les types sémantiques variables de Python et peuvent être mis à niveau vers des types facultatifs si nécessaire pour ne gérer aucune valeur facultatif. un tuple du type i1 de LLVM et du type sous-jacent, le premier indiquant si le type facultatif contient une valeur. Les options sur les types référence sont spécifiquement conçues pour utiliser un pointeur nul pour indiquer les valeurs manquantes.
Les générateurs sont une construction de langage populaire en Python ; en fait, chaque boucle for parcourt un générateur. Il est important de noter que les générateurs de Codon n’entraînent aucune surcharge supplémentaire et se compilent autant que possible en code C standard équivalent. À cette fin, Codon utilise des coroutines LLVM pour implémenter des générateurs.
Codon utilise une petite bibliothèque d'exécution lors de l'exécution du code. En particulier, le garbage collector Boehm est utilisé pour gérer la mémoire allouée. Codon propose deux modes de compilation : débogage et release. Le mode débogage inclut des informations de débogage complètes, permettant au programme d'être débogué à l'aide d'outils tels que GDB et LLDB, ainsi que des informations de traçage complètes, notamment le nom du fichier et le numéro de ligne. Le mode Release effectue davantage d'optimisations (y compris les optimisations -O3 de GCC/Clang) et omet certaines informations de sécurité et de débogage. Par conséquent, les utilisateurs peuvent utiliser le mode débogage pour des cycles de programmation et de débogage rapides et le mode release pour un déploiement hautes performances.
En raison de la flexibilité et de l'IR bidirectionnel du framework, ainsi que de l'expressivité globale de la syntaxe Python, les applications Codon implémentent généralement la plupart des fonctionnalités des composants spécifiques à un domaine dans le code source lui-même. Une approche modulaire peut être présentée sous forme de bibliothèques dynamiques et de fichiers sources Codon. Ce plugin peut être chargé par le compilateur de codons au moment de la compilation.
Certains frameworks, comme MLIR, permettent la personnalisation. Condon IR, en revanche, limite certains types de nœuds et s'appuie sur la bidirectionnalité pour plus de flexibilité. En particulier, CIR permet aux utilisateurs de dériver des types, flux, constantes et directives « personnalisés » qui interagissent avec le reste du framework via des interfaces déclaratives. Par exemple, les nœuds personnalisés proviennent de la classe de base personnalisée appropriée (type personnalisé, flux personnalisé, etc.) et exposent un « constructeur » pour construire l'IR LLVM correspondant. L'implémentation de types et de nœuds personnalisés implique de définir un générateur (par exemple un type de bâtiment) via une méthode virtuelle ; la classe de type personnalisé elle-même définit une méthode getBuilder pour obtenir une instance de ce générateur. Cette construction standardisée de nœuds fonctionne de manière transparente avec les canaux et analyses existants.
De nombreux programmes Python standard fonctionnent déjà immédiatement, ce qui facilite l'optimisation de plusieurs modèles courants dans le code Python, tels que les mises à jour de dictionnaire (qui peuvent être optimisées pour utiliser une seule recherche au lieu de two ), ou des ajouts de chaînes consécutifs (qui peuvent être regroupés en une seule connexion pour réduire la surcharge d'allocation).
Le graphique ci-dessus montre les performances d'exécution de Codon, ainsi que les performances de CPython (v3.10) et PyPy (v7.3), sur des benchmarks, limités à un ensemble de benchmarks "de base", ne dépendant pas de Bibliothèques externes. Comparé à CPython et PyPy, Codon est toujours plus rapide, parfois d'un ordre de grandeur. Même si les benchmarks constituent un bon indicateur de performance, ils ne sont pas sans inconvénients et ne racontent souvent pas tout. Codon permet aux utilisateurs d'écrire du code Python simple pour une variété de domaines tout en offrant des performances élevées sur des applications et des ensembles de données du monde réel.
Étant donné que Codon est construit indépendamment du runtime Python existant, il n'est pas affecté par le verrouillage global de l'interpréteur CPython et peut donc tirer pleinement parti du multi-threading. Pour prendre en charge la programmation parallèle, une extension de Codon permet aux utilisateurs finaux d'utiliser OpenMP.
Pour OpenMP, le corps de la boucle parallèle est présenté comme une nouvelle fonction, qui est ensuite appelée par plusieurs threads par le runtime OpenMP. Par exemple, le corps de la boucle dans la figure ci-dessous serait décrit comme une fonction f qui prend comme paramètres les variables a, b, c et la variable de boucle i.
L'appel à f sera ensuite inséré dans une nouvelle fonction g qui appelle la routine de planification dynamique à tour de rôle d'OpenMP avec une taille de bloc de 10. Enfin, tous les threads de la file d'attente appelleront g via la fonction fork_call d'OpenMP. Le résultat est affiché dans l'extrait de code correct dans l'image ci-dessus, avec une attention particulière étant accordée à la gestion des variables privées ainsi que des variables partagées. La réduction des variables nécessite également une génération de code supplémentaire pour les opérations atomiques (ou l'utilisation de verrous) et une couche supplémentaire d'appels API OpenMP.
La compilation bidirectionnelle de Codon est un élément clé du pass OpenMP. Les « modèles » pour les différentes boucles sont implémentés dans le code source de Codon. Après analyse du code, ces « modèles » sont transmis et spécialisés en remplissant les corps de boucles, les tailles et plannings de blocs, en réécrivant les expressions qui dépendent des variables partagées, etc. Cette conception simplifie grandement la mise en œuvre du pass et ajoute un certain degré de polyvalence.
Contrairement à Clang ou GCC, le canal OpenMP de Codon peut déduire quelles variables sont partagées et lesquelles sont privées, ainsi que tout code minifié en cours. Des réductions personnalisées peuvent être effectuées simplement en fournissant une méthode de magie atomique appropriée (par exemple .aborom_add) sur le type de réduction. Codon parcourt le générateur (le comportement par défaut des boucles Python) jusqu'à une "boucle impérative", c'est-à-dire une boucle de style C avec des valeurs de début, d'arrêt et de pas. Si la balise @par est présente, la boucle forcée sera convertie en boucle parallèle OpenMP. Les boucles parallèles non forcées sont parallélisées en générant une nouvelle tâche OpenMP pour chaque itération de boucle et en plaçant un point de synchronisation après la boucle. Ce schéma permet de paralléliser toutes les boucles for Python.
Les transformations OpenMP sont implémentées sous la forme d'un ensemble de CIR correspondant à des boucles for marquées avec l'attribut @par, et ces boucles sont transformées en constructions OpenMP appropriées dans le CIR. Presque toutes les structures OpenMP sont implémentées en tant que fonctions d'ordre supérieur de Condon lui-même.
CoLa est un DSL basé sur Codon qui cible la compression de données par blocs, qui est au cœur de nombreux algorithmes de compression d'images et de vidéos couramment utilisés aujourd'hui. Ces types de compression reposent fortement sur la division d'une zone de pixels en une série de blocs de plus en plus petits, formant une hiérarchie de données multidimensionnelle où chaque bloc doit connaître sa position par rapport aux autres blocs. Par exemple, la compression vidéo H.264 divise l'image d'entrée en une série de blocs de 16 x 16 pixels, chaque pixel en blocs de 8 x 8 pixels, puis divise ces pixels en blocs de 4 x 4 pixels. Le suivi de la position entre ces parcelles individuelles de pixels nécessite une grande quantité de données d'information, ce qui obscurcit rapidement les algorithmes sous-jacents dans les implémentations existantes.
CoLa introduit l'abstraction du tableau hiérarchique multidimensionnel (HMDA), qui simplifie l'expression et l'utilisation des données hiérarchiques. HMDA représente un tableau multidimensionnel avec une notion de position, qui suit l'origine de tout HMDA donné par rapport à un système de coordonnées global. HMDA peut également suivre leur taille et leur longueur de foulée. Avec ces trois éléments de données, n’importe quel HMDA peut déterminer sa position par rapport à n’importe quel autre HMDA à tout moment du programme. CoLa résume HMDA dans Codon en tant que bibliothèque centrée sur deux nouveaux types de données : les blocs et les vues. Le bloc crée et possède un tableau multidimensionnel sous-jacent, et la vue pointe vers une zone spécifique du bloc. CoLa expose deux hiérarchies principales : les opérations de construction, la copie positionnelle et le partitionnement, qui créent respectivement des blocs et des vues. CoLa prend en charge l'indexation standard à l'aide d'index entiers et de tranches, mais introduit également deux schémas d'indexation uniques qui imitent la façon dont les normes de compression décrivent l'accès aux données. Les index « hors limites » permettent aux utilisateurs d'accéder aux données entourant une vue, tandis que les index « gérés » permettent aux utilisateurs d'indexer un HMDA à l'aide d'un autre HMDA.
Bien que la combinaison de la physique de Codon et des abstractions de CoLa offre aux utilisateurs les avantages d'un langage de haut niveau et d'abstractions spécifiques à la compression, l'abstraction HMDA introduit une surcharge d'exécution importante en raison des opérations d'indexation supplémentaires requises. Pour la compression, de nombreux accès HMDA se produisent au niveau le plus interne du calcul, de sorte que tout calcul supplémentaire en plus de l'accès au tableau d'origine s'avère préjudiciable au moteur d'exécution. CoLa exploite le framework Codon pour implémenter des hiérarchies, réduisant ainsi le nombre de vues intermédiaires créées et les tentatives de propagation pour déduire la position d'un HMDA donné. Cela réduit la taille globale de la hiérarchie et simplifie les calculs d'index réels. Sans ces optimisations, CoLa est en moyenne 48,8×, 6,7× et 20,5× plus lent que le code C de référence pour JPEG et H.264]. Après optimisation, les performances ont été grandement améliorées, avec des temps d'exécution moyens de respectivement 1,06×, 0,67× et 0,91× par rapport au même code de référence.
CoLa est implémenté en tant que plugin Codon et, en tant que tel, est livré avec une bibliothèque de primitives de compression, ainsi qu'un ensemble de canaux CIR et LLVM qui optimisent les routines de création et d'accès. CoLa simplifie également les opérations courantes d'indexation et de réduction à l'aide d'une syntaxe d'accès à la structure de données personnalisée et d'opérateurs fournis par Codon.
Essentiellement, codon est un framework configurable par domaine pour concevoir et mettre en œuvre rapidement le DSL. En appliquant un algorithme de vérification de type spécialisé et un nouvel algorithme IR bidirectionnel, le code dynamique dans divers domaines peut être facilement optimisé. Par rapport à l'utilisation directe de Python, Codon peut égaler les performances C/C++ sans sacrifier la simplicité de haut niveau.
Actuellement, Codon possède plusieurs fonctionnalités Python qui ne sont pas prises en charge, notamment le polymorphisme d'exécution, la réflexion d'exécution et les opérations de type (par exemple, la modification dynamique de la table de méthodes, l'ajout dynamique de membres de classe, les métaclasses et les décorateurs de classe). couverture de la bibliothèque Python standard. Bien que Python compilé par Codon puisse exister en tant que solution restrictive, il convient d'y prêter attention.
【Matériel de référence et lectures associées】
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!