Journal Quelques surprises techniques dans Pythran

Posté par  (site web personnel) . Licence CC By‑SA.
Étiquettes :
31
7
nov.
2019

Demat' iNal,

Il y a peu sortait la version 0.9.4 du compilateur Pythran, délicatement appelée Hollsent. Pour rappel, Pythran est un compilateur pour du code scientifique écrit en Python, canal optimisation de performances.

À cette occasion, en plus de l'habituelle annonce sur la mliste de diffusion, un petit pot-pourri des améliorations techniques a été rédigé par votre serviteur. Et comme c'est quand même plus marrant d'écrire en français, en voici une traduction libre.

C'est surprenant, mais il y a dans cette sortie des choses que je n'aurais jamais cru voir dans Pythran. Et surtout, c'est venu tout naturellement. Si si.

Support (partiel) de isinstance(...)

Cet innocent bout de code est complètement valide en Python :

def abssqr(x):
    if isinstance(x, complex):
        return x.real ** 2 + x.imag ** 2
    else:
        return abs(x) ** 2

Mais il a longtemps été invalide en Pythran, à cause de la garde sur isinstance(...). Si on instanciait cette fonction sur un float, on se payait une erreur comme quoi il n'y a pas de méthode real sur un nombre flottant.

Ce genre de test que l'on pourrait résoudre lors de la compilation n'est pas sans rappeler le if constexpr introduit en C++17. Et la construction syntaxique est assez proche d'un if x is None:. Construction supportée par Pythran ! Après quelques détails techniques que je vous passe (mais dont vous pouvez avoir un avant goût avec pythran -P moncode.py), liés au fait que Pythran utilise C++11 comme backend, on s'en sort très bien.

Pour la blagounette, le code sus-cîté est de toute façon optimisé par Pythran sous la forme abs(x) ** 2 qui est un motif connu et remplacé par un appel à un intrinsèque. Mais bon, c'est pour l'exemple.

Support (incomplet) de type(...)

Encore une histoire de (sale) type. On sent que ce n'est pas ma tasse de thé… Et pourtant l'implémentation dans pythonic est d'une simplissime simplicité :

template <class T>
typename type_functor<T>::type type(T const &)
{
    return {};
}

type_functor fait juste une association entre un type et un foncteur permettant de générer des objets de ce type, du genre

template <class T>
struct type_functor<types::list<T>> {
    using type = functor::list;
};

Ce qui est fascinant, c'est que functor::list existait déjà dans Pythran, pour représenter le builtin list(...). Il se passe des choses…

Grâce à type(...) on peut écrire de jolis codes polymorphes, avec des fonctions d'ordre supérieur, comme :

def poly(x, l):
    return type(x)(l) + x

Et ça passe crème dans Pythran, youhou.

clang-cl.exe

