GCC, строгое псевдонимы и приведение через союз
У вас есть какие-нибудь страшные истории, чтобы рассказать? Руководство GCC недавно добавило предупреждение относительно -fstrict-aliasing и приведение указателя через объединение:
[...] Взятие адреса, приведение результирующего указателя и разыменование результата имеют неопределенное поведение [выделение добавлено], даже если приведение использует тип объединения, например:
union a_union {
int i;
double d;
};
int f() {
double d = 3.0;
return ((union a_union *)&d)->i;
}
У кого-нибудь есть пример, иллюстрирующий это неопределенное поведение?
Обратите внимание, что этот вопрос не о том, что говорит или не говорит стандарт C99. Сегодня речь идет о реальном функционировании gcc и других существующих компиляторов.
Я только догадываюсь, но одна потенциальная проблема может заключаться в d
до 3,0. Так как d
это временная переменная, которая никогда не читается напрямую, и которая никогда не читается через "несколько совместимый" указатель, компилятор может не потрудиться установить ее. И тогда f() вернет мусор из стека.
Моя простая, наивная попытка не удалась. Например:
#include <stdio.h>
union a_union {
int i;
double d;
};
int f1(void) {
union a_union t;
t.d = 3333333.0;
return t.i; // gcc manual: 'type-punning is allowed, provided...' (C90 6.3.2.3)
}
int f2(void) {
double d = 3333333.0;
return ((union a_union *)&d)->i; // gcc manual: 'undefined behavior'
}
int main(void) {
printf("%d\n", f1());
printf("%d\n", f2());
return 0;
}
отлично работает, давая на CYGWIN:
-2147483648
-2147483648
Глядя на ассемблер, мы видим, что gcc полностью оптимизирует t
далеко: f1()
просто сохраняет предварительно рассчитанный ответ:
movl $-2147483648, %eax
в то время как f2()
помещает 3333333.0 в стек с плавающей точкой, а затем извлекает возвращаемое значение:
flds LC0 # LC0: 1246458708 (= 3333333.0) (--> 80 bits)
fstpl -8(%ebp) # save in d (64 bits)
movl -8(%ebp), %eax # return value (32 bits)
И функции также встроены (что, по-видимому, является причиной некоторых тонких ошибок, связанных со строгим псевдонимом), но здесь это не имеет значения. (И этот ассемблер не так важен, но он добавляет подтверждающие детали.)
Также обратите внимание, что брать адреса явно неправильно (или правильно, если вы пытаетесь проиллюстрировать неопределенное поведение). Например, как мы знаем, это неправильно:
extern void foo(int *, double *);
union a_union t;
t.d = 3.0;
foo(&t.i, &t.d); // undefined behavior
мы также знаем, что это неправильно
extern void foo(int *, double *);
double d = 3.0;
foo(&((union a_union *)&d)->i, &d); // undefined behavior
Для дополнительной информации об этом см., Например:
http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1422.pdf
http://gcc.gnu.org/ml/gcc/2010-01/msg00013.html
http://davmac.wordpress.com/2010/02/26/c99-revisited/
http://cellperformance.beyond3d.com/articles/2006/06/understanding-strict-aliasing.html
(= поиск страницы в Google, затем просмотр кэшированной страницы)
Что такое строгое правило псевдонимов?
C99 строгие правила псевдонимов в C++ (GCC)
В первой ссылке, черновик протокола собрания ISO семь месяцев назад, один участник отмечает в разделе 4.16:
Есть ли кто-нибудь, кто считает, что правила достаточно ясны? Никто на самом деле не может их интерпретировать.
Другие примечания: Мой тест был с gcc 4.3.4, с -O2; опции -O2 и -O3 подразумевают -fstrict-aliasing. Пример из руководства GCC предполагает sizeof (double) > = sizeof (int); не имеет значения, если они неравны.
Кроме того, как отметил Майк Актон в ссылке cellperformace, -Wstrict-aliasing=2
, но не =3
, производит warning: dereferencing type-punned pointer might break strict-aliasing rules
для примера здесь.
7 ответов
Тот факт, что GCC предупреждает о профсоюзах, не обязательно означает, что профсоюзы в настоящее время не работают. Но вот немного менее простой пример, чем ваш:
#include <stdio.h>
struct B {
int i1;
int i2;
};
union A {
struct B b;
double d;
};
int main() {
double d = 3.0;
#ifdef USE_UNION
((union A*)&d)->b.i2 += 0x80000000;
#else
((int*)&d)[1] += 0x80000000;
#endif
printf("%g\n", d);
}
Выход:
$ gcc --version
gcc (GCC) 4.3.4 20090804 (release) 1
Copyright (C) 2008 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
$ gcc -oalias alias.c -O1 -std=c99 && ./alias
-3
$ gcc -oalias alias.c -O3 -std=c99 && ./alias
3
$ gcc -oalias alias.c -O1 -std=c99 -DUSE_UNION && ./alias
-3
$ gcc -oalias alias.c -O3 -std=c99 -DUSE_UNION && ./alias
-3
Таким образом, в GCC 4.3.4 объединение "спасает день" (при условии, что я хочу вывод "-3"). Он отключает оптимизацию, которая основывается на строгом псевдониме, что приводит к выводу "3" во втором случае (только). С -Wall, USE_UNION также отключает предупреждение о каламбурах.
У меня нет gcc 4.4 для тестирования, но, пожалуйста, попробуйте этот код. Ваш код в действительности проверяет, является ли память для d
инициализируется перед чтением через объединение: моя проверяет, не модифицировано ли оно.
Кстати, безопасный способ читать половину двойного числа как int это:
double d = 3;
int i;
memcpy(&i, &d, sizeof i);
return i;
При оптимизации на GCC это приводит к:
int thing() {
401130: 55 push %ebp
401131: 89 e5 mov %esp,%ebp
401133: 83 ec 10 sub $0x10,%esp
double d = 3;
401136: d9 05 a8 20 40 00 flds 0x4020a8
40113c: dd 5d f0 fstpl -0x10(%ebp)
int i;
memcpy(&i, &d, sizeof i);
40113f: 8b 45 f0 mov -0x10(%ebp),%eax
return i;
}
401142: c9 leave
401143: c3 ret
Так что нет фактического вызова memcpy. Если вы этого не делаете, то вы заслуживаете того, что получаете, если кастовые объединения перестают работать в GCC;-)
Ну, это немного некропостинга, но вот ужасная история. Я портирую программу, которая была написана с допущением, что порядок байтов в нативном порядке является байтовым. Теперь мне нужно, чтобы она работала также с прямым порядком байтов. К сожалению, я не могу просто использовать собственный порядок байтов везде, так как данные могут быть доступны разными способами. Например, 64-разрядное целое число может рассматриваться как два 32-разрядных целых числа или как 4 16-разрядных целых числа, или даже как 16 4-разрядных целых числа. Что еще хуже, нет никакого способа выяснить, что именно хранится в памяти, потому что программное обеспечение является интерпретатором для некоторого вида байтового кода, и данные формируются этим байтовым кодом. Например, байт-код может содержать инструкции для записи массива 16-разрядных целых чисел, а затем обращаться к их паре как 32-разрядное число с плавающей запятой. И нет никакого способа предсказать это или изменить байтовый код.
Поэтому мне пришлось создать набор классов-оболочек для работы со значениями, хранящимися в порядке с прямым порядком байтов, независимо от собственного порядка байтов. Отлично работал в Visual Studio и в GCC на Linux без оптимизации. Но с gcc -O2 ад вырвался на свободу. После долгих отладок я понял, что причина была здесь:
double D;
float F;
Ul *pF=(Ul*)&F; // Ul is unsigned long
*pF=pop0->lu.r(); // r() returns Ul
D=(double)F;
Этот код использовался для преобразования 32-разрядного представления числа с плавающей запятой, хранящегося в 32-разрядном целом числе, в удвоенное значение. Похоже, что компилятор решил выполнить присваивание *pF после присваивания D - в результате при первом выполнении кода значение D было мусором, а последующие значения были "запоздалыми" на 1 итерацию.
Чудесным образом в тот момент не было никаких других проблем. Поэтому я решил продолжить и протестировать свой новый код на исходной платформе HP-UX на процессоре RISC с собственным порядком байтов. Теперь это сломалось снова, на этот раз в моем новом классе:
typedef unsigned long long Ur; // 64-bit uint
typedef unsigned char Uc;
class BEDoubleRef {
double *p;
public:
inline BEDoubleRef(double *p): p(p) {}
inline operator double() {
Uc *pu = reinterpret_cast<Uc*>(p);
Ur n = (pu[7] & 0xFFULL) | ((pu[6] & 0xFFULL) << 8)
| ((pu[5] & 0xFFULL) << 16) | ((pu[4] & 0xFFULL) << 24)
| ((pu[3] & 0xFFULL) << 32) | ((pu[2] & 0xFFULL) << 40)
| ((pu[1] & 0xFFULL) << 48) | ((pu[0] & 0xFFULL) << 56);
return *reinterpret_cast<double*>(&n);
}
inline BEDoubleRef &operator=(const double &d) {
Uc *pc = reinterpret_cast<Uc*>(p);
const Ur *pu = reinterpret_cast<const Ur*>(&d);
pc[0] = (*pu >> 56) & 0xFFu;
pc[1] = (*pu >> 48) & 0xFFu;
pc[2] = (*pu >> 40) & 0xFFu;
pc[3] = (*pu >> 32) & 0xFFu;
pc[4] = (*pu >> 24) & 0xFFu;
pc[5] = (*pu >> 16) & 0xFFu;
pc[6] = (*pu >> 8) & 0xFFu;
pc[7] = *pu & 0xFFu;
return *this;
}
inline BEDoubleRef &operator=(const BEDoubleRef &d) {
*p = *d.p;
return *this;
}
};
По какой-то очень странной причине первый оператор присваивания правильно назначил только байты с 1 по 7. Байт 0 всегда содержал какую-то ерунду, которая нарушала все, поскольку есть бит знака и часть порядка.
Я попытался использовать профсоюзы в качестве обходного пути:
union {
double d;
Uc c[8];
} un;
Uc *pc = un.c;
const Ur *pu = reinterpret_cast<const Ur*>(&d);
pc[0] = (*pu >> 56) & 0xFFu;
pc[1] = (*pu >> 48) & 0xFFu;
pc[2] = (*pu >> 40) & 0xFFu;
pc[3] = (*pu >> 32) & 0xFFu;
pc[4] = (*pu >> 24) & 0xFFu;
pc[5] = (*pu >> 16) & 0xFFu;
pc[6] = (*pu >> 8) & 0xFFu;
pc[7] = *pu & 0xFFu;
*p = un.d;
но это тоже не сработало. На самом деле, это было немного лучше - это не удалось только для отрицательных чисел.
На данный момент я думаю о том, чтобы добавить простой тест для собственного порядка байтов, а затем сделать все через char*
указатели с if (LITTLE_ENDIAN)
проверяет вокруг. Что еще хуже, программа интенсивно использует союзы повсюду, что, кажется, пока работает нормально, но после всего этого беспорядка я не удивлюсь, если он внезапно сломается без видимой причины.
Ваше утверждение, что следующий код "неправильный":
extern void foo(int *, double *);
union a_union t;
t.d = 3.0;
foo(&t.i, &t.d); // undefined behavior
... неправильно. Просто взятие адреса двух членов объединения и передача их внешней функции не приводит к неопределенному поведению; Вы получаете это только от разыменования одного из этих указателей недопустимым способом. Например, если функция foo немедленно возвращает значение без разыменования указателей, которые вы передали, то поведение не является неопределенным. При строгом чтении стандарта C99 даже в некоторых случаях можно ссылаться на указатели, не вызывая неопределенное поведение; например, он может прочитать значение, на которое ссылается второй указатель, и затем сохранить значение через первый указатель, при условии, что они оба указывают на динамически размещенный объект (т. е. объект без "объявленного типа").
Псевдоним возникает, когда компилятор имеет два разных указателя на один и тот же фрагмент памяти. Типизируя указатель, вы генерируете новый временный указатель. Если оптимизатор, например, переупорядочивает инструкции по сборке, доступ к двум указателям может дать два совершенно разных результата - он может изменить порядок чтения перед записью на один и тот же адрес. Вот почему это неопределенное поведение.
Вы вряд ли увидите проблему в очень простом тестовом коде, но она появится, когда многое будет происходить.
Я думаю, что предупреждение состоит в том, чтобы прояснить, что профсоюзы не являются особым случаем, даже если вы ожидаете, что они будут.
См. Эту статью в Википедии для получения дополнительной информации о псевдонимах: http://en.wikipedia.org/wiki/Aliasing_(computing)
Вы видели это? Что такое строгое правило псевдонимов?
Ссылка содержит дополнительную ссылку на эту статью с примерами gcc. http://cellperformance.beyond3d.com/articles/2006/06/understanding-strict-aliasing.html
Попытка подобного союза была бы ближе к проблеме.
union a_union {
int i;
double *d;
};
Таким образом, у вас есть 2 типа, int и double*, указывающие на одну и ту же память. В этом случае используется двойной (*(double*)&i)
может вызвать проблему.
Вот мое: В думаю, это ошибка во всех GCC v5.x и позже
#include <iostream>
#include <complex>
#include <pmmintrin.h>
template <class Scalar_type, class Vector_type>
class simd {
public:
typedef Vector_type vector_type;
typedef Scalar_type scalar_type;
typedef union conv_t_union {
Vector_type v;
Scalar_type s[sizeof(Vector_type) / sizeof(Scalar_type)];
conv_t_union(){};
} conv_t;
static inline constexpr int Nsimd(void) {
return sizeof(Vector_type) / sizeof(Scalar_type);
}
Vector_type v;
template <class functor>
friend inline simd SimdApply(const functor &func, const simd &v) {
simd ret;
simd::conv_t conv;
conv.v = v.v;
for (int i = 0; i < simd::Nsimd(); i++) {
conv.s[i] = func(conv.s[i]);
}
ret.v = conv.v;
return ret;
}
};
template <class scalar>
struct RealFunctor {
scalar operator()(const scalar &a) const {
return std::real(a);
}
};
template <class S, class V>
inline simd<S, V> real(const simd<S, V> &r) {
return SimdApply(RealFunctor<S>(), r);
}
typedef simd<std::complex<double>, __m128d> vcomplexd;
int main(int argc, char **argv)
{
vcomplexd a,b;
a.v=_mm_set_pd(2.0,1.0);
b = real(a);
vcomplexd::conv_t conv;
conv.v = b.v;
for(int i=0;i<vcomplexd::Nsimd();i++){
std::cout << conv.s[i]<<" ";
}
std::cout << std::endl;
}
Должен дать
c010200:~ peterboyle$ g++-mp-5 Gcc-test.cc -std=c++11
c010200:~ peterboyle$ ./a.out
(1,0)
Но под -O3: я думаю, что это неправильно и ошибка компилятора
c010200:~ peterboyle$ g++-mp-5 Gcc-test.cc -std=c++11 -O3
c010200:~ peterboyle$ ./a.out
(0,0)
Под g++4.9
c010200:~ peterboyle$ g++-4.9 Gcc-test.cc -std=c++11 -O3
c010200:~ peterboyle$ ./a.out
(1,0)
Под llvm xcode
c010200:~ peterboyle$ g++ Gcc-test.cc -std=c++11 -O3
c010200:~ peterboyle$ ./a.out
(1,0)
Я не очень понимаю вашу проблему. Компилятор сделал именно то, что должен был сделать в вашем примере. union
преобразование - это то, что вы сделали в f1
, В f2
это обычный тип указателя, то, что вы привели его к объединению, не имеет значения, это все еще приведение указателя