2 points par GN⁺ 2024-08-05 | 1 commentaires | Partager sur WhatsApp
  • La propriété essentielle de temps constant (constant-time) dans le code cryptographique peut être rompue par les seules optimisations du compilateur ; une expérience consiste donc à ajouter un patch d’avertissement dans LLVM pour repérer les motifs à risque
  • Les « optimisations » du compilateur peuvent accélérer certains benchmarks, mais les chemins critiques réels s’appuient souvent sur des intrinsics et de l’assembleur, tandis que le coût des bugs introduits par l’optimisation s’accumule séparément
  • En juin 2024, Antoon Purnal a confirmé que le code de référence Kyber pouvait, avec certaines options d’optimisation de Clang 15 et ultérieur, être transformé en branchements conditionnels dépendant de valeurs secrètes, ouvrant potentiellement la voie à des attaques temporelles
  • TIMECOP 2 vérifie, dans SUPERCOP, les résultats de compilation déclarés en temps constant, mais il est limité par les instructions prises en charge par Valgrind et par les flux de données effectivement observés pendant les tests
  • Les réponses pratiques consistent à empêcher le compilateur de voir un résultat sur 1 bit comme un bool, via des fonctions comme crypto_{int,uint}{8,16,32,64}.h, ou à migrer vers de l’assembleur vérifié, des langages orientés sécurité ou des compilateurs dédiés

Le vide de responsabilité créé par les « optimisations » du compilateur

  • Les journaux de changements récents de LLVM et GCC mentionnent sans cesse des « optimisations », des tests d’« optimisation », des corrections de tests et des corrections de bugs d’« optimisation »
  • Quand du code qui fonctionnait correctement avant compilation change de comportement après une modification du compilateur, la responsabilité est souvent renvoyée au programmeur qui aurait déclenché un « undefined behavior »
  • Ces « language standards » sont rédigés par les auteurs de compilateurs ; en pratique, cela crée une structure où le code de millions de programmeurs porte plus de responsabilité que les changements d’un petit groupe d’auteurs de compilateurs
  • Dans le code cryptographique, par exemple, sur plusieurs benchmarks CPU, l’implémentation avx2 de kyber768 est environ 4 fois plus rapide que du code portable compilé avec un compilateur « optimisant »

Les limites de la mesure des performances d’optimisation

  • En 2000, Todd A. Proebsting a formulé dans Proebsting's Law que « les progrès des compilateurs doublent la puissance de calcul tous les 18 ans », concluant que l’apport des optimisations de compilateurs est marginal
  • En 2022, Arseny Kapoulkine a résumé dans un benchmark que LLVM 11 prend deux fois plus de temps que LLVM 2.7 pour compiler avec optimisation, et que le code exécuté est généralement 10 à 20 % plus rapide
  • Les deux discussions passent à côté de la performance réellement perçue par les utilisateurs
    • Les hotspots, où se concentre la performance, contiennent beaucoup d’intrinsics et d’assembleur
    • FFmpeg contient 160 000 lignes d’assembleur si l’on compte les fichiers .asm et .S
    • Plus les ordinateurs et les réseaux traitent de données, plus le temps CPU réel se concentre sur ces hotspots
  • Les coûts de sécurité augmentent eux aussi séparément dans les discussions sur l’optimisation
    • Deloitte a indiqué qu’en 2023, les budgets de sécurité IT représentaient 0,5 % du chiffre d’affaires des entreprises
    • Mis en regard du chiffre selon lequel le chiffre d’affaires total des entreprises dans le monde dépassait 48 000 milliards de dollars en 2022, l’ordre de grandeur global pourrait atteindre des centaines de milliards de dollars
    • Il faut toutefois noter que les 0,5 % de Deloitte pourraient être une simple moyenne par entreprise, et que toutes les entreprises n’ont pas répondu à l’enquête

