4
votes

L'utilisation de `size_t` pour les longueurs a un impact sur les optimisations du compilateur?

En lisant cette question , j'ai vu le premier commentaire disant que:

size_t pour la longueur n'est pas une bonne idée, les types appropriés sont ceux signés pour des raisons d'optimisation / UB.

suivi d'un autre commentaire appuyant le raisonnement. Est-ce vrai?

La question est importante, car si je devais écrire par exemple une bibliothèque de matrices, les dimensions de l'image pourraient être size_t , juste pour éviter de vérifier si elles sont négatives. Mais alors toutes les boucles utiliseraient naturellement size_t . Cela pourrait-il avoir un impact sur l'optimisation?


14 commentaires

Beaucoup de gens, y compris branje stroustrup, estiment que l'utilisation d'une taille non signée était une erreur. Je suis d'accord avec cela et j'aime utiliser ptrdiff_t comme taille de conteneur.


Vous pouvez simplement nommer un alias pour votre type de taille (ex. en utilisant my_size_type = std :: size_t; ). Cela vous permet de changer facilement le type que vous utilisez. Vous pouvez alors mesurer les performances des deux. Veillez simplement à ne pas faire d'hypothèses sur my_size_type dans votre code.


Durée de quoi? Si vous utilisez un conteneur standard, vous devez utiliser leur size_type . Si vous mesurez la taille de l'objet, vous devez utiliser le type qui donne sizeof . Et ainsi de suite.


Lorsque les optimisations pertinentes ne parviennent pas à faire la différence (d'après mon expérience), la différence est généralement négligeable. Je suis d'accord avec @SergeyA en ce sens que j'utilise généralement tout type correspondant à ce avec quoi ma classe interagira probablement le plus. Bien que d'autres aient pu avoir des expériences différentes.


Je considère VTC comme «basé sur l'opinion principale» ... Y at-il une raison pour laquelle je ne devrais pas?


@ FrançoisAndrieux dans un sens strict, oui. Mais cela semble devenir basé sur l'opinion une fois que vous dépassez le simple oui, pensez-vous? Je ne suis pas sûr cependant, c'est pourquoi je ne vote pas encore.


@NathanOliver Je ne trouve pas de citation de Stroustrup disant cela. Avez-vous un pointeur / lien / détail?


@SergeyA La question était à l'origine étiquetée C également. Si j'écris une bibliothèque, y a-t-il une raison pour ne pas utiliser size_t pour les longueurs? D'après vos commentaires et la réponse, il me semble que la réponse est: "non".


cette question vous permet d'obtenir de nombreuses informations générales et connexes.


@ FrançoisAndrieux Merci!


@NathanOliver avez-vous de bons liens sur le sujet? À propos de la taille non signée étant une erreur, c'est-à-dire.


Jetez un oeil à stackoverflow.com/questions/49782609/... Je maintiens mon commentaire.


@Ramon et l'OP: youtube.com/watch?v=Puio5dly9N8#t= 42 min 40 s


Sensationnel. Regardez ces jeunes punks.


3 Réponses :


2
votes

Ce commentaire est tout simplement faux. Lorsque vous travaillez avec des opérandes natifs de la taille d'un pointeur sur une architecture raisonnable, il n'y a aucune différence au niveau de la machine entre les décalages signés et non signés , et donc pas de place pour qu'ils aient des propriétés de performances différentes.

Comme vous l'avez noté, l'utilisation de size_t a quelques propriétés intéressantes comme ne pas avoir à tenir compte de la possibilité qu'une valeur puisse être négative (bien que la comptabilisation puisse être aussi simple que d'interdire cela dans votre contrat d'interface). Cela garantit également que vous pouvez gérer n'importe quelle taille demandée par un appelant en utilisant le type standard pour les tailles / nombres, sans troncature ni vérification des limites. D'un autre côté, cela empêche d'utiliser le même type pour les décalages d'index lorsque le décalage peut avoir besoin d'être négatif, et à certains égards, il est difficile d'effectuer certains types de comparaisons (vous devez les écrire disposés algébriquement de sorte qu'aucun côté ne soit négatif), mais le même problème se pose lors de l'utilisation de types signés, en ce sens que vous devez effectuer des réarrangements algébriques pour vous assurer qu'aucune sous-expression ne peut déborder.

