web-dev-qa-db-fra.com

Devrais-je m'inquiéter de l'alignement lors de la conversion du pointeur?

Dans mon projet, nous avons un morceau de code comme ceci: 

// raw data consists of 4 ints
unsigned char data[16];
int i1, i2, i3, i4;
i1 = *((int*)data);
i2 = *((int*)(data + 4));
i3 = *((int*)(data + 8));
i4 = *((int*)(data + 12));

J'ai indiqué à mon responsable technique que ce code n'était peut-être pas portable car il essayait de convertir un unsigned char* en un int* qui nécessite généralement un alignement plus strict. Mais le responsable technique déclare que tout va bien, la plupart des compilateurs conservent la même valeur de pointeur après la conversion et je peux simplement écrire le code de cette manière.

Pour être franc, je ne suis pas vraiment convaincu. Après des recherches, je découvre que certaines personnes s'opposent à l'utilisation de la fonte au pointeur comme ci-dessus, par exemple, ici et ici .

Donc, voici mes questions:

  1. Est-il vraiment prudent de déréférencer le pointeur après l'insertion d'un projet réel?
  2. Existe-t-il une différence entre le casting en style C et reinterpret_cast?
  3. Existe-t-il une différence entre C et C++?
50
Eric Z

1. Est-il vraiment prudent de déréférencer le pointeur après l'insertion d'un projet réel?

Si le pointeur se trouve mal aligné, cela peut poser problème. J'ai personnellement vu et corrigé des erreurs de bus dans le code de production réel, causées par le transtypage d'un char* dans un type plus strictement aligné. Même si vous n'obtenez pas d'erreur évidente, vous pouvez avoir des problèmes moins évidents, tels qu'un ralentissement des performances. Suivre strictement la norme pour éviter les UB est une bonne idée même si vous ne voyez pas immédiatement de problèmes. (Et une règle que le code enfreint est la règle de repliement strict, § 3.10/10 *)

Une meilleure alternative consiste à utiliser std::memcpy() ou std::memmove si les tampons se chevauchent (ou mieux encore bit_cast<>() )

unsigned char data[16];
int i1, i2, i3, i4;
std::memcpy(&i1, data     , sizeof(int));
std::memcpy(&i2, data +  4, sizeof(int));
std::memcpy(&i3, data +  8, sizeof(int));
std::memcpy(&i4, data + 12, sizeof(int));

Certains compilateurs travaillent plus que d'autres pour s'assurer que les tableaux de caractères sont alignés plus strictement que nécessaire, car les programmeurs s'y trompent souvent.

#include <cstdint>
#include <typeinfo>
#include <iostream>

template<typename T> void check_aligned(void *p) {
    std::cout << p << " is " <<
      (0==(reinterpret_cast<std::intptr_t>(p) % alignof(T))?"":"NOT ") <<
      "aligned for the type " << typeid(T).name() << '\n';
}

void foo1() {
    char a;
    char b[sizeof (int)];
    check_aligned<int>(b); // unaligned in clang
}

struct S {
    char a;
    char b[sizeof(int)];
};

void foo2() {
    S s;
    check_aligned<int>(s.b); // unaligned in clang and msvc
}

S s;

void foo3() {
    check_aligned<int>(s.b); // unaligned in clang, msvc, and gcc
}

int main() {
    foo1();
    foo2();
    foo3();
}

http://ideone.com/FFWCjf

2. Existe-t-il une différence entre le casting en style C et reinterpret_cast?

Ça dépend. Les casts de style C font différentes choses selon les types impliqués. Le transtypage de style C entre les types de pointeurs donnera la même chose qu’un reinterpret_cast; Voir § 5.4 Conversion de type explicite (notation transt) et § 5.2.9-11.

3. Y a-t-il une différence entre C et C++?

Il ne devrait pas y en avoir aussi longtemps que vous avez affaire à des types légaux en C.


* Un autre problème est que C++ ne spécifie pas le résultat de la conversion d'un type de pointeur vers un type avec des exigences d'alignement plus strictes. Ceci est destiné aux plates-formes où les pointeurs non alignés ne peuvent même pas être représentés. Cependant, les plates-formes typiques d'aujourd'hui peuvent représenter des pointeurs non alignés et des compilateurs spécifient les résultats d'une telle distribution pour correspondre à vos attentes. En tant que tel, ce problème est secondaire à la violation de crénelage. Voir [expr.reinterpret.cast]/7.

34
bames53

Ce n'est pas bien, vraiment. L'alignement peut être incorrect et le code peut enfreindre un alias strict. Vous devriez le décompresser explicitement.

i1 = data[0] | data[1] << 8 | data[2] << 16 | data[3] << 24;

etc. Ceci est définitivement un comportement bien défini, et en prime, il est également indépendant de l'endianisme, contrairement à votre distribution de pointeur.

27
Puppy

Dans l'exemple que vous montrez ici, ce que vous ferez sera sans danger pour la quasi-totalité des CPU modernes si le pointeur de caractère initial est correctement aligné. En général, cela n’est ni sûr ni garanti de fonctionner.

