• # Simples precisions

    Posté par (site web personnel) . En réponse à la dépêche Le projet Unladen Swallow vise à accélérer Python d'un facteur 5. Évalué à 10.

    Je vais essayer de préciser un peu ce que l'auteur dis, arrêtez moi des que je dis une bêtise. Mes excuses par avance pour l'orthographe...

    Le gros problème des langages interprètes, tel que Python, réside dans la lecture du bytecode et l'affectation des instruction bytecode a une opération spécifique.

    Pour comprendre, il faut revenir au base du fonctionnement d'un programme sur un CPU.

    Un programme est une suite d'instructions, plus ou moins simples, que peut comprendre un être humain. Le programme est ensuite compilé en programme machine par un compilateur.

    Un programme machine n'est qu'une suite d'instructions très simples que peut comprendre la machine. Ces instructions sont chargées en mémoire et le CPU les exécute de façon séquentielles.

    Le chargement des instructions s'appelle le *fetch*. Le CPU conserve un pointeur qui lui indique la zone en mémoire qui contient l'instruction à effectuer. Cette instruction est donc chargée dans le CPU, puis vas ensuite être analysée pour savoir comment paramétrer le CPU pour effectuer le calcul. Cette opération s'appelle le *dispatch*. Ensuite le CPU effectue le calcul, et un nouveau cycle de fetch/dispatch peut commencer.

    Il s'agit de quelques chose de TRÈS simple et qui est effectué par le matériel par des câblages spécifiquement dédiés à ce traitement. C'est donc TRÈS rapide.

    Ce mode de fonctionnement pose toutefois un problème. Plus le langage initial (avant compilation) est complexe, plus le code à générer est complexe et plus le travail du compilateur est complexe, voir impossible (quoi que... ?). À cela s'ajoute le fait que tous CPU possède des jeux d'instructions différentes.

    C'est pourquoi les langages interprétés, comme python, utilisent ce que l'on appelle une machine virtuelle.

    Le code python est traduit en instructions pour cette machine virtuelle. Ces instructions sont de beaucoup plus haut niveau que celles que peut comprendre un CPU standard et chaque instruction est lié a un code plus complexe. Par example, la ou un CPU standard ne possède qu'une instruction simple ADD qui ajoute deux nombres, on peut imaginer une instruction de machine virtuelle qui s'appellerait FAIT_LE_CAFÉ associée à un morceaux de code bien plus complexe.

    Deux problèmes se posent ici pour les performances :

    1) ce morceaux de code plus complexe, si il s'agit d'une instruction simple de la machine virtuelle, n'est plus simple pour le CPU qui se trouve en dessous et donc nécessite plus de calcul. Au final ce n'est pas si grave, car cela fait plus de choses.

    2) Le traitement de fetch/dispatch que doit faire la machine virtuelle pour charger les informations se fait de manière logicielle et est donc forcement beaucoup plus lent. Malheureusement ici cela pose problème car pour un traitement équivalent à ce qu'un CPU sait faire, le traitement se retrouve ENORMEMENT plus lent.

    Maintenant que les fondations sont posées, quel est le but de se projet. La *boucle d'évaluation principale* citée dans cette dépêche n'est autre qu'un bout de code (C pour python) qui se contente de faire:


    while(true)
    {
    instruction = fetch_instruction()


    if(instruction == INSTRUCTION_AJOUT)
    faire_instruction_ajout()
    elseif(....)
    faire_autre_instruction()

    }


    Cette boucle est lourde, impose des traitement complexes, impose des empilements de fonctions et des branchements qui sont difficilement optimisables, c'est donc une perte de temps impressionnante.

    Sauf qu'en pratique, au moment de l'exécution du code python, le bytecode ne change plus. Il serait donc possible, à ce moment precit, de transformer la boucle principale en un morceaux de code machine spécifiquement prévu pour traiter ce morceaux de bytecode particulier. C'est le but de la "compilation Just In Time".

    C'est une première étape pour l'optimisation JIT.

    Une deuxième, qui avait déjà été réalisée avec Psyco concerne les optimisations à la volée en fonction du contexte.

    Exemple pratique supposons une fonction python telle que celle-ci

    def max(a,b):
    '''
    Retourne la plus grande valeur entre a et b, ou None si les deux sont egaux
    ''''
    if a > b:
    return a
    else if a < b:
    return b
    else:
    return None

    Python ne forçant pas la déclaration de type, il n'y a aucun moyen de savoir à l'avance ce que sont a et b. Pour chaque test il faut donc aller vérifier que les objets a et b possèdent des méthodes de comparaison, si oui les appeler. Au sein de la méthode de comparaison, il faut regarder le type de a et b et si ce sont des entiers alors l'on peut appeler la méthode de comparaison native du CPU.
    Cela nécessite plusieurs instructions de bytecode, plusieurs cycle fetch/dispatch, bref c'est lent, un peu moins si il existe un JIT sur fetch/dispatch, mais cela reste super lent.

    Il reste possible de crée un JIT qui analyse le code lors de l'appel de la fonction et se rend compte qu'il peut le remplacer par des instructions native du CPU beaucoup plus simples (un CPU sait très bien faire une comparaison entre deux entiers)

    Voila, j'espère que c'est plus clair.