Quelle est la différence de performance entre les entiers non signés et signés? [fermé]


42

Je suis conscient de l'impact sur les performances lorsque je mélange des ints signés avec des floats.

Est-ce pire de mélanger des ints non signés avec des flotteurs?

Y at-il un succès lorsque vous mélangez signé / non signé sans float?

Les différentes tailles (u32, u16, u8, i32, i16, i8) ont-elles un effet sur les performances? Sur quelles plateformes?


2
J'ai supprimé le texte / tag spécifique à la PS3, car il s'agit d'une bonne question à propos de n'importe quelle architecture et la réponse est vraie pour toutes les architectures qui séparent des registres à nombres entiers et à virgule flottante, ce qui correspond à la quasi-totalité d'entre eux.

Réponses:


36

La pénalité importante due au mélange des ints (de tout type) et des floats est due au fait que ceux-ci sont dans des ensembles de registres différents. Pour passer d'un jeu de registres à un autre, vous devez écrire la valeur en mémoire et la relire, ce qui entraîne une perte de charge .

Le fait de passer de tailles différentes ou de signer des tailles conserve tout dans le même jeu de registres afin d’éviter les lourdes pénalités. Il peut y avoir de plus petites pénalités dues aux extensions de panneau, etc., mais elles sont beaucoup plus petites qu’un chargement-hit-store.


L'article que vous avez lié indique que le processeur de cellule PS3 est une exception à cette règle car, apparemment, tout est stocké dans le même ensemble de registres (se trouve approximativement au milieu de l'article ou recherchez "Cell").
Bummzack

4
@bummzack: cela s'applique uniquement aux SPE, pas aux EPI; les SPE ont un environnement très spécial, en virgule flottante, et la distribution est encore relativement chère. En outre, les coûts sont toujours les mêmes pour les entiers signés par rapport aux entiers non signés.

C'est un bon article et il est important de connaître LHS (et je le vote pour cela), mais ma question concerne les pénalités relatives aux panneaux. Je sais que ces chiffres sont petits et probablement négligeables, mais j'aimerais quand même voir de vrais chiffres ou références à leur sujet.
Luis le

1
@ Luis - J'essayais de trouver une documentation publique à ce sujet mais je ne la trouve pas pour le moment. Si vous avez accès à la documentation de la Xbox 360, il existe un bon livre blanc de Bruce Dawson qui couvre une partie de ce problème (et son très bon en général).
celion le

@ Luis: J'ai posté une analyse ci-dessous, mais si cela vous satisfait, donnez la réponse à Celion - tout ce qu'il a dit est correct, tout ce que j'ai fait est de lancer GCC à quelques reprises.

12

Je soupçonne que les informations sur la Xbox 360 et la PS3 en particulier seront cachées derrière des murs réservés aux développeurs licenciés, comme la plupart des détails de bas niveau. Cependant, nous pouvons construire un programme x86 équivalent et le désassembler pour avoir une idée générale.

Voyons d’abord ce que l’élargissement des coûts non signés entraîne:

unsigned char x = 1;
unsigned int y = 1;
unsigned int z;
z = x;
z = y;

La partie pertinente se désassemble en (utilisant GCC 4.4.5):

    z = x;
  27:   0f b6 45 ff             movzbl -0x1(%ebp),%eax
  2b:   89 45 f4                mov    %eax,-0xc(%ebp)
    z = y;
  2e:   8b 45 f8                mov    -0x8(%ebp),%eax
  31:   89 45 f4                mov    %eax,-0xc(%ebp)

Donc, fondamentalement, la même chose - dans un cas, nous déplaçons un octet, dans l'autre, nous déplaçons un mot. Prochain:

signed char x = 1;
signed int y = 1;
signed int z;
z = x;
z = y;

Se transforme en:

   z = x;
  11:   0f be 45 ff             movsbl -0x1(%ebp),%eax
  15:   89 45 f4                mov    %eax,-0xc(%ebp)
    z = y;
  18:   8b 45 f8                mov    -0x8(%ebp),%eax
  1b:   89 45 f4                mov    %eax,-0xc(%ebp)

Ainsi, le coût de l’extension du panneau de signalisation est ce qu’il en coûte movsblplutôt que de le faire movzbl- niveau de sous-instruction. C'est fondamentalement impossible à quantifier sur les processeurs modernes en raison de la façon dont les processeurs modernes fonctionnent. Tout le reste, qu'il s'agisse de la vitesse de la mémoire, de la mise en cache ou de ce qui était prévu dans le pipeline, va dominer l'exécution.

