• [^] # Re: Aucune légitimité

    Posté par (site web personnel, Mastodon) . En réponse au lien B. Kernighan (co-créateur d'Unix) sur Rust : « Je ne pense pas qu'il va remplacer C tout de suite ». Évalué à 10.

    Question de béotien : comment arrives-tu à ça ? C'est intéressant comme point de vue, je suis curieux !

    En regardant ce qui se fait ailleurs, par exemple, les LISP Machines qui ont (ou avaient) un jeu d'instruction plutôt dédié à manipuler très rapidement des listes chaînées; les données dans la mémoire sont "taggées" par exemple pour indiquer si une adresse contient une valeur entière, une référence vers un autre objet, ou rien du tout, ce qui est très utile au garbage collector pour nettoyer la mémoire.

    Ou plein d'autres architectures historiques comme le z80, sur lequel écrire un compilateur C est possible, mais assez pénible, parce que les registres et instructions sont spécialisés pour faire d'autres choses. Par exemple, il n'y a pas vraiment de quoi implémenter un "frame pointer". Sur un processeur 68000, SPARc ou x86, ça se fait en une instruction (link/unlink, save/restore, et un mov bp, sp avec un offset sur le dernier). Ils ont tous les 3 également des modes d'adressages indexés, on peut donc facilement avoir sur la pile un pointeur vers une structure, et en une ou deux instructions, on va:

    • Récupérer ce pointeur dans la mémoire (adressage par bp + offset)
    • Ajouter un offset à ce pointeur (soit un offset constant pour accéder à un champ d'une structure, soit un nombre d'éléments multiplié par la taille d'un élément pour accéder à un tableau
    • lire ou écrire quelque chose à cette addresse

    C'est un exemple de ce que je disais par "nombreux niveaux d'indirection de pointeurs". Bien sur on écrit pas int**** truc; dans un programme, mais on écrit facilement ce genre de code:

    struct machin* truc;
    truc->champ1[index].chose = 3;
    

    Si on compte on a ici:

    • Premier niveau: le pointeur de pile
    • Deuxième niveau: le pointeur truc avec l'offset champ1
    • Troisième niveau: l'index dans le tableau champ1
    • Quatrième niveau, l'index de chose dans l'élément de champ1

    Le code passe plus de temps à manipuler des adresses que des données (comme donnée dans cet exemple il y a la constante 3). Si on regarde un processeur comme le 68000, on voit que la moitié des registres sont dédiés aux calculs d'adresses en conséquence. C'est encore plus flagrant sur son petit frère 8 bit le 6809: pour gérer les adresses on a 4 registres 16 bit, alors que pour les données on en a un seul (découpable en 2 moitiés de 8 bits). Et les calculs sur les adresses sont plus rapides car ils disposent d'une unité arithmétique 16 bit dédiée (alors que les calculs sur les données sont fait en 8 bit et donc en 2 fois).

    Il y a d'autres processeurs où la pile est limitée à 256 éléments au total. Sur le z80, on peut simuler un registre BP avec l'un des deux registres d'index, mais on ne peut pas adresser plus de 256 octets de mémoire et les index sont forcément constants. Pour tout le reste il faut faire des acrobaties qui consomment une quinzaine d'instructions.

    On a des informations historiques des concepteurs de ces langages, aussi. Pour le x86, ce n'est pas exactement le C qui était visé, mais plutôt le Pascal. Mais le modèle d'exécution est assez similaire: fonctions avec passage de paramètres sur la pile, structures, tableaux. Pas de programmation objet, par exemple (ce qui se concentrerait plutôt sur des sauts indirects via des vtables). ça n'a pas vraiment été étudié pour les coroutines non plus.

    Pour le SPARC, les choix de jeu d'instruction ont été faits en simulant l'exécution de code existant. Comme c'était en plein dans l'époque de UNIX pour Sun, ça a forcément été fait avec du C.

    De mémoire, il y avait eu un essai avec des processeurs Java, et c'était pas la folie. Ça ne veut pas dire que la prochaine fois ce sera aussi un échec, mais l'idée a été testée.
    D'ailleurs, dans les processeurs Apple (M1 etc.), il me semble qu'il y a des circuits dédiés à JavaScript, pour accélérer certains comportements.

    Pour le Java, je suppose que tu parles des processeurs ARM Jazelle. Qui ont été un échec pour deux raisons: d'une part parce que ce mode d'exécution est incomplètement documenté par ARM, et d'autre part parce qu'il ne correspond pas du tout au fonctionnement interne des machines virtuelles Java de l'époque (qui font beaucoup de transformations sur le bytecode pour l'opptimiser avant de l'exécuter).

    Pour le Javascript, effectivement il existe plusieurs opérations dédiées pour des comportements spécifiques sur certaines opérations mathématiques (ce n'esst pas spécifique à Apple, on trouve ça dans plusieurs processeurs ARM et c'est dans le jeu d'instruction officiel).

    Dans les choses qui n'ont pas marché on peut aussi citer l'Intel iAPX 432 dont le but était de faire un processeur orienté objet.

    Et un autre qui rejoint un peu ce que j'essaie de dire, c'est l'architecture Itanium de Intel. Le but était justement de supprimer des couches de complexité dans les processeurs (réordonnancement des instructions, renommage de registres, exécution spéculative) pour remettre tout ça sous la responsabilité du compilateur. En théorie, ce n'est pas une mauvaise idée pour avoir un langage vraiment "proche du matériel". En pratique, la densité du code est plus basse (et donc il s'exécute moins vite, parce que la mémoire est beaucoup plus lente que le processeur), et surtout, le code compilé est lié à une implémentation spécifique du CPU. Si une nouvelle génération de CPU devient disponible, avec, par exemple, des unités de traitement ou des registres supplémentaires, il faut tout recompiler pour en tirer parti. C'est l'erreur qu'ont fait les générations précédentes de RISC avec le "branch delay slot" par exemple (exécuter une instruction supplémentaire après un jump, avant d'aller à la nouvelle addresse: ça simplifiait beaucoup le design initial avec un pipeline à 4 étages seulement, mais c'est resté ensuite dans le jeu d'instruction alors que ça n'avait plus aucun intérêt).

    On semble donc se diriger, aussi bien dans le logiciel que dans le matériel, vers une solution intermédiaire: un code assembleur qui fournit une représentation compacte de ce qu'il faut exécuter, mais tout en étant facile à décoder et optimiser par des phases de pré-traitement avant d'être exécutée. C'est le principe des machines virtuelles (javascript, java, wasm), mais c'est aussi ce qu'il se passe à l'intérieur des processeurs modernes (pré-décodage du jeu d'instruction x86 ou ARM vers des instructions internes microcodées; jeu d'instruction Thumb sur ARM pour gagner en densité et en bande passante mémoire). Une exception notable est le RISC-V, qui part dans la direction opposée avec un jeu d'instruction très minimaliste et donc peu dense, avec dans l'idée que cela pourrait permettre à ce pré-traitement d'être plus complexe et intrusif (c'est un peu plus facile de manipuler ce flot d'instruction si chaque instruction fait très peu de choses), mais par contre avec une densité faible qui risque de poser problème si les mémoires ne font pas de gros progrès en vitesse d'accès.

    Au final, il y a en fait un genre de co-évolution: on exécute beaucoup de code basé sur le modèle d'exécution du C, donc on achète (et on fabrique) les processeurs qui sont efficace pour ça, et les autres architectures sont oubliées depuis longtemps. Les compilateurs s'adaptent également à ces processeurs courants. Et si quelqu'un vient proposer un langage avec des idées radicalement différentes, il vient se heurter à des processeurs pas forcément adaptés et donc à une pénalité de performances.