Стоит ли беспокоиться о выравнивании во время приведения указателя?

В моем проекте у нас есть такой код:

// 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));

Я говорил с моим техническим руководителем, что этот код не может быть переносимым, так как он пытается unsigned char* к int* который обычно имеет более строгое требование выравнивания. Но технический лидер говорит, что все в порядке, большинство компиляторов остается неизменным значением указателя после приведения, и я могу просто написать такой код.

Честно говоря, я не совсем уверен. После исследования я обнаружил, что некоторые люди против использования приведений указателей, как указано выше, например, Вот а также Вот.

Итак, вот мои вопросы:

  1. Действительно ли безопасно разыменовать указатель после приведения в реальном проекте?
  2. Есть ли разница между кастингом в стиле C и reinterpret_cast?
  3. Есть ли разница между C и C ++?

50

Решение

1. ДЕЙСТВИТЕЛЬНО ли безопасно разыменовать указатель после приведения в реальном проекте?

Если указатель не выровнен должным образом, это может вызвать проблемы. Я лично видел и исправлял ошибки шины в реальном, производственном коде, вызванном приведением char* к более строго выровненному типу. Даже если вы не получите очевидную ошибку, у вас могут возникнуть менее очевидные проблемы, такие как снижение производительности. Строгое следование стандарту во избежание UB — хорошая идея, даже если вы сразу не видите никаких проблем. (И одно из правил, которое нарушает код, — это строгое правило псевдонимов, § 3.10 / 10 *)

Лучшая альтернатива — использовать std::memcpy() или же std::memmove если буферы перекрываются (или еще лучше 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));

Некоторые компиляторы работают больше, чем другие, чтобы убедиться, что массивы символов выровнены более строго, чем необходимо, потому что программисты часто ошибаются в этом.

#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. Есть ли разница между приведением в стиле C и reinterpret_cast?

Это зависит. Приведения в стиле C делают разные вещи в зависимости от используемых типов. Приведение в стиле C между указателями приводит к тому же, что и reinterpret_cast; См. П. 5.4 Явное преобразование типов (нотация приведения) и § 5.2.9-11.

3. Есть ли разница между C и C ++?

Не должно быть так долго, пока вы имеете дело с типами, которые допустимы в C.


* Другая проблема заключается в том, что C ++ не определяет результат приведения из одного типа указателя к типу с более строгими требованиями к выравниванию. Это делается для поддержки платформ, на которых указатели без выравнивания даже не могут быть представлены. Однако сегодня типичные платформы могут представлять собой невыровненные указатели, а компиляторы указывают, что результаты такого приведения будут такими, как вы ожидаете. Таким образом, эта проблема является вторичной по отношению к нарушению псевдонимов. См. [Expr.reinterpret.cast] / 7.

34

Другие решения

Это не хорошо, правда. Выравнивание может быть неправильным, и код может нарушать строгий псевдоним. Вы должны распаковать это явно.

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

и т.д. Это определенно четко определенное поведение, и в качестве бонуса оно также не зависит от порядка байтов, в отличие от вашего указателя.

27

В примере, который вы здесь показываете, то, что вы делаете, будет безопасно почти на всех современных процессорах, если исходный указатель на символ правильно выровнен. В целом, это небезопасно и не гарантирует работу.

Если начальный указатель на символ не выровнен правильно, это будет работать на x86 и x86_64, но может не работать на других архитектурах. Если вам повезет, это просто даст вам сбой, и вы исправите свой код. Если вам не повезло, доступ к невыровненному будет исправлен обработчиком ловушек в вашей операционной системе, и вы будете иметь ужасную производительность без какой-либо очевидной обратной связи о том, почему она такая медленная (для некоторого кода мы говорим крайне медленно, это было огромной проблемой на альфе 20 лет назад).

Даже на х86 & co, не выровненный доступ будет медленнее.

Если вы хотите быть в безопасности сегодня и в будущем, просто memcpy вместо того, чтобы делать назначение, как это. Современный компилятор, вероятно, будет иметь оптимизации для memcpy и делать правильные вещи, а если нет, memcpy Сам будет иметь обнаружение выравнивания и сделает самую быструю вещь.

Кроме того, ваш пример неверен в одном: sizeof (int) не всегда 4.

6

Правильный способ распаковать char буферизованные данные должны использовать 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));

Преобразование нарушает псевдонимы, что означает, что компилятор и оптимизатор могут обрабатывать исходный объект как неинициализированный.

По поводу ваших 3 вопросов:

  1. Нет, разыменование указателя приведения вообще небезопасно из-за наложения и выравнивания.
  2. Нет, в C ++ приведение в стиле C определяется в терминах reinterpret_cast,
  3. Нет, C и C ++ договариваются о псевдонимах на основе типов. Существует разница в обработке псевдонимов, основанных на объединении (C допускает это в некоторых случаях, C ++ — нет).
4

Обновить:
Я упустил из виду тот факт, что действительно меньшие типы могут быть выровнены относительно большего, как это может быть в вашем примере. Вы можете решить эту проблему, изменив способ преобразования массива: объявите массив как массив типа int и приведите его к char * когда вам нужно получить к нему доступ таким образом.

// 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));

Не будет никаких проблем с массивом типов примитивов. Проблемы выравнивания возникают при работе с массивами структурированные данные (struct в с), если исходный тип примитива массива больше, чем тот, к которому он приведен, см. обновление выше.

Должно быть совершенно нормально привести массив char к массиву int, если вы замените смещение 4 на sizeof(int), чтобы соответствовать размеру int на платформе, на которой должен выполняться код.

// 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));

Обратите внимание, что вы получите порядок байт проблемы только в том случае, если вы как-то делитесь этими данными с одной платформы на другую с другим порядком байтов. В противном случае все должно быть прекрасно.

1

Вы можете показать ему, как все может отличаться в зависимости от версии компилятора:

Помимо выравнивания есть еще одна проблема: стандарт позволяет вам int* в char* но не наоборот (если только char* был изначально отлит из int*). Смотрите этот пост для более подробной информации.

1

Нужно ли вам беспокоиться о выравнивании, зависит от выравнивания объекта, от которого произошел указатель.

Если вы приведете к типу с более строгими требованиями к выравниванию, он не будет переносимым.

Основание char массив, как в вашем примере, не обязательно должен иметь более строгое выравнивание, чем для типа элемента char,

Тем не менее, указатель на любой тип объекта может быть преобразован в char * и обратно, независимо от выравнивания. char * Указатель сохраняет более сильное выравнивание оригинала.

Вы можете использовать объединение для создания массива char, который выровнен более строго:

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

Все члены профсоюза начинаются с одного и того же адреса: в начале нет отступов. Когда объект объединения определен в хранилище, он должен иметь выравнивание, которое подходит для наиболее строго выровненного элемента.

наш union u Выше выровнен достаточно строго для объектов типа long,

Нарушение ограничений выравнивания может привести к сбою программы при переносе на некоторые архитектуры. Или это может сработать, но с незначительным или серьезным влиянием на производительность, в зависимости от того, реализован ли неправильно выровненный доступ к памяти на аппаратном уровне (за счет некоторых дополнительных циклов) или в программном обеспечении (ловушки для ядра, где программное обеспечение эмулирует доступ, за плату). много циклов).

0
По вопросам рекламы ammmcru@yandex.ru
Adblock
detector