Les extensions C notables incluent-elles des types entiers dont le comportement est indépendant de la taille du mot machine

12

Une caractéristique intéressante de C par rapport à d'autres langages est que bon nombre de ses types de données sont basés sur la taille des mots de l'architecture cible, plutôt que d'être spécifiés en termes absolus. Bien que cela permette au langage d'être utilisé pour écrire du code sur des machines qui pourraient avoir des difficultés avec certains types, il est très difficile de concevoir du code qui s'exécutera de manière cohérente sur différentes architectures. Considérez le code:

uint16_t ffff16 = 0xFFFF;
int64_t who_knows = ffff16 * ffff16;

Sur une architecture où intest 16 bits (toujours vrai de nombreux petits microcontrôleurs) ce code attribuerait une valeur de 1 en utilisant un comportement bien défini. Sur les machines où intest 64 bits, il attribuerait une valeur 4294836225, toujours en utilisant un comportement bien défini. Sur les machines où intest 32 bits, il attribuerait probablement une valeur de -131071 (je ne sais pas si ce serait un comportement défini par l'implémentation ou indéfini). Même si le code n'utilise rien sauf ce qui est censé être des types "de taille fixe", la norme exigerait que deux types de compilateurs différents utilisés aujourd'hui donnent deux résultats différents, et de nombreux compilateurs populaires en rapportent aujourd'hui un troisième.

Cet exemple particulier est quelque peu artificiel, en ce que je ne m'attendrais pas à ce que le code du monde réel attribue directement le produit de deux valeurs 16 bits à une valeur 64 bits, mais il a été choisi comme un bref exemple pour montrer un entier de trois manières les promotions peuvent interagir avec des types non signés de taille supposée fixe. Il y a des situations du monde réel où il est nécessaire que les mathématiques sur des types non signés soient effectuées selon les règles de l'arithmétique mathématique des entiers, d'autres où il est nécessaire qu'elles soient effectuées selon les règles de l'arithmétique modulaire, et d'autres où elles ne le font vraiment pas '' t importe. Une grande partie du code du monde réel pour des choses comme les sommes de contrôle repose sur le uint32_tmod de bouclage arithmétique 2³² et sur la possibilité d'effectuer des arbitrairesuint16_t arithmétique et obtenir des résultats qui sont, au minimum, définis comme étant le mod 65536 précis (par opposition au déclenchement d'un comportement indéfini).

Même si cette situation semble clairement indésirable (et le deviendra davantage à mesure que le traitement 64 bits devient la norme à de nombreuses fins), le comité des normes C de ce que j'ai observé préfère introduire des fonctionnalités de langage qui sont déjà utilisées dans certaines productions notables. plutôt que de les inventer "à partir de zéro". Existe-t-il des extensions notables du langage C qui permettraient au code de spécifier non seulement comment un type sera stocké mais aussi comment il devrait se comporter dans des scénarios impliquant des promotions possibles? Je peux voir au moins trois façons dont une extension de compilateur peut résoudre ces problèmes:

  1. En ajoutant une directive qui demanderait au compilateur de forcer certains types entiers "fondamentaux" à certaines tailles.

  2. En ajoutant une directive qui demanderait au compilateur d'évaluer divers scénarios de promotion comme si les types de la machine avaient des tailles particulières, quelles que soient les tailles réelles des types sur l'architecture cible.

  3. En autorisant des moyens de déclarer des types avec des caractéristiques spécifiques (par exemple, déclarer qu'un type doit se comporter comme un anneau algébrique enveloppant le mod-65536, quelle que soit la taille du mot sous-jacent, et ne doit pas être implicitement convertible en d'autres types; l'ajout d'un wrap32à un intdevrait produire un résultat de type, wrap32qu'il intsoit supérieur ou non à 16 bits, tout en ajoutant un wrap32directement à un wrap16devrait être illégal (car aucun ne pourrait se convertir à l'autre).

Ma propre préférence serait la troisième alternative, car elle permettrait même aux machines avec des tailles de mots inhabituelles de travailler avec beaucoup de code qui s'attend à ce que les variables soient «encapsulées» comme elles le feraient avec des tailles de puissance de deux; le compilateur peut devoir ajouter des instructions de masquage de bits pour que le type se comporte correctement, mais si le code a besoin d'un type qui enveloppe le mod 65536, il est préférable que le compilateur génère un tel masquage sur les machines qui en ont besoin que d'encombrer le code source avec lui ou simplement avoir un tel code inutilisable sur des machines où un tel masquage serait nécessaire. Je suis curieux, cependant, s'il existe des extensions communes qui permettraient d'obtenir un comportement portable via l'un des moyens ci-dessus, ou via des moyens auxquels je n'ai pas pensé.

