59 votes

Initialiser tous les éléments d'un tableau au même nombre

Il y a quelque temps, mon ancien professeur a posté ce code en disant que c'était une autre façon d'initialiser un tableau au même nombre (autre que zéro bien sûr).

Trois dans ce cas.

Il a déclaré que cette méthode était légèrement meilleure que celle de la for boucle. Pourquoi ai-je besoin de l'opérateur de décalage vers la gauche ? Pourquoi ai-je besoin d'un autre tableau de long ? Je ne comprends rien à ce qui se passe ici.

int main() {

    short int A[100];

    long int v = 3;
    v = (v << 16) + 3;
    v = (v << 16) + 3;
    v = (v << 16) + 3;
    long *B = (long*)A;

    for(int i=0; i<25; i++)
        B[i] = v;

    cout << endl;
    print(A,100);
}

18 votes

Je ne sais pas pourquoi il est censé être légèrement meilleur puisqu'il semble avoir comportement indéfini . S'agit-il d'une amélioration des performances ?

0 votes

Votre professeur semble supposer que long est 4 fois plus grand que le short int - ce qui n'est en aucun cas garanti par la norme (et donc erroné sur certains compilateurs, par exemple : MSVC)

33 votes

Notez que v se termine par 0x3000000300000030000003L en supposant qu'il ne déborde pas d'abord. Et ce débordement n'est que le premier problème de ce code - ce professeur devrait no enseigner le C++.

76voto

Charles Bailey Points 244082

Il existe de nombreuses façons de remplir un tableau avec la même valeur, et si vous vous préoccupez des performances, vous devez les mesurer.

Le C++ dispose d'une fonction spécifique pour remplir un tableau avec une valeur, et je l'utiliserais (après la fonction #include <algorithm> y #include <iterator> ) :

std::fill(std::begin(A), std::end(A), 3);

Il ne faut pas sous-estimer ce que les compilateurs optimisants peuvent faire avec ce genre de choses.

Si vous souhaitez voir ce que fait le compilateur, vous pouvez consulter l'article de Matt Godbolt intitulé Explorateur de compilateur est un très bon outil si vous êtes prêt à apprendre un peu d'assembleur. Comme vous pouvez le voir dans aquí Les compilateurs peuvent optimiser la fill appel à douze (et un peu plus) magasins de 128 bits avec toutes les boucles déroulées. Comme les compilateurs connaissent l'environnement cible, ils peuvent le faire sans encoder dans le code source des hypothèses spécifiques à la cible.

10 votes

Pourquoi avez-vous fait le tableau extern dans votre exemple chez Godbolt ? Il en résulte que GCC fait beaucoup de choses non pertinentes pour le cas où A n'est pas correctement aligné, ce qui fait que votre point de vue n'est pas vraiment évident. Pourquoi ne pas simplement supprimer le extern mot-clé ?

67voto

Paweł Dymowski Points 830

Il part du principe que long est quatre fois plus long que le short (ce qui n'est pas garanti ; il devrait utiliser int16_t et int64_t).

Il prend cet espace mémoire plus long (64 bits) et le remplit avec quatre valeurs courtes (16 bits). Il configure les valeurs en décalant les bits de 16 espaces.

Il souhaite ensuite traiter un tableau de shorts comme un tableau de longs, de manière à pouvoir définir 100 valeurs de 16 bits en effectuant seulement 25 itérations de boucle au lieu de 100.

C'est ce que pense votre professeur, mais comme d'autres l'ont dit, ce casting est un comportement non défini.

24 votes

D'un point de vue technique, la distribution elle-même est tout à fait satisfaisante. C'est l'accès par le pointeur de mauvais type qui a un problème avec UB.

0 votes

Bien qu'il s'agisse techniquement d'UB, tout compilateur qui utilise des shorts 16 bits et des longs 64 bits et pour lequel cela ne fonctionne pas exactement comme prévu est considéré comme cassé.

20 votes

@LeeDanielCrocker Vous pouvez considérer ce que vous voulez, bien sûr. Mais la spécification est la spécification, et les compilateurs font souvent des choses que vous pourriez considérer comme surprenantes ou cassées lorsqu'il s'agit d'un comportement non défini.

44voto

Bathsheba Points 23209

C'est une véritable foutaise.

  1. Pour commencer, v sera calculée à temps de compilation .

  2. Le comportement du déréférencement B suivants long *B = (long*)A; est indéfinie car les types ne sont pas liés. B[i] est un déréférencement de B .

  3. Il n'y a aucune justification à l'hypothèse selon laquelle un long est quatre fois plus grand qu'un short .

Utiliser un for de manière simple et faire confiance au compilateur pour l'optimisation. S'il vous plaît, avec du sucre sur le dessus.

4 votes

Techniquement, ce n'est pas la création de B qui est un comportement indéfini, c'est l'accès à travers elle.

0 votes

@MartinBonner : Correct (comme toujours ;-)), ces choses sont importantes. J'ai modifié.

21voto

Marek R Points 4503

La question comporte la balise C++ (pas de balise C), ce qui signifie qu'elle doit être réalisée dans le style C++ :

// C++ 03
std::vector<int> tab(100, 3);

// C++ 11
auto tab = std::vector<int>(100, 3);
auto tab2 = std::array<int, 100>{};
tab2.fill(3);

De plus, le professeur essaie d'être plus malin que le compilateur qui peut faire des choses époustouflantes. Il n'y a aucun intérêt à faire de tels tours puisque le compilateur peut le faire pour vous s'il est configuré correctement :

Comme vous pouvez le constater, le -O2 Le code de résultat est (presque) le même pour chaque version. Dans le cas de -O1 Les astuces permettent d'améliorer la situation.

En fin de compte, vous devez faire un choix :

  • Écrire un code difficile à lire et ne pas utiliser les optimisations du compilateur
  • Écrire un code lisible et utiliser -O2

Utilisez le site Godbolt pour expérimenter d'autres compilateurs et configurations. Voir aussi le dernier discours de la cppCon .

3 votes

+1 pour Donc, en fin de compte [...] . En tant que personne non débutante et non professionnelle, le code de l'OP est très difficile à comprendre. Il peut fonctionner comme un fait amusant (si c'est le cas, vu les autres réponses), mais le message est obscur, et il ne devrait pas être présenté comme "une autre méthode", mais comme un "fait amusant", explicitement.