Après environ 10 minutes d'écriture de ces tests, j'aurais facilement pu trouver un véritable problème de performances. Dès que j'active l'optimisation du compilateur, le code devient méconnaissable pour des tâches aussi simples.

Ce n'est pas un débordement de pile, j'espère donc que personne ici ne dira que la microoptimisation n'a pas d'importance. Les jeux fonctionnent souvent avec des données très volumineuses et très numériques. Par conséquent, une attention particulière aux branches, aux diffusions, à la planification, à l'alignement de la structure, etc. peut donner lieu à des améliorations très critiques. Quiconque a passé beaucoup de temps à optimiser le code PPC a probablement au moins une histoire d'horreur à propos de load-hit-store. Mais dans ce cas, ce n'est pas grave. La taille de stockage de votre type entier n'affecte pas les performances, tant qu'elle est alignée et s'inscrit dans un registre.


2
(CW, car il ne s'agit en réalité que d'un commentaire sur la réponse de celion, et parce que je suis curieux de savoir quelles modifications pourraient être apportées au code pour le rendre plus illustratif.)

Les informations sur le processeur de la PS3 sont facilement et légalement disponibles. Par conséquent, la discussion sur les ressources du processeur relatives à la PS3 ne pose pas de problème. Jusqu'à ce que Sony supprime le support OtherOS, n'importe qui pouvait coller Linux sur une PS3 et le programmer. Le GPU était hors limites, mais le CPU (y compris les SPE) va bien. Même sans le support OtherOS, vous pouvez facilement récupérer le code GCC approprié et voir à quoi ressemble le code-gen.
JasonD

@Jason: J'ai signalé mon message comme CW, donc si quelqu'un le fait, il peut fournir les informations. Cependant, toute personne ayant accès au compilateur GameOS officiel de Sony - qui est vraiment le seul qui compte - est probablement empêchée de le faire.

En réalité, l'entier signé est plus cher sur PPC IIRC. Il a un impact minime sur les performances, mais il est là ... de nombreux détails PPU / SPU pour PS3 sont également disponibles: jheriko-rtw.blogspot.co.uk/2011/07/ps3-ppuspu-docs.html et ici: jheriko-rtw.blogspot.co.uk/2011/03/ppc-instruction-set.html . Curieux de savoir ce qu'est ce compilateur GameOS? Est-ce le compier GCC ou celui de SNC? Les comparaisons signées ont un coût supplémentaire lorsqu'il s'agit d'optimiser les boucles les plus internes. Cependant, je n'ai pas accès aux documents décrivant cela - et même si je le faisais ...
jheriko

4

Les opérations sur les entiers signés peuvent être plus coûteuses sur presque toutes les architectures. Par exemple, la division par une constante est plus rapide quand elle n'est pas signée, par exemple:

unsigned foo(unsigned a) { return a / 1024U; }

va être optimisé pour:

unsigned foo(unsigned a) { return a >> 10; }

Mais...

int foo(int a) { return a / 1024; }

optimisera pour:

int foo(int a) {
  return (a + 1023 * (a < 0)) >> 10;
}

ou sur des systèmes où la ramification est bon marché,

int foo(int a) {
  if (a >= 0) return a >> 10;
  else return (a + 1023) >> 10;
}