Pour clarifier ce que je recherche, il y a quelques choses; notamment:

  1. Bien qu'il existe de nombreuses façons d'écrire du code de manière à garantir la sémantique souhaitée (par exemple, définir des macros pour effectuer des calculs sur des opérandes non signés de taille particulière afin de produire un résultat qui enveloppe explicitement ou non) ou au moins empêche les indésirables la sémantique (par exemple, définir de manière conditionnelle un type wrap32_tpour être uint32_tsur des compilateurs où a uint32_tne serait pas promu, et comprendre qu'il vaut mieux pour le code qui nécessite l' wrap32_téchec de la compilation sur des machines où ce type serait promu que de le faire s'exécuter et produire un comportement faux), s'il existe un moyen d'écrire le code qui jouerait le plus favorablement avec les futures extensions de langage, l'utiliser serait mieux que de concevoir ma propre approche.

  2. J'ai quelques idées assez solides sur la façon dont le langage pourrait être étendu afin de résoudre de nombreux problèmes de taille entière, permettant au code de générer une sémantique identique sur des machines avec des tailles de mots différentes, mais avant de passer beaucoup de temps à les écrire, je voudrais de savoir quels efforts dans ce sens ont déjà été entrepris.

Je ne souhaite en aucun cas être perçu comme dénigrant le Comité des normes C ou le travail qu'il a produit; Je m'attends cependant à ce que d'ici quelques années, il devienne nécessaire de faire fonctionner correctement le code sur les machines où le type de promotion "naturel" serait 32 bits, ainsi que celles où il serait 64 bits. Je pense qu'avec quelques extensions modestes de la langue (plus modestes que la plupart des autres changements entre C99 et C14), il serait possible non seulement de fournir un moyen propre d'utiliser efficacement les architectures 64 bits, mais dans le marché de faciliter également l'interaction avec les machines de "taille de mot inhabituelle" que la norme a historiquement recourbées pour prendre en charge [par exemple, permettant à une machine avec un charcode 12 bits d'exécuter du code qui attend unuint32_tpour envelopper le mod 2³²]. Selon la direction que prendront les futures extensions, je m'attendrais également à ce qu'il soit possible de définir des macros qui permettraient au code écrit aujourd'hui d'être utilisable sur les compilateurs d'aujourd'hui où les types entiers par défaut se comportent comme "attendus", mais également utilisable sur les futurs compilateurs où l'entier les types seraient par défaut se comporter différemment, mais où peuvent fournir les comportements requis.

supercat
la source
4
@RobertHarvey Êtes-vous sûr? Si je comprends bien la promotion entière , si intest plus grand que uint16_t, les opérandes de la multiplication seraient promus intet la multiplication serait effectuée comme intmultiplication, et la intvaleur résultante serait convertie en int64_tpour l'initialisation de who_knows.
3
@RobertHarvey Comment? Dans le code OP, il n'y est fait aucune mention int, mais il se faufile toujours. (Encore une fois, en supposant que ma compréhension de la norme C est correcte.)
2
@RobertHarvey Bien sûr, cela semble mauvais, mais à moins que vous ne puissiez indiquer une telle façon, vous ne contribuez à rien en disant "non, vous devez faire quelque chose de mal". La question même est de savoir comment éviter la promotion entière ou contourner ses effets!
3
@RobertHarvey: L' un des objectifs historiques du Comité des normes C a été de permettre à la plupart des machines d'avoir un « compilateur C », et que les règles soient suffisamment précis pour que les compilateurs C indépendamment développés pour une machine cible particulière serait être principalement interchangeable. Cela a été compliqué par le fait que les gens ont commencé à écrire des compilateurs C pour de nombreuses machines avant la rédaction des normes, et le Comité des normes n'a pas voulu interdire aux compilateurs de faire quoi que ce soit sur lequel le code existant pourrait s'appuyer . Quelques aspects assez fondamentaux de la norme ...
supercat
3
... ne le sont pas parce que quelqu'un a essayé de formuler un ensemble de règles qui "avait du sens", mais plutôt parce que le Comité essayait de définir toutes les choses que les compilateurs indépendants qui existaient déjà avaient en commun. Malheureusement, cette approche a conduit à des normes qui sont à la fois trop vagues pour permettre aux programmeurs de spécifier ce qui doit être fait, mais trop spécifiques pour permettre aux compilateurs de "simplement le faire".
supercat

Réponses:

4

Comme l'intention typique d'un code comme celui-ci

uint16_t ffff16 = 0xFFFF;
int64_t who_knows = ffff16 * ffff16;

consiste à effectuer la multiplication en 64 bits (la taille de la variable dans laquelle le résultat est stocké), la manière habituelle d'obtenir le résultat correct (indépendant de la plate-forme) consiste à transtyper l'un des opérandes pour forcer une multiplication à 64 bits:

uint16_t ffff16 = 0xFFFF;
int64_t i_know = (int64_t)ffff16 * ffff16;

Je n'ai jamais rencontré d'extensions C qui rendent ce processus automatique.

Bart van Ingen Schenau
la source
1
Ma question n'était pas de savoir comment forcer l'évaluation correcte d'une expression arithmétique particulière (selon le type de résultat souhaité, soit convertir un opérande uint32_tou utiliser une macro définie comme étant #define UMUL1616to16(x,y)((uint16_t)((uint16_t)(x)*(uint16_t)(y)))soit en #define UMUL1616to16(x,y)((uint16_t)((uint32_t)(x)*(uint16_t)(y)))fonction de la taille de int), mais plutôt s'il existe toute norme émergente sur la façon de gérer ces choses de manière utile plutôt que de définir mes propres macros.
supercat
J'aurais également dû mentionner que pour des choses comme le hachage et les calculs de somme de contrôle, le but sera souvent de prendre un résultat et de le tronquer à la taille des opérandes. L'intention typique d'une expression telle (ushort1*ushort2) & 65535userait d'effectuer une arithmétique mod-65536 pour toutes les valeurs d'opérande. En lisant la justification de C89, je pense qu'il est assez clair que même si les auteurs ont reconnu qu'un tel code pourrait échouer sur certaines implémentations si le résultat dépassait 2147483647, ils s'attendaient à ce que ces implémentations deviennent de plus en plus rares. Cependant, ce code échoue parfois sur gcc moderne.
supercat