Initialiser tous les éléments d’un tableau au même numéro

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

Trois dans ce cas.

Il a dit que cette voie est légèrement meilleure que la boucle for . Pourquoi ai-je besoin de l’opérateur de 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); } 

Il suppose que le long est quatre fois plus long que le short (ce 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 de quatre valeurs courtes (16 bits). Il configure les valeurs en décalant les bits de 16 espaces.

Ensuite, il veut traiter un tableau de courts-circuits comme un tableau de longues, donc il peut configurer 100 valeurs de 16 bits en ne faisant que 25 itérations de boucle au lieu de 100.

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

Il existe de nombreuses manières de remplir un tableau avec la même valeur, et si vous êtes préoccupé par les performances, vous devez mesurer.

C ++ a une fonction dédiée pour remplir un tableau avec une valeur, et je l’utilise (après #include et #include ):

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

Vous ne devriez pas sous-estimer ce que l’optimisation des compilateurs peut faire avec quelque chose comme ça.

Si vous êtes intéressé à voir ce que fait le compilateur, alors Comstackr Explorer de Matt Godbolt est un très bon outil si vous êtes prêt à apprendre un peu l’assembleur. Comme vous pouvez le voir ici , les compilateurs peuvent optimiser l’appel de fill à douze (et un peu) stockages 128 bits avec toutes les boucles déroulées. Comme les compilateurs connaissent l’environnement cible, ils peuvent le faire sans coder les hypothèses spécifiques à la cible dans le code source.

Quelle charge absolue de foutaise.

  1. Pour les débutants, v sera calculé au moment de la compilation .

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

  3. Il n’y a aucune justification à l’hypothèse qu’un long soit quatre fois plus grand qu’un short .

Utilisez une boucle for de manière simple et faites confiance au compilateur pour optimiser. Avec un peu de sucre dessus s’il te plaît.

La question a la balise C ++ (pas de balise C), donc cela devrait être fait en style C ++:

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

De plus, le professeur essaie de déjouer le compilateur qui peut faire des choses époustouflantes. Il n’y a aucun intérêt à faire de telles astuces puisque le compilateur peut le faire pour vous s’il est configuré correctement:

  • Vos assemblages de code
  • Vos assemblages de code avec tick enlevé
  • Approche tableau
  • Approche vectorielle

Comme vous pouvez le voir, le code de résultat -O2 est (presque) identique pour chaque version. Dans le cas de -O1 , les astuces donnent des améliorations.

Donc, en fin de compte, vous devez faire un choix:

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

Utilisez le site Godbolt pour expérimenter avec d’autres compilateurs et configurations. Voir aussi la dernière discussion sur cppCon .

Comme l’expliquent d’autres réponses, le code enfreint les règles d’alias de type et émet des hypothèses qui ne sont pas garanties par la norme.

Si vous vouliez vraiment faire cette optimisation à la main, ce serait un moyen correct qui a un 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 non sécurisées concernant la taille des objects ont été corrigées par l'utilisation de sizeof et la violation d'alias a été corrigée à l'aide de std::memcpy , qui a un comportement bien défini, quel que soit le type sous-jacent.

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


Pourquoi j'ai besoin de l'opérateur de gauche?

Le but est de remplir un entier plus grand avec plusieurs copies du plus petit entier. Si vous écrivez une valeur à deux octets s sur un grand nombre entier l , puis déplacez les bits restants pour deux octets (ma version fixe devrait être plus claire sur l'origine de ces nombres magiques), vous aurez alors un entier avec deux copies du octets qui constituent la valeur s . Ceci est répété jusqu'à ce que toutes les paires d'octets dans l soient définies sur ces mêmes valeurs. Pour faire le quart, vous avez besoin de l'opérateur de quart.

Lorsque ces valeurs sont copiées sur un tableau contenant un tableau d'entiers à deux octets, une seule copie définit la valeur de plusieurs objects sur la valeur des octets de l'object plus grand. Puisque chaque paire d'octets a la même valeur, il en sera de même pour les entiers plus petits du tableau.

Pourquoi j'ai besoin d'un autre tableau de long ?

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

Le code que votre enseignant vous a montré est un programme mal formé, aucun diagnostic requirejs, car il enfreint une exigence selon laquelle les pointeurs pointent réellement sur ce qu’ils prétendent être pointé (autrement dit «aliasing ssortingct»).

Comme exemple concret, un compilateur peut parsingr votre programme, remarquer que A n’a pas été écrit directement et qu’aucun short n’a été écrit, et prouver que A n’a jamais été modifié une fois créé.

Tout ce qui dérange avec B peut être prouvé, sous le standard C ++, comme ne pas pouvoir modifier A dans un programme bien formé.

Une boucle for(;;) ou même une range-for est susceptible d’être optimisée jusqu’à l’initialisation statique de A Le code de votre enseignant, sous un compilateur optimisant, optimisera le comportement non défini.

Si vous avez vraiment besoin d’un moyen de créer un tableau initialisé avec une valeur, vous pouvez utiliser ceci:

 template auto index_over(std::index_sequence) { return [](auto&&f)->decltype(auto) { return f( std::integral_constant{}... ); }; } template auto index_upto(std::integral_constant ={}) { return index_over( std::make_index_sequence{} ); } template std::array make_filled_array() { return index_upto()( [](auto...Is)->std::array{ return {{ (void(Is),value)... }}; }); } 

et maintenant:

 int main() { auto A = make_filled_array(); std::cout << "\n"; print(A.data(),100); } 

crée le tableau rempli au moment de la compilation, aucune boucle impliquée.

En utilisant godbolt, vous pouvez voir que la valeur du tableau a été calculée au moment de la compilation et que la valeur 3 a été extraite lorsque j'accède au 50ème élément.

Ceci est cependant excessif (et c ++ 14 ).

Je pense qu’il essaie de réduire le nombre d’itérations de boucle en copiant plusieurs éléments de tableau en même temps. Comme d’autres utilisateurs l’ont déjà mentionné, cette logique conduirait à un comportement indéfini.

S’il s’agit de réduire les itérations, nous pouvons réduire le nombre d’itérations avec le déroulement de la boucle. Mais ce ne sera pas beaucoup plus rapide pour ces baies plus petites.

 int main() { short int A[100]; for(int i=0; i<100; i+=4) { A[i] = 3; A[i + 1] = 3; A[i + 2] = 3; A[i + 3] = 3; } print(A, 100); }