Fuites temporelles et cas Kyber

  • Les problèmes de sécurité créés par les compilateurs « optimisants » ne se limitent pas aux bugs traditionnels : ils incluent aussi des fuites temporelles, où des informations secrètes transparaissent dans le temps d’exécution
  • L’article EuroS&P 2018 de Laurent Simon, David Chisnall et Ross Anderson avertit qu’une mise à niveau du compilateur peut ouvrir sans prévenir un canal temporel dans du code auparavant sûr
  • L’exemple mis en avant dans l’article de 2018 portait sur du code qui choisissait entre deux valeurs avec un bool, ce bool poussant le compilateur à générer un saut conditionnel
    • Dans les implémentations cryptographiques, la pratique consiste à éviter cela en supprimant bool du code critique et en créant des fonctions de comparaison en temps constant séparées
    • OpenSSL est cité comme déclarant 37 fonctions à cette fin
  • Le cas de 2015 concernant curve25519-donna et MSVC 2015 est présenté dans l’article comme un malentendu
    • En réalité, lors de la compilation pour x86 32 bits, les opérations int64 étaient transformées en appels à llmul.asm, la bibliothèque int64 32 bits de Microsoft
    • La fuite temporelle provenait de branchements dépendant des données dans llmul.asm, et cette bibliothèque devrait elle aussi être incluse dans une conception raisonnable du code source
  • En juin 2024, Antoon Purnal a confirmé que le code de référence Kyber pouvait permettre des attaques temporelles avec certaines options d’optimisation de Clang 15 et ultérieur
    • La forme problématique était (-((x>>j)&1))&y, un calcul qui produit y si le bit j de x est défini, et 0 sinon
    • Clang transforme ce bit en bool via une instruction de test de bit, puis génère un branchement conditionnel fondé sur ce bool
    • Dans LLVM, cette « optimisation » est gérée par combineShiftAnd1ToBitTest dans lib/CodeGen/SelectionDAG/DAGCombiner.cpp
    • Cette fonction a été ajoutée par Sanjay Patel en septembre 2019, puis modifiée par plusieurs personnes
  • GCC présente aussi des cas similaires de franchissement de limite
    • Un patch GCC d’ARM, datant de novembre 2021, transforme (-x)>>31 en -(x>0)
    • Un avertissement à ce sujet est apparu en avril 2024

TIMECOP et vérification du temps constant

  • TIMECOP 2 est intégré au framework de tests cryptographiques SUPERCOP et vérifie automatiquement les branchements conditionnels dérivés de valeurs secrètes dans le code compilé déclaré en temps constant
  • La vérification ne couvre pas seulement les branchements conditionnels, mais aussi les index de tableaux dérivés de valeurs secrètes
    • L’article KyberSlash décrit aussi un patch vérifiant les divisions dérivées de valeurs secrètes
  • TIMECOP 1 était un outil créé par Moritz Neikes en modifiant SUPERCOP, automatisant l’approche ctgrind d’Adam Langley
  • TIMECOP 2 étend l’approche existante sur plusieurs points
    • Il marque automatiquement la sortie du RNG comme valeur secrète
    • Il prend en charge la « declassification »
    • Il prend en charge la déclaration d’« entrées publiques »
    • Il s’exécute sur plusieurs cœurs
  • TIMECOP a des limites claires
    • Il ne peut traiter que les instructions prises en charge par Valgrind, et s’arrête donc par exemple sur les instructions AMD XOP
    • Il ne vérifie que les flux de données observés pendant l’exécution effective des tests
  • Les travaux sur les outils de vérification du comportement en temps constant se poursuivent ; une liste d’outils liés est disponible sur ct-tools
  • Une vérification équivalente à TIMECOP a été intégrée à la suite de tests de libmceliece et pourrait se diffuser à d’autres bibliothèques

Méthodes de réécriture en temps constant

  • Après avoir trouvé des fragments de code à temps variable, il faut une méthode pour les réécrire en temps constant sans introduire de bugs
  • Une présentation de juillet 2024 a montré certaines fonctions en temps constant fournies par libmceliece et SUPERCOP
    • Les fichiers s’appellent crypto_{int,uint}{8,16,32,64}.h
    • Ces fichiers peuvent être copiés dans d’autres projets
  • La fonction d’exemple crypto_uint32_bitmod_mask(x,j) a le même effet que -((x>>(j&31))&1), mais empêche le compilateur de voir le résultat sur 1 bit
  • Un exemple plus complexe est crypto_uint32_max(x,y)
  • L’article de 2018 traite d’un tweak ajoutant à Clang/LLVM une fonction en temps constant __builtin_ct_choose(bool cond, x, y)
    • L’article suggère à tort que cette seule fonction suffirait
    • Cette fonction pourrait un jour entrer dans le compilateur, mais il pourrait falloir longtemps avant que les projets puissent en dépendre
    • Son mode d’implémentation est jugé plus fragile que crypto_{int,uint}{8,16,32,64}.h

