web-dev-qa-db-fra.com

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

Il y a quelque temps, mon ancien professeur a publié ce code en disant que c'était 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 méthode est légèrement meilleure que la boucle for. Pourquoi ai-je besoin de l'opérateur de décalage gauche? Pourquoi ai-je besoin d'un autre tableau de long? Je ne comprends rien de 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);
}
60

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

Ensuite, il veut traiter un tableau de courts métrages comme un tableau de longs, afin qu'il puisse configurer 100 valeurs 16 bits en ne faisant que 25 itérations de boucle au lieu de 100.

C'est la façon dont votre professeur pense, mais comme d'autres l'ont dit, cette distribution est un comportement indéfini.

69
Paweł Dymowski

Il existe de nombreuses façons 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'utiliserais (après #include <algorithm> et #include <iterator>):

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

Vous ne devez 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 Compiler Explorer de Matt Godbolt est un très bon outil si vous êtes prêt à apprendre un peu d'assembleur. Comme vous pouvez le voir sur ici , les compilateurs peuvent optimiser l'appel fill à douze (et un peu) magasins 128 bits avec toutes les boucles déroulées. Comme les compilateurs connaissent l'environnement cible, ils peuvent le faire sans encoder d'hypothèses spécifiques à la cible dans le code source.

77
CB Bailey

Quelle charge absolue de bain de bouche.

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

  2. Le comportement de déréférencement B suivant long *B = (long*)A; n'est pas dé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 pour l'hypothèse qu'un long est quatre fois plus grand qu'un short.

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

46
Bathsheba

La question a la balise C++ (pas de balise C), donc cela devrait être fait 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);

Le professeur essaie également de déjouer le compilateur qui peut faire des choses époustouflantes. Il est inutile de faire de telles astuces car le compilateur peut le faire pour vous s'il est correctement configuré:

Comme vous pouvez le voir, le -O2 le code de résultat est (presque) le même pour chaque version. En cas de -O1, les astuces apportent quelques améliorations.

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

  • Écrivez du code difficile à lire et n'utilisez pas les optimisations du compilateur
  • Écrivez du code lisible et utilisez -O2

Utilisez le site Godbolt pour expérimenter avec d'autres compilateurs et configurations. Voir aussi la dernière conférence cppCon .

22
Marek R

Comme expliqué par d'autres réponses, le code viole 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 une manière correcte 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 les tailles des objets ont été corrigées à l'aide 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 probablement préférable de garder votre code simple et de laisser le compilateur faire sa magie à la place.


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

Le but est de remplir un entier plus grand avec plusieurs copies du plus petit entier. Si vous écrivez une valeur sur deux octets s dans un grand entier l, alors shift les bits restants pour deux octets (ma version fixe devrait être plus claire sur l'endroit où ceux-ci les nombres magiques viennent), vous aurez alors 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 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 qui contient un tableau des entiers à deux octets, une seule copie définira la valeur de plusieurs objets sur la valeur des octets de l'objet plus grand. Puisque chaque paire d'octets a la même valeur, il en sera 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. Uniquement un tableau de short.

8
eerorika

Le code que votre professeur vous a montré est un programme mal formé, aucun diagnostic requis, car il viole une exigence selon laquelle les pointeurs pointent réellement vers la chose qu'ils prétendent être pointés (autrement connu sous le nom de "strict aliasing").

À titre d'exemple concret, un compilateur peut analyser 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 la norme C++, comme ne pouvant pas modifier A dans un programme bien formé.

Une boucle for(;;) ou même une plage à distance est susceptible d'être optimisée jusqu'à l'initialisation statique de A. Le code de votre professeur, sous un compilateur d'optimisation, sera optimisé pour un comportement indéfini.

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

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

et maintenant:

int main() {

  auto A = make_filled_array<short, 100, 3>();

  std::cout << "\n";
  print(A.data(),100);
}

crée le tableau rempli au moment de la compilation, sans boucles impliquées.

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

C'est cependant exagéré (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é ici, cette logique entraînerait un comportement indéfini.

S'il s'agit de réduire les itérations, le déroulement des boucles permet de réduire le nombre d'itérations. Mais ce ne sera pas beaucoup plus rapide pour des baies aussi 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);
}
3
nyemul