Et oui, un .exe. C'est fou ! En général, les extensions natives sous Windows sont compilées par le Microsoft Visual Studio Compiler. C'est même une obligation codée en dur dans distutils (contrairement à Linux où l'on peut paramétrer assez facilement le compilateur utilisé).
Et un des problèmes avec ce compilateur, c'est qu'il peine terriblement à compilé du C++11 un peu poilu, là où clang-3.5 et gcc-5 s'en sortent très bien… Et comme j'ai des utilisateurs Windows… Et bien en tirant profit de clang-cl.exe, qui fournit une interface compatible avec cl.exe, on peut enfin compiler du C++ moderne sous MS.

Il y a bien eu besoin d'une petite modification de singe sur distutils, mais une petite crasse est peu de chose à ce niveau… Et bien que l'approche ne soit pas officiellement compatible, ça marche chez moi enfin, sur AppVeyor.

Support de Python 3.8

La représentation interne de Pythran est proche de celle de l'ast Python, mais elle se doit d'être capable de représenter les différentes versions de dernier : py2, py35, py36, py37, py38. Un travail grandement facilité par l'existence du paquet gast dont je remercie chaleureusement l'auteur !

Comme prévu, cette bafouille est plus longue que la version anglaise, et beaucoup plus agréable pour moi à rédiger, en espérant qu'elle ait été agréable à lire !

  • # Types optionnels

    Posté par  . Évalué à 1.

    Chez mypy, il y a un sous-projet pour utiliser les types optionnels afin de réaliser des compilations optimisées de code python. Est-ce que cette approche est envisagée dans Pythran ?

    • [^] # Re: Types optionnels

      Posté par  (site web personnel) . Évalué à 2.

      mmmh, je ne suis pas sur de comprendre, ou disons que je peux comprendre plusieurs choses, tu peux détailler ?

      • [^] # Re: Types optionnels

        Posté par  . Évalué à 1.

        Le projet est là, même si il ne décrit pas vers où ils veulent aller : https://github.com/python/mypy/tree/master/mypyc .

        De ce que j'en avais compris, l'idée c'est de pouvoir faire "quelques optimisations supplémentaires" grâce aux annotations:

        def func(a: int) -> int: return a+1
        Comme on est sure que c'est un int, on va pouvoir compiler ça un peu mieux (même si c'est pas forcement le meilleur exemple, un int en python et un int en C, c'est loin d'être pareil, il faudrait faire du formel (ou définir des subtypes) pour pouvoir être sure qu'on peut le réduire en un "int" de C).

        Je ne sais pas si ça explique mieux…

        • [^] # Re: Types optionnels

          Posté par  (site web personnel) . Évalué à 2.

          Oui, je me demandais si tu parlais de types optionnels au sens de Option Type ou au sens de types que l'on peut ajouter de façon optionnelle. Cela semble être le second point.

          Pythran pourrait utiliser ces types, mais comme ils sont optionnels, il doit surtout savoir faire sans :-) Et dans ce cas, autant faire sans !

          L'intérêt serait serait pourtant de bénéficier de l'inférence / vérification de type par mypy, idéalement en en faisant une dépendance de Pythran…

          • [^] # Re: Types optionnels

            Posté par  . Évalué à 3.

            L'intérêt serait serait pourtant de bénéficier de l'inférence / vérification de type par mypy, idéalement en en faisant une dépendance de Pythran…

            Mais quitte à faire du typage statique autant utiliser un langage qui a un typage statique ^^

            https://linuxfr.org/users/barmic/journaux/y-en-a-marre-de-ce-gros-troll

  • # Question bête ?

    Posté par  (site web personnel) . Évalué à 3.

    Est-ce que pour des raisons de performance tu peux modifier le layout mémoire utilisé en C++ ?

    C'est souvent une des raisons que l'utilisation automatique de SIMD ne marche pas, c'est que le layout choisi par l'utilisateur n'est "pas gentil". Vu que tout est généré, est-ce que tu y touches ou est-ce que tu peux y toucher ? (l'optimisation de base étant de trier de façon décroissante les champs d'une structure)

    "La première sécurité est la liberté"

    • [^] # Re: Question bête ?

      Posté par  (site web personnel) . Évalué à 4.

      Est-ce que pour des raisons de performance tu peux modifier le layout mémoire utilisé en C++ ?
      On peut mais on ne le fait pas. Pour le moment on garde le layout C et on ne vectorise que les opérations point à point. Je ne suis pas encore allé gratter de ce côté.

      l'optimisation de base étant de trier de façon décroissante les champs d'une structure

      Et le AoS ou SoA, non ? Comme on n'a pas de types custom, le cas le plus proche serait les tableaux de complexes. Mais on ne fait pas actuellement. On pourrait par contre faire une passe qui décide du bon layout et intégrer ça à la génération de code, oui.

      Tu as un peu de temps de dev libre ? Tes remarques sont souvent ultra pertinentes ;-)

      • [^] # Re: Question bête ?

        Posté par  (site web personnel) . Évalué à 3.

        Disons que l'optimisation, c'est mon dada (je suis trop vieux…). Après les archis de cpu avec le f-cpu, le langage ultrarapide, lisaac qui est mort-né, ton projet est tout de même pas mal intéressant :) Lisaac partait du principe que plus le langage est de haut niveau et chargé de sémantique et plus l'optimisation est facile.

        Et le AoS ou SoA, non ?

        Oui, mais c'est compliqué, car la bonne réponse peut changer en fonction des caches, des tailles de champs, des patterns d'accès, etc. Le trie de structure, cela marche toujours car cela réduit le padding, donc l'empreinte mémoire, donc l'usage du cache, etc….

        Tu as un peu de temps de dev libre ? Tes remarques sont souvent ultra pertinentes ;-)

        Pourquoi pas.

        "La première sécurité est la liberté"

        • [^] # Re: Question bête ?

          Posté par  (site web personnel) . Évalué à 2.

          Je pense avoir vu une présentation de Lisaac aux journées de la compilation françaises, à Aussois. Probablement donnée par toi ?

          Les fondements théoriques de Pythran sont assez flous et mouvants, mais il reste des choses amusantes à faire, soit au niveau du typage si c'est un truc qui te branche, ou également (mais c'est un gros chantier) sur l'ownership.

          Plus humblement, certaines fonctions de pythonic gagneraient à avoir une version vectorielle, ça peut être un bon sujet pour commencer !

          • [^] # Re: Question bête ?

            Posté par  (site web personnel) . Évalué à 3.

            Je pense avoir vu une présentation de Lisaac aux journées de la compilation françaises, à Aussois. Probablement donnée par toi ?

            Non, c'est sans doute Benoit Sonntag.

            Les fondements théoriques de Pythran sont assez flous et mouvants, mais il reste des choses amusantes à faire, soit au niveau du typage si c'est un truc qui te branche, ou également (mais c'est un gros chantier) sur l'ownership.

            L'ownership, c'est le suivi du cycle de vie de la mémoire ?

            Plus humblement, certaines fonctions de pythonic gagneraient à avoir une version vectorielle, ça peut être un bon sujet pour commencer !

            Il s'agit de fonction de base à rendre vectorielle en C++ ?

            "La première sécurité est la liberté"

            • [^] # Re: Question bête ?

              Posté par  (site web personnel) . Évalué à 2.

              L'ownership, c'est le suivi du cycle de vie de la mémoire ?

              Dans mon jargon, oui :-) Pour le moment tous les types non scalaires passent par un shared_ptr, on doit pouvoir supprimer ça dans pas mal de cas !

              Il s'agit de fonction de base à rendre vectorielle en C++ ?

              oui

              • [^] # Re: Question bête ?

                Posté par  (site web personnel) . Évalué à 3.

                Dans mon jargon, oui :-) Pour le moment tous les types non scalaires passent par un shared_ptr, on doit pouvoir supprimer ça dans pas mal de cas !

                Lisaac avait fait plus simple : un mot clef, Expanded. Ainsi, Ben a pu faire un OS où les pixels de l'écran sont des objets.

                "La première sécurité est la liberté"

          • [^] # Re: Question bête ?

            Posté par  (site web personnel) . Évalué à 3.

            Un autre point concernant l'AST, en général, il y a plus d'une centaine d'éléments différent. L'optimisation consiste à reconnaître des patterns (visiteur ?) puis remplacer le bout d'arbre par un autre, et même de le "faire en boucle" (Lisaac tournait jusqu'à obtenir un point bas).

            Lisaac, avec son AST à ~15 classes, avait montré un truc hyper sympa : bien plus d'optimisations devenaient possibles. Vu qu'il n'y a qu'un seul moyen de faire une boucle, toutes les optimisations de boucle s'appliquaient tout le temps.

            Je me demandais donc si c'était pensable de passer d'un AST pléthorique, à un truc plus simple pour rendre les optimisations plus simples à écrire.

            "La première sécurité est la liberté"

            • [^] # Re: Question bête ?

              Posté par  (site web personnel) . Évalué à 2.

              On a fait ça en partie : suppression de l'assignation de tuple, des lambda, des fonctions imbriquées… on pourrait effectivement aller encore plus loin dans ce sens, je partage ton point de vue sur les effets bénéfiques de l'approche. Quand à la recherche d'un « point bas » on fait ça aussi, avec une limite sur le nombre d'itérations quand même !

              • [^] # Re: Question bête ?

                Posté par  (site web personnel) . Évalué à 3.

                Cela un sens d'avoir un AST de haut niveau pour faire des optims de haut niveau et ensuite de générer le C++ ?

                J'avais regardé un jour l'AST de llvm, c'est très très bas niveau. Le truc marrant, c'est qu'ensuite un tel AST serait une cible de choix pour tout nouveau langage qui bénéficierait ainsi de tes optims + celle de C++.

                "La première sécurité est la liberté"

                • [^] # Re: Question bête ?

                  Posté par  (site web personnel) . Évalué à 3.

                  Cela un sens d'avoir un AST de haut niveau pour faire des optims de haut niveau et ensuite de générer le C++ ?

                  C'est ce qu'on fait et oui ça a un sens : on fait les optims qui ont du sens au niveau Python, et qui seraient infaisable à un plus bas niveau (ou du moins plus dures à prouver, soyons modestes). Et on délègue le reste au compilo, qui fait lui aussi des optims à son niveau (p.e. la copy elision etc) et ensuite le compilateur, disons clang, génère de l'IR LLVM et encore d'autres optimisations arrivent. N'est-ce pas fantastique ? ;-)

                  pour l'IR LLVM, elle est de très bas niveau par rapport à ce qu'on veut faire, et surtout il n'y a pas de types paramétrés, alors que ceux ci sont au coeur de Pythran (pythran travaille sur des fonctions python polymorphes, génères des fonction c++ polymorphiques (template) et c'est à la toute fin que les annotations sont prises en compte. Ainsi on peut traduire du code python en C++, et l'embarquer dans une appli native.

  • # attributs real et imag

    Posté par  . Évalué à 1.

    À propos du premier exemple, les int et les float on des attributs real et imag, au moins depuis Python 3.4. Donc la garde ici est inutile.

Suivre le flux des commentaires

Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.