Éviter le problème en amont

  • Si les tests avant distribution d’une bibliothèque compilée détectent une fuite temporelle introduite par le compilateur, la distribution peut utiliser l’ancienne version du compilateur pendant la réécriture du code
    • Cette approche est une réponse temporaire qui continue de protéger les utilisateurs
  • Une solution consiste à distribuer la bibliothèque en assembleur
    • La présentation RWC 2024 Adoption of high-assurance and highly performant cryptographic algorithms at AWS présente un logiciel X25519 rapide dont il est prouvé qu’il calcule correctement X25519 pour toutes les entrées
    • L’implémentation est écrite en assembleur, en deux versions pour CPU Intel/AMD 64 bits et deux versions pour CPU ARM 64 bits
    • La proposition de correction porte sur le code machine réellement exécuté par l’utilisateur, et la preuve est vérifiée par le prouveur de théorèmes HOL Light
  • Cependant, pour les logiciels cryptographiques qui n’atteignent pas ce niveau, la difficulté d’auditer l’assembleur demeure
  • Pour le code écrit en C, C++ et autres langages similaires, des méthodes permettant d’ajouter rapidement un « vaccin » contre les fuites temporelles sont également explorées

Expérience de patch clang-vs-clang

  • x&1 et x>>31 ont en commun de ne produire que deux résultats possibles
    • x&1 vaut 0 ou 1
    • Pour un uint32, x>>31 vaut 0 ou 1
    • Pour un int32, x>>31 vaut 0 ou -1
  • Ces formes sont faciles à envoyer vers un bool pour les auteurs d’« optimisations » de compilateurs
  • Il est recommandé de toujours compiler avec -fwrapv afin que GCC et Clang supposent une arithmétique en complément à deux
  • Un simple scan des sources à la recherche de &1, 1&, >>31, etc. remonte déjà de nombreux exemples, mais ici le scan est effectué autrement, en patchant directement l’« optimizer » de LLVM
  • Le patch part du commit LLVM 68df06a0b2998765cb0a41353fcf0919bbf57ddb, cherche &1 et >>31, puis émet l’avertissement suivant
    • please take this away before clang does something bad
  • Un exemple de commande de compilation est clang -Rpass-analysis=clang-vs-clang -O -c x.c
  • La fonction de test est la suivante
int sra31(int x)
    {
      x >>= 31;
      return x;
    }
  • Il n’est pas surprenant que le même avertissement soit répété
    • Le compilateur continue d’essayer d’appliquer des « optimisations » jusqu’à ce qu’elles ne progressent plus
  • La sortie de clang-vs-clang distingue signed et unsigned dans les shifts
    • Cette différence est importante pour une réécriture manuelle ou automatique fondée sur crypto_{int,uint}{8,16,32,64}.h
    • clang-tidy fait partie des méthodes possibles pour automatiser la transformation des sources
  • Le code exclu par #ifdef ou éliminé avant cette phase d’« optimisation » ne produit pas d’avertissement clang-vs-clang

Résultats d’exécution SUPERCOP et cas trouvés

  • SUPERCOP 20240716 a été exécuté sur un double EPYC 7742 avec ./data-do-biglittle
    • L’overclocking était désactivé
    • La liste des compilateurs SUPERCOP a été ajustée pour utiliser clang-vs-clang en ajoutant -Rpass-analysis=clang-vs-clang aux lignes clang de okcompilers/{c,cpp}
  • Les résultats étaient prêts après 3 heures
    • La sortie de Clang comptait au total 675 752 lignes
    • La taille d’origine était de 210 786 494 octets
    • Le résultat compressé est 20240803-fromclang.txt.gz, de 3 595 199 octets
  • La sortie contient beaucoup de bruit issu de branchements source fondés sur des données publiques, qui créent des &1 en interne dans Clang
  • Un exemple clairement pertinent pour une modification préventive est le suivant