Si le pointeur de caractères initial n'est pas correctement aligné, cela fonctionnera sur x86 et x86_64, mais peut échouer sur d'autres architectures. Si vous êtes chanceux, il va simplement vous donner un crash et vous corrigerez votre code. Si vous êtes malchanceux, l'accès non aligné sera corrigé par un gestionnaire de pièges dans votre système d'exploitation et vous obtiendrez des performances médiocres sans aucun retour évident sur la raison de sa lenteur c'était un problème énorme sur alpha il y a 20 ans).

Même sur x86 & co, l'accès non aligné sera plus lent.

Si vous voulez être en sécurité aujourd'hui et à l'avenir, il suffit de memcpy au lieu de faire la tâche comme ceci. Un observateur moderne aura probablement des optimisations pour memcpy et agira comme il convient. Sinon, memcpy aura lui-même une détection de l'alignement et fera le plus rapidement possible.

De plus, votre exemple est faux sur un point: sizeof (int) n'est pas toujours 4.

6
Art

La façon correcte de décompresser les données char en mémoire tampon consiste à utiliser memcpy:

unsigned char data[4 * sizeof(int)];
int i1, i2, i3, i4;
memcpy(&i1, data, sizeof(int));
memcpy(&i2, data + sizeof(int), sizeof(int));
memcpy(&i3, data + 2 * sizeof(int), sizeof(int));
memcpy(&i4, data + 3 * sizeof(int), sizeof(int));

La conversion enfreint l'aliasing, ce qui signifie que le compilateur et l'optimiseur sont libres de traiter l'objet source comme non initialisé.

Concernant vos 3 questions:

  1. Non, le déréférencement d'un pointeur de diffusion est généralement dangereux, à cause des alias et de l'alignement.
  2. Non, en C++, le transtypage de style C est défini en termes de reinterpret_cast.
  3. Non, C et C++ s'accordent sur le crénelage basé sur le casting. Il existe une différence dans le traitement du repliement sur l'union (C le permet dans certains cas; C++ ne le permet pas).
4
ecatmur

Mise à jour: J'ai négligé le fait qu'effectivement, les types plus petits peuvent être non alignés par rapport à un plus grand, comme dans votre exemple. Vous pouvez résoudre ce problème en inversant la façon dont vous convertissez votre tableau: déclarez votre tableau en tant que tableau d'int et redonnez-le à char * lorsque vous devez y accéder de cette façon.

// raw data consists of 4 ints
int data[4];

// here's the char * to the original data
char *cdata = (char *)data;
// now we can recast it safely to int *
i1 = *((int*)cdata);
i2 = *((int*)(cdata + sizeof(int)));
i3 = *((int*)(cdata + sizeof(int) * 2));
i4 = *((int*)(cdata + sizeof(int) * 3));

Il n'y aura pas de problème sur les types de primitives. Les problèmes d’alignement se produisent lorsqu’il s’agit de tableaux de données structurées (struct en C), si le type de primitive original du tableau est plus grand que celui vers lequel il est jeté, voir la mise à jour ci-dessus.

Il devrait être parfaitement correct de convertir un tableau de caractères en un tableau de int, à condition de remplacer le décalage de 4 par sizeof(int), afin de correspondre à la taille de int sur la plate-forme sur laquelle le code est exécuté.

// raw data consists of 4 ints
unsigned char data[4 * sizeof(int)];
int i1, i2, i3, i4;
i1 = *((int*)data);
i2 = *((int*)(data + sizeof(int)));
i3 = *((int*)(data + sizeof(int) * 2));
i4 = *((int*)(data + sizeof(int) * 3));

Notez que vous ne rencontrerez endianness issues que si vous partagez ces données d’une plate-forme à une autre avec un ordre d’octets différent. Sinon, ça devrait aller parfaitement.

1
didierc

Vous voudrez peut-être lui montrer comment les choses peuvent différer selon la version du compilateur:

Outre l'alignement, il existe un deuxième problème: la norme vous permet de convertir un int* en char*, mais pas l'inverse (à moins que le char* ait été initialement créé à partir d'un int*). Voir ce post pour plus de détails.

1
StackedCrooked

Si vous devez vous préoccuper de l'alignement dépend de l'alignement de l'objet à l'origine du pointeur.

Si vous convertissez un type dont les exigences d'alignement sont plus strictes, il n'est pas portable.

La base d'un tableau char, comme dans votre exemple, n'est pas obligée d'avoir un alignement plus strict que celui du type d'élément char

Cependant, un pointeur sur n'importe quel type d'objet peut être converti en char * et inversement, quel que soit l'alignement. Le pointeur char * préserve l'alignement plus fort de l'original.

Vous pouvez utiliser une union pour créer un tableau de caractères qui est plus fortement aligné:

union u {
    long dummy; /* not used */
    char a[sizeof(long)];
};

Tous les membres d'un syndicat commencent à la même adresse: il n'y a pas de remplissage au début. Lorsqu'un objet d'union est défini en stockage, il doit donc avoir un alignement adapté à l'élément le plus strictement aligné.

Notre union u ci-dessus est correctement aligné pour les objets de type long.

La violation des restrictions d'alignement peut entraîner le blocage du programme lorsqu'il est porté sur certaines architectures. Ou cela peut fonctionner, mais avec un impact léger à sévère sur les performances, selon que les accès mémoire mal alignés sont implémentés dans le matériel (au prix de cycles supplémentaires) ou dans le logiciel (interruptions vers le noyau, où le logiciel émule l'accès, à un coût de nombreux cycles).

0
Kaz