Même chose pour le modulo. Ceci est également vrai pour les non-pouvoirs de 2 (mais l'exemple est plus complexe). Si votre architecture ne comporte pas de division matérielle (par exemple, la plupart des processus ARM), les divisions non signées de non-constants sont également plus rapides.

En général, dire au compilateur que les nombres négatifs ne peuvent pas en résulter facilitera l'optimisation des expressions, en particulier celles utilisées pour la terminaison de boucle et d'autres conditions.

En ce qui concerne les tailles de taille différente, oui, il y a un léger impact, mais vous devrez peser cela par rapport à moins de mémoire. De nos jours, vous gagnez probablement plus en accédant à moins de mémoire que vous n'en perdez en augmentant la taille. Vous êtes très loin dans la micro-optimisation à ce stade.


J'ai modifié votre code optimisé pour mieux refléter ce que GCC génère réellement, même sur -O0. Avoir une branche était trompeur quand un test + lea vous permet de le faire sans branche.

2
Sur x86, peut-être. Sur ARMv7, il est simplement exécuté de manière conditionnelle.
John Ripley

3

Les opérations avec int signé ou non signé ont le même coût sur les processeurs actuels (x86_64, x86, powerpc, arm). Sur les processeurs 32bits, u32, u16, u8 s32, s16, s8 devraient être les mêmes. Vous pouvez avoir une pénalité avec un mauvais alignement.

Mais convertir int en float ou float to int est une opération coûteuse. Vous pouvez facilement trouver une implémentation optimisée (SSE2, Neon ...).

Le point le plus important est probablement l’accès à la mémoire. Si vos données ne tiennent pas dans le cache L1 / L2, vous perdrez plus de cycle que de conversion.


2

Jon Purdy dit ci-dessus (je ne peux pas commenter) que unsigned peut être plus lent car il ne peut pas déborder. Je ne suis pas d'accord, l'arithmétique non signée est une arithmétique de moular simple modulo 2 au nombre de bits dans le mot. Les opérations signées peuvent en principe subir des débordements, mais elles sont généralement désactivées.

Parfois, vous pouvez faire des choses intelligentes (mais pas très lisibles), comme empaqueter deux éléments de données ou plus dans un int, et obtenir plusieurs opérations par instruction (arithmétique de poche). Mais tu dois comprendre ce que tu fais. Bien sûr, MMX vous permet de le faire naturellement. Mais parfois, en utilisant la plus grande taille de mot prise en charge par le matériel et en compressant manuellement les données, vous obtenez la mise en œuvre la plus rapide.

Faites attention à l'alignement des données. Sur la plupart des implémentations matérielles, les charges et les magasins non alignés sont plus lents. L'alignement naturel signifie que, pour un mot de 4 octets par exemple, l'adresse est un multiple de quatre et que les adresses de mot sur huit octets doivent être des multiples de huit octets. Cela se répercute sur le SSE (128 bits favorisent un alignement de 16 octets). AVX étendra bientôt ces tailles de registres "vectoriels" à 256 bits, puis à 512 bits. Et les charges / magasins alignés seront plus rapides que ceux non alignés. Pour les geeks matériels, une opération de mémoire non alignée peut couvrir des éléments tels que la ligne de cach et même les limites de page, pour lesquels le matériel doit faire attention.


1

Il est légèrement préférable d'utiliser des entiers signés pour les index de boucle, car le débordement signé n'est pas défini en C, le compilateur supposera donc que ces boucles ont moins de cas de coin. Ceci est contrôlé par "-fstrict-overflow" de gcc (activé par défaut) et l'effet est probablement difficile à remarquer sans lire la sortie de l'assembly.

Au-delà de cela, x86 fonctionne mieux si vous ne mélangez pas de types, car il peut utiliser des opérandes en mémoire. S'il doit convertir des types (signe ou zéro extension), cela signifie un chargement explicite et l'utilisation d'un registre.

Coller avec int pour les variables locales et la plupart de ceci se produira par défaut.


0

Comme le souligne celion, la conversion entre ints et floats a essentiellement pour corollaire la copie et la conversion de valeurs entre registres. La seule surcharge des entrées non signées en elles-mêmes provient de leur comportement enveloppant garanti, ce qui nécessite un certain contrôle de débordement dans le code compilé.

La conversion entre entiers signés et non signés n'est en principe pas onéreuse. Différentes tailles de nombre entier peuvent être (infinitésimalement) plus rapides ou plus lentes à accéder en fonction de la plate-forme. De manière générale, la taille de l’entier la plus proche de la taille des mots de la plate-forme sera la plus rapide à accéder, mais la différence de performances globale dépend de nombreux autres facteurs, notamment la taille du cache: si vous utilisez uniquement uint64_tlorsque vous avez besoin uint32_t, Cependant, moins de vos données vont tenir dans la mémoire cache en même temps, et vous risquez d’engendrer une surcharge de charge.

C'est quand même un peu excessif d'y penser. Si vous utilisez des types adaptés à vos données, les choses doivent parfaitement fonctionner et la quantité de puissance à gagner en sélectionnant des types basés sur l'architecture est néanmoins négligeable.


De quelle vérification de débordement faites-vous allusion? À moins que vous ne vouliez dire un niveau inférieur à celui d’assembleur, le code permettant d’ajouter deux ints est identique sur la plupart des systèmes et pas beaucoup plus longtemps sur les rares systèmes qui utilisent, par exemple, l’indication du signe. Juste différent.

@ JoeWreschnig: Zut. Je n'arrive pas à le trouver, mais je sais que j'ai vu des exemples de différentes sorties d'assembleurs tenant compte d'un comportement enveloppant défini, du moins sur certaines plates-formes. Le seul article lié que j'ai pu trouver: stackoverflow.com/questions/4712315/…
Jon Purdy

Une sortie d'assembleur différente pour un comportement enveloppant différent est due au fait que le compilateur peut effectuer des optimisations dans le cas signé, par exemple si b> 0 alors a + b> a, car le dépassement signé n'est pas défini (et ne peut donc pas être invoqué). C'est vraiment une situation totalement différente.
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.