Gcc, строгое сглаживание и литье через объединение

Есть ли у вас ужасные истории? Руководство GCC недавно добавило предупреждение о -fstrict-aliasing и литье указателя через объединение:

[...] Принимая адрес, вызывая результирующий указатель и разыменовывая результат, имеет undefined поведение [выделение добавлено], даже если cast использует тип объединения, например:

    union a_union {
        int i;
        double d;
    };

    int f() {
        double d = 3.0;
        return ((union a_union *)&d)->i;
    }

Есть ли у кого-нибудь пример для иллюстрации этого поведения undefined?

Обратите внимание, что этот вопрос не касается того, что говорит стандарт 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

while 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)

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

Также обратите внимание, что прием адресов явно неверен (или правильно, если вы пытаетесь проиллюстрировать поведение undefined). Например, как мы знаем, это неверно:

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 строгие правила псевдонимов в С++ (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.

Ответ 1

Тот факт, что 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, прежде чем прочитывать через объединение: my проверяет, изменяется ли она.

Btw, безопасный способ чтения половины двойника как 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 прекратится, -)

Ответ 2

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

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

Я думаю, что предупреждение состоит в том, чтобы четко указать, что профсоюзы не являются особым случаем, даже если вы можете ожидать, что они будут.

См. эту статью в Википедии для получения дополнительной информации об псевдониме: http://en.wikipedia.org/wiki/Aliasing_(computing)#Conflicts_with_optimization

Ответ 3

Ваше утверждение, что следующий код "неправильный":

extern void foo(int *, double *);
union a_union t;
t.d = 3.0;
foo(&t.i, &t.d); // undefined behavior

... неверно. Простое обращение адреса двух членов профсоюза и передача их во внешнюю функцию не приводит к поведению undefined; вы получаете только это от разыменования одного из этих указателей недействительным способом. Например, если функция foo немедленно возвращается без разыменования указателей, которые вы передали, тогда поведение не undefined. При строгом чтении стандарта C99 есть даже некоторые случаи, когда указатели могут быть разыменованы без вызова поведения undefined; например, он мог бы прочитать значение, на которое ссылается второй указатель, а затем сохранить значение через первый указатель, если они оба указывают на динамически выделенный объект (т.е. один без "объявленного типа" ).

Ответ 4

Ну, это немного некро-сообщений, но вот ужасная история. Я переношу программу, которая была написана с предположением, что собственный порядок байтов является большим. Теперь мне нужно, чтобы он работал и с маленьким концом. К сожалению, я не могу просто использовать собственный порядок байтов везде, так как доступ к данным можно было бы разными способами. Например, 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). Чтобы усугубить ситуацию, программа сильно использует профсоюзы, которые, похоже, работают нормально на данный момент, но после всего этого беспорядка я не удивлюсь, если он внезапно прорвется без видимых причин.

Ответ 5

Вы видели это? Что такое строгое правило псевдонимов?

Ссылка содержит вторичную ссылку на эту статью с примерами gcc. http://cellperformance.beyond3d.com/articles/2006/06/understanding-strict-aliasing.html

Попытка объединения вроде этого будет ближе к проблеме.

union a_union {
    int i;
    double *d;
};

Таким образом, у вас есть 2 типа: int и double *, указывающие на одну и ту же память. В этом случае использование double (*(double*)&i) может вызвать проблему.

Ответ 6

Вот мой:. Думайте, что это ошибка во всех 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) 

Ответ 7

Я не понимаю вашу проблему. Компилятор сделал именно то, что он должен был делать в вашем примере. Преобразование union - это то, что вы сделали в f1. В f2 это обычный тип указателя, который вы передали его в объединение, не имеет значения, он все еще является указателем casting