Il paraît que uint32_t
est beaucoup plus répandu que uint_fast32_t
_ (Je réalise que ceci est une preuve anecdotique). Cela semble contre-intuitif pour moi, cependant.
Presque toujours, quand je vois une implémentation, utiliser uint32_t
, tout ce qu’il veut vraiment, c’est un entier pouvant contenir des valeurs allant jusqu’à 4 294 967 295 (généralement une limite beaucoup plus basse située entre 65 535 et 4 294 967 295).
Il semble bizarre d'utiliser ensuite uint32_t
, comme la garantie 'exactement 32 bits' n'est pas nécessaire, et la garantie 'le plus rapide disponible> = 32 bits' de uint_fast32_t
semble être exactement la bonne idée. De plus, bien que généralement implémenté, uint32_t
_ n'est pas réellement garanti.
Pourquoi, alors, uint32_t
être préféré? Est-ce simplement mieux connu ou y a-t-il des avantages techniques par rapport aux autres?
uint32_t
est garanti avoir presque les mêmes propriétés sur toutes les plates-formes qui le prennent en charge.1
uint_fast32_t
n'a que très peu de garanties sur la façon dont il se comporte sur différents systèmes en comparaison.
Si vous passez sur une plate-forme où uint_fast32_t
a une taille différente, tout le code utilisant uint_fast32_t
doit être retesté et validé. Toutes les hypothèses de stabilité vont sortir par la fenêtre. L'ensemble du système va fonctionner différemment.
Lorsque vous écrivez votre code, vous n’avez peut-être même pas accès à accès à un uint_fast32_t
système dont la taille n’est pas de 32 bits.
uint32_t
ne fonctionnera pas différemment (voir note de bas de page).
La correction est plus importante que la vitesse. L’exactitude prématurée est donc un meilleur plan que l’optimisation prématurée.
Si j’écrivais du code pour des systèmes où uint_fast32_t
avait 64 bits ou plus, je pourrais tester mon code dans les deux cas et l’utiliser. Sauf si besoin et opportunité, c'est un mauvais plan.
Finalement, uint_fast32_t
lorsque vous le stockez pour une durée indéterminée ou qu'un nombre d'instances peut être plus lent que uint32
simplement en raison de problèmes de taille de cache et de bande passante mémoire. De nos jours, les ordinateurs sont beaucoup plus souvent liés à la mémoire qu’au processeur, et uint_fast32_t
pourrait être plus rapide isolément, mais pas après la prise en compte de la surcharge de mémoire.
1 Comme @chux l’a noté dans un commentaire, si unsigned
est supérieur à uint32_t
, arithmétique sur uint32_t
passe par les promotions entières habituelles, et sinon, il reste comme uint32_t
. Cela peut causer des bugs. Rien n'est jamais parfait.
Pourquoi beaucoup de gens utilisent-ils
uint32_t
plutôt queuint32_fast_t
?
Remarque: mal nommé uint32_fast_t
devrait être uint_fast32_t
.
uint32_t
a une spécification plus stricte que uint_fast32_t
et ainsi permet une fonctionnalité plus cohérente.
uint32_t
avantages:
uint32_t
les inconvénients:
uint_fast32_t
avantages:
uint_fast32_t
les inconvénients:
uint32_fast_t
. On dirait que beaucoup n'ont tout simplement pas besoin et utilisent ce type. Nous n'avons même pas utilisé le bon nom!uint_fast32_t
n'est qu'une approximation du 1er ordre. En fin de compte, le meilleur dépend de l'objectif de codage. À moins que vous ne codiez pour une très grande portabilité ou une fonction de performance spécifique, utilisez uint32_t
.
L'utilisation de ces types pose un autre problème: leur rang par rapport à int/unsigned
Vraisemblablement uint_fastN_t
serait au moins le rang de unsigned
. Ce n'est pas spécifié, mais une condition certaine et testable.
Ainsi, uintN_t
est plus probable que uint_fastN_t
être plus étroit le unsigned
. Cela signifie que le code qui utilise uintN_t
_ maths est plus susceptible de faire l’objet de promotions d’entier que uint_fastN_t
concernant la portabilité.
Avec cette préoccupation: avantage de portabilité uint_fastN_t
avec certaines opérations mathématiques.
Note latérale sur int32_t
plutôt que int_fast32_t
: Sur des machines rares, INT_FAST32_MIN
peut être -2 147 483 647 et non -2 147 483 648. Le plus gros point: (u)intN_t
Les types sont bien spécifiés et mènent au code portable.
Pourquoi beaucoup de gens utilisent-ils
uint32_t
plutôt queuint32_fast_t
?
Réponse idiote:
uint32_fast_t
, l'orthographe correcte est uint_fast32_t
.Réponse pratique:
uint32_t
ou int32_t
pour leur sémantique précise, exactement 32 bits avec une arithmétique non signée en boucle (uint32_t
) ou représentation du complément à 2 (int32_t
). Le xxx_fast32_t
les types peuvent être plus volumineux et donc inappropriés pour être stockés dans des fichiers binaires, utilisés dans des tableaux et des structures compactés, ou envoyés via un réseau. De plus, ils ne seront peut-être même pas plus rapides.Réponse pragmatique:
uint_fast32_t
, comme démontré dans les commentaires et les réponses, et supposons probablement que unsigned int
pour avoir la même sémantique, bien que de nombreuses architectures actuelles aient encore int
s en 16 bits et que quelques rares exemples du Musée aient d’autres tailles étranges inférieures à 32.Réponse UX:
uint32_t
, uint_fast32_t
est plus lent à utiliser: la saisie prend plus de temps, ce qui prend en compte notamment la recherche d'orthographe et de sémantique dans la documentation C ;-)L'élégance compte (évidemment basée sur l'opinion):
uint32_t
semble assez mauvais pour que de nombreux programmeurs préfèrent définir leur propre u32
ou uint32
type ... De ce point de vue, uint_fast32_t
semble maladroit au-delà de la réparation. Pas de surprise, il est assis sur le banc avec ses amis uint_least32_t
et autres choses de ce genre.Une des raisons est que unsigned int
est déjà "le plus rapide" sans qu'il soit nécessaire de recourir à des types de caractères spéciaux ou d'inclure quelque chose. Donc, si vous en avez besoin rapidement, utilisez simplement le fondamental int
ou unsigned int
type.
Bien que la norme ne garantisse pas explicitement qu’elle est la plus rapide, elle indirectement le fait en indiquant "Les entités simples ont la taille naturelle suggérée par l’architecture de l’environnement d’exécution" dans 3.9.1. En d'autres termes, int
(ou son équivalent non signé) est ce avec quoi le processeur est le plus à l'aise.
Maintenant bien sûr, vous ne savez pas quelle taille unsigned int
pourrait être. Vous savez seulement que c'est au moins aussi grand que short
(et je semble me souvenir que short
doit avoir au moins 16 bits, bien que je ne le trouve pas dans la norme maintenant!). Habituellement, il s'agit simplement de 4 octets, mais il peut en théorie être plus grand ou, dans les cas extrêmes, encore plus petit (bien que je n’ai personnellement jamais rencontré d’architecture où c’était le cas, pas même sur des ordinateurs 8 bits dans les années 1980 ... peut-être quelques microcontrôleurs, qui sait s’avère que je souffre de démence, int
était très clairement 16 bits à l’époque).
La norme C++ ne se donne pas la peine de spécifier ce que le <cstdint>
Les types sont ou ce qu’ils garantissent, il est simplement mentionné "comme en C".
uint32_t
, selon le standard C, garantit que vous obtenez exactement 32 bits. Rien de différent, rien de moins et pas de bits de remplissage. Parfois, c’est exactement ce dont vous avez besoin, ce qui est donc très précieux.
uint_least32_t
garantit que quelle que soit sa taille, il ne peut pas être inférieur à 32 bits (mais il pourrait très bien être plus volumineux). Parfois, mais beaucoup plus rarement qu'une pensée exacte ou "ne s'en soucie pas", c'est ce que vous voulez.
Enfin, uint_fast32_t
est à mon avis un peu superflu, sauf pour des raisons de documentation d’intention. La norme C stipule "désigne un type entier qui est généralement le plus rapide" (notez le mot "habituellement") et mentionne explicitement qu'il n'est pas nécessairement rapide à toutes fins. En d'autres termes, uint_fast32_t
est à peu près la même chose que uint_least32_t
, qui est généralement le plus rapide aussi, seulement aucune garantie donnée (mais aucune garantie dans les deux cas).
Depuis la plupart du temps, soit vous ne vous souciez pas de la taille exacte, soit vous voulez exactement 32 (ou 64, parfois 16) bits, et depuis le "ne vous souciez pas" unsigned int
le type est le plus rapide de toute façon, cela explique pourquoi uint_fast32_t
_ n'est pas si fréquemment utilisé.
Je n'ai pas vu de preuves que uint32_t
_ être utilisé pour son plage. Au lieu de cela, la plupart du temps j'ai vu uint32_t
_ est utilisé, il contient exactement 4 octets de données dans différents algorithmes, avec une sémantique enveloppante et de décalage garantie!
Il y a aussi d'autres raisons d'utiliser uint32_t
au lieu de uint_fast32_t
: Souvent, cela donne une ABI stable. De plus, l'utilisation de la mémoire peut être connue avec précision. Cela compense beaucoup quel que soit le gain de vitesse de uint_fast32_t
, à chaque fois ce type serait distinct de celui de uint32_t
.
Pour les valeurs <65536, il existe déjà un type pratique, il s'appelle unsigned int
(unsigned short
doit avoir au moins cette plage, mais unsigned int
est de taille Word native) Pour les valeurs <4294967296, il existe un autre nom appelé unsigned long
.
Et enfin, les gens n'utilisent pas uint_fast32_t
parce qu’il est fastidieux de taper et facile à taper: D
Plusieurs raisons.
En résumé, les types "rapides" sont des déchets sans valeur. Si vous avez vraiment besoin de savoir quel type est le plus rapide pour une application donnée, vous devez analyser votre code avec votre compilateur.
Du point de vue de l'exactitude et de la facilité de codage, uint32_t
Présente de nombreux avantages par rapport à uint_fast32_t
, Notamment en raison de la taille et de la sémantique arithmétiques définies plus précisément, comme de nombreux utilisateurs l'ont déjà souligné.
Ce qui a peut-être été oublié, c’est que le un avantage supposé de uint_fast32_t
- qu’il puisse être plus rapide , mais jamais matérialisé de manière significative. La plupart des processeurs 64 bits qui ont dominé l'ère 64 bits (principalement x86-64 et Aarch64) ont évolué à partir d'architectures 32 bits et ont rapide Opérations natives 32 bits, même en mode 64 bits. Donc, uint_fast32_t
Est identique à uint32_t
Sur ces plateformes.
Même si certaines des plates-formes "également exécutées" telles que POWER, MIPS64, SPARC n'offrent que des opérations ALU 64 bits, les grande majorité d'intéressantes opérations 32 bits peuvent être bien fait sur les registres 64 bits: le 32 bits inférieur aura les résultats souhaités (et toutes les plates-formes grand public vous permettront au moins de charger/stocker des fichiers 32 bits). Le décalage gauche est le principal problème, mais être optimisé dans de nombreux cas par des optimisations de suivi valeur/plage dans le compilateur.
Je doute que le décalage gauche légèrement plus lent occasionnel ou la multiplication 32x32 -> 64 l'emporte double sur l'utilisation de la mémoire pour de telles valeurs, sauf la plus obscure. applications.
Enfin, je noterai que, même si le compromis a été largement qualifié de "potentiel d’utilisation de la mémoire et de vectorisation" (en faveur de uint32_t
) Par rapport au nombre d’instructions/vitesse (en faveur de uint_fast32_t
) - même ce n'est pas clair pour moi. Oui, sur certaines plates-formes, vous aurez besoin d'instructions supplémentaires pour certaines opérations 32 bits, mais vous aurez également sauve quelques instructions parce que:
struct two32{ uint32_t a, b; }
Dans rax
comme two32{1, 2}
peut être optimisée dans un seul mov rax, 0x20001
la version bit a besoin de deux instructions. En principe, cela devrait également être possible pour les opérations arithmétiques adjacentes (même opération, opérande différent), mais je ne l'ai pas vu dans la pratique.Les types de données plus petits exploitent souvent des conventions d’appel plus modernes telles que l’ABI SysV, qui encapsule efficacement les données de structure de données dans des registres. Par exemple, vous pouvez retourner jusqu'à une structure de 16 octets dans les registres rdx:rax
. Pour une fonction retournant une structure avec 4 valeurs uint32_t
(Initialisée à partir d'une constante), cela se traduit par
ret_constant32():
movabs rax, 8589934593
movabs rdx, 17179869187
ret
La même structure avec 4 uint_fast32_t
64 bits nécessite un déplacement de registre et quatre magasins en mémoire pour faire la même chose (et l'appelant devra probablement lire les valeurs de la mémoire après le retour):
ret_constant64():
mov rax, rdi
mov QWORD PTR [rdi], 1
mov QWORD PTR [rdi+8], 2
mov QWORD PTR [rdi+16], 3
mov QWORD PTR [rdi+24], 4
ret
De même, lors du passage d'arguments de structure, les valeurs 32 bits sont regroupées deux fois plus étroitement dans les registres disponibles pour les paramètres. Il est donc moins probable que vous manquiez d'arguments de registre et que vous deviez en déborder la pile.1.
Même si vous choisissez d'utiliser uint_fast32_t
Pour les endroits où "la vitesse compte", vous aurez souvent aussi des endroits où vous aurez besoin d'un type de taille fixe. Par exemple, lorsque vous transmettez des valeurs pour une sortie externe, depuis une entrée externe, en tant que partie de votre ABI, en tant que partie d'une structure nécessitant une mise en page spécifique, ou parce que vous utilisez intelligemment uint32_t
Pour de grandes agrégations de valeurs sur lesquelles enregistrer empreinte mémoire. Aux endroits où vos types uint_fast32_t
Et `` uint32_t` doivent s'interfacer, vous pouvez trouver (en plus de la complexité du développement), des extensions de signature inutiles ou un autre code lié à une différence de taille. Les compilateurs parviennent à optimiser cette situation dans de nombreux cas, mais il n’est pas rare de voir cela dans une sortie optimisée lors du mélange de types de tailles différentes.
Vous pouvez jouer avec certains des exemples ci-dessus et plus encore sur godbolt .
1 Pour être clair, la convention consistant à bien tasser les structures dans des registres ne permet pas toujours de gagner des valeurs plus petites. Cela signifie que les plus petites valeurs devront peut-être être "extraites" avant de pouvoir être utilisées. Par exemple, une fonction simple qui renvoie la somme des deux membres de la structure nécessite un mov rax, rdi; shr rax, 32; add edi, eax
, Tandis que pour la version 64 bits, chaque argument obtient son propre registre et nécessite simplement un seul add
ou lea
. Néanmoins, si vous acceptez que la conception "compacte les structures en passant" soit logique, les valeurs plus petites tireront davantage parti de cette fonctionnalité.
D'après ma compréhension, int
était initialement supposé être un type entier "natif" avec une garantie supplémentaire qu'il devait avoir une taille d'au moins 16 bits - une taille considérée à l'époque comme "raisonnable".
Lorsque les plates-formes 32 bits sont devenues plus courantes, on peut dire que la taille "raisonnable" a été modifiée en 32 bits:
int
32 bits sur toutes les plateformes.int
est au moins de 32 bits.int
qui est garanti pour être exactement 32 bits.Mais lorsque la plate-forme 64 bits est devenue la norme, personne n'a développé int
pour devenir un entier 64 bits à cause de:
int
.int
pourrait être déraisonnable dans la plupart des cas, étant donné que, dans la plupart des cas, leur nombre utilisé est nettement inférieur à 2 milliards.Maintenant, pourquoi préféreriez-vous uint32_t
À uint_fast32_t
? Pour les mêmes langages de raison, C # et Java utilisent toujours des entiers de taille fixe: le programmeur n'écrit pas de code en pensant aux tailles possibles de différents types, il écrit pour une plate-forme et teste le code sur cette plate-forme. du code dépend implicitement de la taille spécifique des types de données, raison pour laquelle uint32_t
est un meilleur choix dans la plupart des cas - elle ne permet aucune ambiguïté quant à son comportement.
De plus, uint_fast32_t
Est-il vraiment le type le plus rapide sur une plate-forme de taille égale ou supérieure à 32 bits? Pas vraiment. Considérez ce compilateur de code par GCC pour x86_64 sous Windows:
extern uint64_t get(void);
uint64_t sum(uint64_t value)
{
return value + get();
}
L'assemblage généré ressemble à ceci:
Push %rbx
sub $0x20,%rsp
mov %rcx,%rbx
callq d <sum+0xd>
add %rbx,%rax
add $0x20,%rsp
pop %rbx
retq
Maintenant, si vous modifiez la valeur de retour de get()
en uint_fast32_t
(Ce qui correspond à 4 octets sous Windows x86_64), vous obtenez ceci:
Push %rbx
sub $0x20,%rsp
mov %rcx,%rbx
callq d <sum+0xd>
mov %eax,%eax ; <-- additional instruction
add %rbx,%rax
add $0x20,%rsp
pop %rbx
retq
Notez que le code généré est presque identique, à l'exception de l'instruction supplémentaire mov %eax,%eax
Après l'appel de la fonction, destinée à développer la valeur 32 bits en une valeur 64 bits.
Il n'y a pas de problème de ce type si vous utilisez uniquement des valeurs 32 bits, mais vous utiliserez probablement celles avec des variables size_t
(Tailles de tableaux probablement?) Et 64 bits sur x86_64. Sur Linux, uint_fast32_t
A 8 octets, la situation est donc différente.
Beaucoup de programmeurs utilisent int
quand ils ont besoin de retourner une petite valeur (disons dans l'intervalle [-32,32]). Cela fonctionnerait parfaitement si int
était une taille d’entier native pour la plate-forme, mais comme elle n’est pas sur les plates-formes 64 bits, un autre type qui correspond au type de plate-forme native constitue un meilleur choix (à moins qu’il soit fréquemment utilisé avec d’autres entiers de taille plus petite).
Fondamentalement, indépendamment de ce que dit la norme, uint_fast32_t
Est cassé sur certaines implémentations de toute façon. Si vous vous souciez des instructions supplémentaires générées à certains endroits, vous devez définir votre propre type entier "natif". Ou vous pouvez utiliser size_t
À cette fin, car il correspond généralement à native
size (je n'inclus pas les anciennes et obscures plates-formes comme 8086, mais uniquement celles qui peuvent fonctionner sous Windows, Linux, etc.).
"Une règle de promotion des nombres entiers" est un autre signe indiquant que int
était supposé être un type natif entier. La plupart des processeurs ne peuvent effectuer des opérations que sur des processus natifs. Par conséquent, les processeurs 32 bits ne peuvent généralement effectuer que des additions, des soustractions, etc. 32 bits (les processeurs Intel sont une exception à cet égard). Les types entiers d'autres tailles ne sont pris en charge que par les instructions de chargement et de stockage. Par exemple, la valeur de 8 bits doit être chargée avec l'instruction "charger 8 bits signé" ou "charger 8 bits non signés" appropriée et étendra la valeur à 32 bits après le chargement. Sans règle de promotion entière, les compilateurs C devraient ajouter un peu plus de code pour les expressions utilisant des types plus petits que le type natif. Malheureusement, cela ne tient plus avec les architectures 64 bits, car les compilateurs doivent maintenant émettre des instructions supplémentaires dans certains cas (comme indiqué ci-dessus).
Pour des raisons pratiques, uint_fast32_t
est complètement inutile. Il est défini de manière incorrecte sur la plate-forme la plus répandue (x86_64) et n'offre aucun avantage ailleurs que si vous avez un compilateur de très basse qualité. Sur le plan conceptuel, il n’a jamais de sens d’utiliser les types "rapides" dans les structures/tableaux de données - les économies que vous réalisez grâce à un type plus efficace à exploiter seront réduites au minimum (coût de l’absence de cache, etc.) en augmentant la taille du fichier. votre ensemble de données de travail. Et pour les variables locales individuelles (compteurs de boucle, temps, etc.), un compilateur non-jouet peut simplement travailler avec un type plus grand dans le code généré si cela est plus efficace, et ne tronquer à la taille nominale que si cela est nécessaire pour la correction (et avec types signés, ce n'est jamais nécessaire).
La seule variante théoriquement utile est uint_least32_t
, lorsque vous devez pouvoir stocker une valeur 32 bits, mais que vous souhaitez être portable sur des ordinateurs dépourvus du type 32 bits de taille exacte. Dans la pratique, toutefois, ce n’est pas un sujet de préoccupation.
Dans de nombreux cas, lorsqu'un algorithme fonctionne sur un tableau de données, le meilleur moyen d'améliorer les performances est de minimiser le nombre d'erreurs dans le cache. Plus chaque élément est petit, plus il peut y en avoir dans le cache. C’est pourquoi beaucoup de code est encore écrit pour utiliser des pointeurs 32 bits sur des ordinateurs 64 bits: ils n’ont besoin de rien de près de 4 GiB de données, mais les pointeurs et les décalages ont besoin de huit octets au lieu de quatre, ce qui est considérable.
Il existe également des ABI et des protocoles spécifiés pour requérir exactement 32 bits, par exemple les adresses IPv4. C'est ce que uint32_t
signifie vraiment: utilisez exactement 32 bits, que cela soit efficace ou non sur le processeur. Celles-ci étaient déclarées comme long
ou unsigned long
, qui a causé beaucoup de problèmes lors de la transition 64 bits. Si vous avez simplement besoin d’un type non signé qui contienne des nombres allant jusqu’à 2³²-1, c’est la définition de unsigned long
depuis la publication du premier standard C. En pratique cependant, suffisamment de code ancien supposait qu'un long
pouvait contenir n'importe quel pointeur, offset ou horodatage de fichier, et suffisamment de code ancien supposait qu'il avait exactement 32 bits de large, que les compilateurs ne pouvaient pas forcément faire long
identique à int_fast32_t
sans casser trop de choses.
En théorie, il serait plus sûr pour un programme d’utiliser uint_least32_t
, et peut-être même charger uint_least32_t
éléments dans un uint_fast32_t
variable pour les calculs. Une implémentation qui n'avait pas uint32_t
type pourrait même se déclarer conforme formellement à la norme! (Il ne pourrait tout simplement pas compiler de nombreux programmes existants.) En pratique, il n’ya plus d’architecture où int
, uint32_t
, et uint_least32_t
ne sont pas les mêmes et aucun avantage, actuellement, à la performance de uint_fast32_t
. Alors pourquoi compliquer les choses?
Pourtant, regardez la raison pour laquelle tous les 32_t
Les types _ devaient exister quand nous avions déjà long
, et vous verrez que ces hypothèses nous ont déjà explosé. Votre code pourrait bien finir par s'exécuter un jour sur une machine où les calculs 32 bits en largeur exacte sont plus lents que la taille native de Word, et il aurait été préférable d'utiliser uint_least32_t
pour le stockage et uint_fast32_t
pour le calcul religieux. Ou si vous traversez ce pont quand vous y arrivez et que vous voulez quelque chose de simple, il y a unsigned long
.
Pour donner une réponse directe: je pense que la vraie raison pour laquelle uint32_t
est utilisé sur uint_fast32_t
ou uint_least32_t
est simplement qu’il est plus facile de taper, et, en raison de sa taille plus courte, beaucoup plus agréable à lire: Si vous faites des structs avec certains types, et que certains d’entre eux sont uint_fast32_t
ou similaire, alors il est souvent difficile de bien les aligner avec int
ou bool
ou d’autres types en C, qui sont assez courts (exemple: char
contre character
). Bien sûr, je ne peux pas confirmer cela avec des données fiables, mais les autres réponses ne peuvent que deviner la raison.
Quant aux raisons techniques de préférer uint32_t
, Je ne pense pas qu'il y en ait - quand vous avez absolument besoin un entier 32 bits non signé exact, alors ce type est votre seulement choix standardisé. Dans presque tous les autres cas, les autres variantes sont techniquement préférables - en particulier, uint_fast32_t
si vous êtes préoccupé par la vitesse et uint_least32_t
si vous êtes préoccupé par l'espace de stockage. En utilisant uint32_t
dans tous les cas, il risque de ne pas pouvoir compiler car le type n’est pas obligatoirement existant.
En pratique, le uint32_t
et les types associés existent sur toutes les plateformes actuelles, à l'exception de très rares (de nos jours) DSP ou implémentations de blagues, de sorte que l'utilisation du type exact présente peu de risques. De même, même si vous pouvez rencontrer des pénalités de vitesse avec les types à largeur fixe, elles ne sont plus (sur les processeurs modernes) affaiblies.
C'est pourquoi, je pense, le type le plus court l'emporte simplement dans la plupart des cas, en raison de la paresse du programmeur.