En fin de compte, vous devez d'abord toujours utiliser le type qui vous convient sémantiquement , plutôt que d'essayer de choisir un type pour les propriétés de performance. Ce n'est que s'il existe un problème de performances mesurées sérieux qui semble pouvoir être amélioré par des compromis impliquant le choix des types que vous devriez envisager de les modifier.


4 commentaires

Les entiers unsigned ont des garanties supplémentaires et moins de causes de comportement indéfini. Cela peut gêner le compilateur. Avec int , le compilateur peut supposer qu'il ne déborde jamais et agir comme s'il continuait indéfiniment car s'il déborde , vous avez UB et tout ce qui se passe est autorisé. Avec des entiers unsigned , le compilateur doit s'assurer que le comportement de bouclage est préservé.


Cette réponse est complètement fausse. Avez-vous vérifié à quoi ressemblait le code généré? Il n'y a pas de différence au niveau machine, il y a une différence au niveau C ++ et donc sur le comportement du compilateur.


@ FrançoisAndrieux: C'est vrai mais cela n'aura aucune pertinence pratique dans le cas du PO.


Voir également la réponse de Matteo qui convient que la performance n'est pas un problème important ici.



6
votes

size_t être non signé est principalement un accident historique - si votre monde est de 16 bits, passer de 32767 à 65535 taille maximale d'objet est une grande victoire; dans l'informatique grand public actuelle (où 64 et 32 ​​bits sont la norme), le fait que size_t ne soit pas signé est généralement une nuisance.