a0 += (a0>>15)&106;
  • Un exemple qui demanderait un effort de parsing C pour être trouvé par un simple scan des sources est le suivant
    • La macro ONE8 est définie comme ((uint8_t)1)
*pk2^=(((* pk_cp)>>ir)&ONE8)<<jr;
  • Un exemple encore plus difficile à trouver vient de macros basées sur des intrinsics AVX2
    • signmask_x16(x) est défini comme _mm256_srai_epi16((x),15)
    • Cela décale à droite de 15 bits chaque fragment signé de 16 bits dans un vecteur de 256 bits
mask = signmask_x16(sub_x16(x,const_x16((q+1)/2)));
  • Ce cas AVX2 n’est pas prioritaire
    • Pour qu’une opération vectorielle devienne un branchement conditionnel, il faudrait compiler en AVX-512 et que le compilateur prenne la décision étrange de transformer un bool vectorisé en branchement conditionnel bool sériel
    • TIMECOP utilise Valgrind, et Valgrind ne prend pas en charge AVX-512
    • Pour l’instant, la compilation AVX-512 n’est pas recommandée

int128 et réponses plus larges

  • La découverte la plus intéressante est un cas où un shift à droite de 64 bits sur int128 a déclenché un avertissement >>
  • Une implémentation int128 peut utiliser en interne un shift à droite de 63 bits pour déterminer le signe du mot haut de 64 bits
  • Si Clang ajoute, comme GCC, une prise en charge transformant un shift à droite de 63 bits en bool, puis en branchement conditionnel, beaucoup de code int128 pourrait soudain devenir à temps variable
    • Dans ce cas, la situation ressemblerait à ce qu’affirmait le titre de l’article de 2015, mais cette fois elle se produirait réellement même sans bool dans le source
  • La protection la plus simple au niveau source est d’éviter l’implémentation int128 existante du compilateur et d’utiliser des fonctions crypto_int128
    • Contrairement au int128 de GCC et Clang, crypto_int128 peut aussi fonctionner sur de petites plateformes 32 bits
  • L’ajout de types de données secrets à GCC et Clang semble une bonne idée, mais il est difficile de voir comment la rendre robuste dans l’architecture de ces deux compilateurs
  • Les compilateurs conçus dès le départ pour la sécurité inspirent davantage confiance
    • Parmi les compilateurs orientés sécurité qui exigent un nouveau langage d’entrée figurent FaCT et Jasmin, activement développé
    • Le temps nécessaire à la réécriture du code suscite des inquiétudes, mais au vu de la façon dont les compilateurs actuels traitent le code existant, une action est nécessaire sous une forme ou une autre