7voto

user2079303 Points 4916

Comme expliqué dans d'autres réponses, le code viole les règles d'aliasing de type et fait des suppositions qui ne sont pas garanties par la norme.

Si vous souhaitiez vraiment effectuer cette optimisation à la main, il s'agirait d'une méthode correcte au comportement bien défini :

long v;
for(int i=0; i < sizeof v / sizeof *A; i++) {
    v = (v << sizeof *A * CHAR_BIT) + 3;
}

for(int i=0; i < sizeof A / sizeof v; i++) {
    std:memcpy(A + i * sizeof v, &v, sizeof v);
}

Les hypothèses peu sûres concernant la taille des objets ont été fixées par l'utilisation de sizeof et la violation de l'aliasing a été corrigée par l'utilisation de std::memcpy qui a un comportement bien défini quel que soit le type sous-jacent.

Cela dit, il est probablement préférable de garder votre code simple et de laisser le compilateur faire sa magie.


Pourquoi ai-je besoin de l'opérateur de gauche ?

Il s'agit de remplir un entier plus grand avec plusieurs copies de l'entier plus petit. Si vous écrivez une valeur de deux octets s en un grand nombre entier l alors changement les bits restants pour deux octets (ma version corrigée devrait être plus claire sur l'origine de ces nombres magiques) alors vous aurez un entier avec deux copies des octets qui constituent la valeur s . Cette opération est répétée jusqu'à ce que toutes les paires d'octets dans l sont fixés à ces mêmes valeurs. Pour effectuer le décalage, vous avez besoin de l'opérateur de décalage.

Lorsque ces valeurs sont copiées sur un tableau contenant un tableau d'entiers de deux octets, une seule copie fixe la valeur de plusieurs objets à la valeur des octets de l'objet le plus grand. Comme chaque paire d'octets a la même valeur, il en va de même pour les plus petits entiers du tableau.

Pourquoi ai-je besoin d'un autre tableau de long ?

Il n'y a pas de tableaux de long . Seul un tableau de short .

1 votes

Est-ce vraiment le cas ? Strictement parlant, je pense qu'il n'y a aucune garantie que la taille d'une position longue soit un multiple de la taille d'une position courte. De plus, les longs pourraient être stockés en little endian, alors que les longs sont en long endian (c'est fou, mais...). Copier les bits sous-jacents de cette manière pourrait aboutir à une représentation de piège pour les valeurs courtes. Je pense donc que cette approche est toujours UB. Peut-être que la solution la plus sûre serait d'utiliser short int a[4] au lieu de long ?

1 votes

@chi bon point. On pourrait utiliser des entiers de taille fixe, ce qui permettrait de ne pas compiler lorsqu'ils ne sont pas disponibles. Votre suggestion serait également sûre et fonctionnerait sur tous les systèmes ésotériques. Les tableaux devraient probablement être complétés par alignas . Votre suggestion me donne une idée (probablement stupide) : Ecrire deux valeurs dans le tableau de sortie, puis copier ces deux valeurs, puis en copier quatre ... ce qui donnerait assez peu d'appels à memcpy (log N) tout en évitant de mettre en place un grand réseau de "sources". Cette boucle devrait cependant être développée à la compilation pour avoir une chance d'être efficace

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X