Bien que les types non signés aient moins undefined comportement (car le wraparound est garanti), le fait qu'ils aient principalement une sémantique "bitfield" est souvent la cause de bogues et autres mauvaises surprises; en particulier:

  • la différence entre les valeurs non signées est également non signée, avec la sémantique habituelle, donc si vous vous attendez à une valeur négative, vous devez effectuer un cast au préalable;

    int sum_arr(int *arr, unsigned len) {
        int ret = 0;
        for(unsigned i = 0; i < len; ++i) {
            ret += arr[i];
        }
        return ret;
    }
    
    // compiles successfully and overflows the array; it len was signed,
    // it would just return 0
    sum_arr(some_array, -10);
    
  • plus en général, dans les comparaisons signées / non signées et les opérations mathématiques non signées gagne (donc la valeur signée est convertie implicitement en non signée) ce qui, encore une fois, conduit à des surprises;

    // This works fine if T is signed, loops forever if T is unsigned
    for(T idx = c.size() - 1; idx >= 0; idx--) {
        // ...
    }
    
  • dans des situations courantes (par exemple, une itération vers l'arrière), la sémantique non signée est souvent problématique, car vous aimeriez que l'index devienne négatif pour la condition aux limites

    unsigned a = 10;
    int b = -2;
    if(a < b) std::cout<<"a < b\n"; // prints "a < b"
    

De plus, le fait qu'une valeur non signée ne puisse pas prendre une valeur négative est surtout un homme de paille; vous pouvez éviter de vérifier les valeurs négatives, mais en raison de conversions implicites signées-non signées, cela n'arrêtera aucune erreur - vous ne faites que rejeter le blâme. Si l'utilisateur transmet une valeur négative à votre fonction de bibliothèque en prenant un size_t , cela deviendra juste un très grand nombre, ce qui sera tout aussi faux sinon pire.

unsigned a = 10, b = 20;
// prints UINT_MAX-10, i.e. 4294967286 if unsigned is 32 bit
std::cout << a-b << "\n"; 


4 commentaires

Ça peut. Le compilateur déroulera des boucles avec des entiers signés, et non avec des non signés, générera SIMD. C'est une GRANDE différence. Ce n'est pas surfaite.


Avec -Wsign-conversion (fortement recommandé), des codes tels que sum_arr (some_array, -10); généreront un avertissement. Je pense que les conversions implicites signées / non signées sont mauvaises, donc j'active toujours cet avertissement. Chaque fois que je veux une conversion signée / non signée, j'utilise un cast explicite.


@MatthieuBrucher Je ne le dirais pas gcc.godbolt.org/z/jw4qIT déroulé et SIMDed juste le même. Si votre compilateur est mauvais, ce n'est pas la faute de unsigned .


@MatthieuBrucher votre commentaire semble être basé sur un seul compilateur (Clang) qui est connu pour son hostilité envers les nombres non signés. Votre propre exemple ci-dessous montre le même codegen avec gcc et icc.



1
votes

Je maintiens mon commentaire.

Il y a un moyen simple de vérifier ceci: vérifier ce que le compilateur génère.

void test1(double* data, size_t size)
{
    for(size_t i = 0; i < size; i += 4)
    {
        data[i] = 0;
    }
}

void test2(double* data, int size)
{
    for(int i = 0; i < size; i += 4)
    {
        data[i] = 0;
    }
}

Alors, que génère le compilateur? Je m'attendrais à un déroulement de boucle, SIMD ... pour quelque chose d'aussi simple:

Vérifions godbolt.

Eh bien, la version signée a un déroulement, SIMD, pas la version non signée.

Je ne vais pas montrer de référence, car dans cet exemple, le goulot d'étranglement va être sur l'accès à la mémoire, pas sur le calcul du processeur. Mais vous voyez l'idée.

Deuxième exemple, gardez simplement le premier devoir:

void test1(double* data, size_t size)
{
    for(size_t i = 0; i < size; i += 4)
    {
        data[i] = 0;
        data[i+1] = 1;
        data[i+2] = 2;
        data[i+3] = 3;
    }
}

void test2(double* data, int size)
{
    for(int i = 0; i < size; i += 4)
    {
        data[i] = 0;
        data[i+1] = 1;
        data[i+2] = 2;
        data[i+3] = 3;
    }
}

Comme vous voulez gcc

OK, pas aussi impressionnant que pour clang, mais il génère quand même un code différent.


8 commentaires

Les deux gcc et icc génèrent un code identique (au moins quand on regarde). Le fait que CLang choisisse un codegen différent nous en dit long sur CLang, non signé vs non signé.


ICC est connu pour être très agressif, et parfois trop, même générer du mauvais code. Je vais creuser un peu plus pour gcc.


Il n'y a rien d'agressif à propos de la CPI dans l'exemple fourni, à mon avis. J'ai entendu des discours de gars de LLVM, à mon avis, ils ont pris le mantra «jamais utilisé des entiers non signés» à cœur et ont simplement décidé de faire d'eux des citoyens de seconde zone. Et pour ce qui est du mauvais code , nous construisons depuis longtemps nos builds de production avec ICC. Je n'ai pas encore vu d'exemple de mauvais codegen (comme dans - fonctionne incorrectement).


Ajout d'un autre exemple sur GCC, code généré plus simple, toujours différent. ICC génère également un mauvais code pour std :: sort sur des paires triviales d'entiers ...


Le code est différent, la version non signée est plus courte. Quant aux performances réelles, je n'en ai aucune idée. Qu'entendez-vous par mauvais code? Un code qui produit des résultats que Standard ne prescrit pas? Pouvez-vous partager l'exemple?


Si quoi que ce soit, dans votre deuxième exemple, la version non signée est légèrement meilleure (et est légèrement plus courte, ce qui ne fait jamais de mal pour l'utilisation de l'i-cache), bien que je parie qu'elles fonctionneront à peu près de la même manière. Mais encore une fois, le fait est que généralement la différence de performance est inférieure au seuil de bruit, donc cela ne devrait pas être un facteur décisif - l'exactitude et la facilité d'utilisation sont bien plus importantes. Vous pouvez toujours passer un après-midi à optimiser une boucle spécifique si elle est réellement plus lente que vous ne le souhaiteriez.


Malheureusement, non, je ne peux pas afficher le code qui fait que icc génère un code erroné ici.


Mais j'ai vu très souvent du mauvais code généré par le compilateur Intel C ++, il est beaucoup moins stable que celui de Fortran. Malheureusement.