1 commentaires

 
GN⁺ 2024-08-05
Avis sur Hacker News
  • Il n’est pas correct d’appeler cela un bug du compilateur quand du code ayant un comportement indéfini ne se comporte pas comme souhaité.
    C’est un peu comme lancer dd avec de mauvais arguments, perdre ses données, puis dire que dd a un bug.

    • L’auteur semble ici confondre comportement défini par l’implémentation et comportement indéfini. Les exemples de l’article sont pour la plupart du code valide, et le vrai problème est que des optimisations du compilateur qui transforment de l’arithmétique sur bits en branchements rendent possibles des attaques par temporisation dans du code cryptographique.
      Il est difficile de dire que le code source ou le compilateur est bogué ; il est plus juste de considérer que la norme C est trop peu spécifiée selon les critères de l’auteur, ce qui crée des bugs de sécurité sur certaines cibles.
      Au bout du compte, les auteurs de la norme C ne peuvent pas définir jusqu’au comportement du matériel ; ils ne peuvent définir que la sémantique du langage. Le domaine de la cryptographie est donc condamné à souffrir de bugs imputables au matériel.
    • Le problème est que les comportements indéfinis en C et C++ sont absurdement nombreux, et qu’il est extrêmement difficile de tous les éviter.
      L’un des avantages de Rust est de limiter les comportements potentiellement indéfinis aux blocs unsafe. Même si Rust définit beaucoup de choses qui sont indéfinies en C, dès qu’on entre dans du code unsafe, il est très facile de tomber par erreur sur un comportement indéfini subtil.
    • Il n’existe que deux modèles de comportement indéfini utiles aux utilisateurs d’un compilateur : soit refuser de compiler si c’est une mauvaise idée, soit faire quelque chose de raisonnable et stable.
      Le troisième modèle, qui consiste à échouer silencieusement en générant du code imprévisible, n’est utile qu’aux auteurs de compilateurs. Se réfugier derrière la spécification n’apporte rien aux vrais utilisateurs.
    • L’article de Russ Cox, C and C++ Prioritize Performance over Correctness, traite bien ce sujet : https://research.swtch.com/ub
    • Cette réfutation ressemble surtout à un argument de l’homme de paille. Le point central est que les auteurs de compilateurs décident eux-mêmes de ce qui relève du comportement indéfini, et définissent la norme de façon à obtenir davantage de marge d’optimisation.
      Ces optimisations cassent du code qui fonctionnait bien auparavant. Les auteurs de compilateurs pourraient privilégier la compatibilité descendante, mais ils ne le font pas.
      De plus, comme ces optimisations n’améliorent pas vraiment les performances du code réel de manière significative, il faudrait réfuter l’idée que ce compromis, qui casse du code, n’en vaut pas la peine.
  • J’aime bien Bernstein, mais il lui arrive de se tromper de cible et de devenir excessif ; cet article en est un bon exemple. Il le reconnaît d’ailleurs à moitié lui-même à la fin.
    Une grande partie de l’article porte sur le point secondaire de savoir à quel point les gains d’optimisation sont intéressants, et même avec des données, c’est un jugement qui dépend des cas d’usage.
    Le grief principal est que les compilateurs C ne prennent pas en compte une sémantique impossible à exprimer dans le langage, ce qui n’a rien d’étonnant.
    À la fin, il dit « utilisez un langage capable d’exprimer la sémantique dont vous avez besoin » ; tout l’article aurait pu être remplacé par cette seule phrase.

    • Le point important est que ceux qui définissent la sémantique de C et C++ jettent beaucoup trop de comportements dans le panier du « comportement indéfini ».
      Pour une bonne partie d’entre eux, la justification est douteuse, et cela rend l’écriture de programmes corrects plus difficile.
    • Le passage expliquant que les gains d’optimisation dépendent des cas d’usage apportait un contexte utile, et m’a pas mal ouvert les yeux.
    • Ici, DJB n’était pas très convaincant. On y voit beaucoup de croyances élitistes quasi religieuses non étayées.
  • C et C++ ne sont pas adaptés à l’écriture d’algorithmes offrant une garantie de temps constant.
    La norme ne contient presque aucune notion de temps réel, et les compilateurs ne fournissent pas non plus de garanties supplémentaires via des extensions.
    Mais rejeter la faute sur les développeurs de compilateurs n’est pas la bonne direction.

    • Si l’on veut produire du code machine qui effectue toujours les opérations en temps constant, indépendamment des branchements, il faut utiliser un langage capable de l’exprimer. C ne le prend pas en charge.
    • Je me demande quel langage serait adapté à l’écriture d’algorithmes avec garantie de temps constant.
  • Sur les CPU Intel, ni clang ni aucun autre compilateur ne peut générer du code correct en mode utilisateur, parce qu’un code correct n’existe tout simplement pas.
    https://www.intel.com/content/www/us/en/developer/articles/t...
    En regardant DOITM dans le document, il est tout simplement impossible pour une bibliothèque de chiffrement en espace utilisateur de définir le bit nécessaire.

    • Le code en mode utilisateur peut tout de même s’exécuter dans le bon mode. Il ne peut simplement pas activer ou désactiver lui-même ce mode.
      Une fois activé, cela fonctionne aussi en espace utilisateur ; on pourrait donc par exemple en faire un indicateur par processus activé via l’appel système prctl, puis ajuster le MSR lors des changements de tâche par l’ordonnanceur.
    • Ne peut-on pas faire un appel système vers le noyau pour définir le flag, puis revenir en mode utilisateur dans cet état ?
  • Rien que la phrase « chaque fois qu’ils le peuvent, les auteurs de compilateurs refusent d’assumer la responsabilité des bugs qu’ils ont créés » suffit à voir rarement l’expertise d’un billet de blog s’effondrer aussi vite.
    Si l’on suit même le lien, il ne s’agit que d’un point très élémentaire de C : un comportement indéfini ne signifie pas qu’il produit une « valeur arbitraire ».

    • Il semble qu’ils appellent « bug » deux choses différentes. L’un parle d’un bug dans le code source, l’autre d’un bug dans le programme généré.
      Même en présence d’un comportement indéfini, le code source peut être bogué alors que le programme généré reste souvent correct. Plus tard, si un auteur de compilateur ajoute une nouvelle optimisation et génère, sur la base de ce comportement indéfini, un programme bogué, la bataille sur les responsabilités commence.
      Ce qu’on rechigne à admettre, c’est que la responsabilité envers l’utilisateur est répartie entre toutes les parties. Si la batterie a pris feu simplement parce qu’une application CRUD a déréférencé NULL, une personne raisonnable ne se contenterait pas de reprocher à l’auteur de l’application d’avoir oublié un test de NULL.
      Les compilateurs, les systèmes d’exploitation et les fabricants de matériel doivent aussi répondre de produits conçus de manière irresponsable ; la mention « comportement indéfini » dans la norme ISO ne suffit pas à clore le sujet. Tous les acteurs de la chaîne d’approvisionnement partagent la responsabilité d’anticiper les mauvais usages possibles de leurs produits et de les gérer raisonnablement.
    • Je pense que l’auteur sait très bien ce qu’est un comportement indéfini. Il porte simplement un regard critique sur l’ensemble du système.
      Le comportement indéfini existe pour apporter de la valeur. On peut concevoir un langage sans cela ; s’il existe malgré tout, c’est pour la portabilité et pour la flexibilité qu’il donne aux auteurs de compilateurs.
      Le cœur du texte est de savoir si cette flexibilité vaut la difficulté d’écrire des programmes sans comportement indéfini.
      L’auteur estime que l’argent perdu à cause des bugs semble supérieur à celui économisé grâce à un bytecode plus rapide, et que, comme les auteurs de compilateurs ont une forte influence sur ce qui entre dans la norme du langage, la volonté de corriger cela est faible.
  • À noter que clang dispose de l’attribut clang::optnone, qui désactive toutes les optimisations fonction par fonction, et que GCC possède l’excellent attribut gnu::optimize, qui permet d’ajouter ou de retirer des optimisations par leur nom, ou de fixer un niveau d’optimisation indépendamment des flags du compilateur.
    gnu::optimize(0) ressemble à ce flag de clang. clang propose aussi clang::no_builtins, qui désactive en particulier les optimisations de memcpy et memset.

  • Je comprends dans une certaine mesure les objectifs recherchés par les gens de la cryptographie, par exemple l’évaluation en temps constant et la dissimulation des valeurs secrètes.
    Mais un compilateur généraliste ne pense pas à ce genre de choses la plupart du temps, donc cela semble difficile d’aller au-delà de hacks qui fonctionnent à peu près.
    Pour faire les choses sérieusement, il faudra sans doute un compilateur spécialisé maison, ou continuer à passer par l’assembleur.

  • Un jour, on considérera probablement notre époque comme le mauvais vieux temps, et on aura quitté C pour des langages avec beaucoup moins de comportements indéfinis
    En C, il est beaucoup trop facile d’écrire des expressions qui compilent, mais dont le compilateur ne peut absolument pas deviner l’intention
    Par exemple, en Python, on peut écrire du code comme result = [something(value) for value in set_object]. Comme un objet set n’a pas d’ordre, il est clair que l’ordre de traitement des éléments et l’ordre du résultat n’ont pas d’importance, ce qui ouvre la voie, au niveau du langage, à de nombreuses optimisations sans que le compilateur ait à deviner l’intention de l’auteur
    Un code similaire dans un autre langage avec des données immuables va encore plus loin : comme something(value1) ne peut pas affecter something(value2), il peut être exécuté en parallèle, que ce soit avec des threads ou des processus
    Une grande partie de l’optimisation des compilateurs C consiste à observer des motifs de code et à trouver comment faire plus vite ce que l’auteur avait probablement l’intention de faire. Par rapport aux langages modernes, C exprime mal l’intention, ce qui laisse une certaine liberté d’interprétation, mais il faut effectuer ce type d’inférence pour obtenir des performances correctes
    Cela dit, c’est peut-être une bénédiction déguisée, un peu comme lorsque le télescope Hubble a eu besoin de lunettes. Pour dépasser ces limites, on a inventé d’excellentes techniques, et une fois le problème corrigé, ces techniques ont produit des performances bien supérieures à ce qui était initialement attendu. Appliquer les optimisations des compilateurs C à des langages non-C pourrait presque fonctionner comme un super-pouvoir

    • Le défaut de l’exemple Python est que, même si l’ordre n’est pas spécifié, les gens peuvent dépendre de certaines propriétés, et si l’optimiseur change l’ordre, le code peut casser
      C’est fondamentalement proche d’un comportement indéfini, sauf que cela ne se manifeste pas immédiatement comme un problème de sûreté, mais comme un résultat incorrect. Bien sûr, un résultat incorrect peut ensuite mener à un problème de sûreté
      Contrairement au comportement indéfini, il est pratiquement impossible de créer un « sanitizer » qui vérifierait que le code fonctionne pour tous les ordres possibles d’un set
      gcc et clang disposent de nombreux indices bas niveau que l’on ne trouve pas souvent dans d’autres langages. Il y a __builtin_expect/__builtin_unpredictable, __builtin_unreachable/__builtin_assume, #pragma clang loop vectorize(assume_safety)/#pragma GCC ivdep, ainsi que des pragma pour désactiver le déroulage de boucle ou la vectorisation, ou pour choisir certaines valeurs
      Le plus grand manque, à mon avis, est une barrière d’optimisation explicite empêchant le compilateur de faire des inférences à partir de l’origine des valeurs. __asm__ le permet dans une certaine mesure, mais avec des effets secondaires indésirables et la nécessité de noms de types de registres propres à chaque plateforme
      Le potentiel des optimisations de haut niveau fondées sur l’intention est lui aussi évident. On peut imaginer réserver l’espace d’une liste-tableau avant de faire n push dans une boucle, fusionner des accès à une hashmap du type contains→get→put avec la même clé, ou encore inférer localement le comportement d’allocation global pour éliminer des objets et des allocations
    • En théorie, cela se tient, mais en pratique, rien n’a prouvé que ce soit plus rapide que C
      C est suffisamment proche du matériel réel pour que le programmeur puisse simplement dire quoi faire, sans que le compilateur ait besoin de deviner son intention
    • Il est vrai qu’il existe une marge pour des optimisations fondées sur la sémantique, mais d’après ce qu’on observe, ces optimisations tournent généralement autour de l’allocation mémoire
      Les langages qui implémentent ce type d’optimisations mémoire sont pour la plupart de la famille Java, et comme ils comportent dès le départ une pessimisation agressive en amont, cela crée une motivation pour ces optimisations. Mais même ces optimisations ne compensent pas les pertes
      L’essentiel, c’est que C n’est pas terrible, mais que l’autre option est pire
  • Si la sémantique de C ne vous plaît pas, ne vous énervez pas contre les ingénieurs compilateurs : utilisez un autre langage de programmation

    • Honnêtement, je ne sais pas si djb peut tolérer quoi que ce soit d’autre que son propre qhasm. Même Zig. Cette critique venant de lui n’est donc pas très surprenante
  • Un article rafraîchissant qui expose un point de vue qu’on n’entendait pas souvent. À lire aussi : https://gavinhoward.com/2023/08/the-scourge-of-00ub/