Есть ли у меня ошибка оптимизации gcc или проблема с кодом C?
Вопрос
Проверьте следующий код:
#include <stdio.h>
#include <stdlib.h>
main()
{
const char *yytext="0";
const float f=(float)atof(yytext);
size_t t = *((size_t*)&f);
printf("t should be 0 but is %d\n", t);
}
Скомпилируйте его с помощью:
gcc -O3 test.c
ХОРОШИЙ результат должен быть:
"t should be 0 but is 0"
Но с моим gcc 4.1.3 у меня есть:
"t should be 0 but is -1209357172"
Решение
Используйте флаг компилятора -fno-strict-aliasing.
При включенном строгом псевдониме, как это установлено по умолчанию как минимум для -O3, в строке:
size_t t = *((size_t*)&f);
компилятор предполагает, что size_t* НЕ указывает на ту же область памяти, что и float*.Насколько мне известно, это поведение соответствует стандартам (как отметил Томас Каммейер, соблюдение строгих правил псевдонимов в стандарте ANSI начинается с gcc-4).
Если я правильно помню, вы можете использовать промежуточное приведение к char*, чтобы обойти это.(компилятор предполагает, что char* может использовать псевдоним чего угодно)
Другими словами, попробуйте это (сам сейчас не могу проверить, но думаю, что это сработает):
size_t t = *((size_t*)(char*)&f);
Другие советы
В стандарте C99 это регулируется следующим правилом в разделах 6.5-7:
Объект должен иметь свое сохраненное значение, доступное только с помощью выражения LVALUE, которое имеет один из следующих типов: 73)
тип, совместимый с эффективным типом объекта,
квалифицированная версия типа, совместимая с эффективным типом объекта,
тип, который является подписанным или без знакового типа, соответствующий эффективному типу объекта,
Тип, который является подписанным или без знака, соответствующий квалифицированной версии эффективного типа объекта,
совокупный тип или профсоюзный тип, который включает в себя один из вышеупомянутых типов среди его членов (включая, рекурсивно, член субагрегатного или содержащегося союза), или
тип персонажа.
Последний вопрос: почему сначала работает приведение к (char*).
Это больше не разрешено в соответствии с правилами C99 по псевдонимам указателей.Указатели двух разных типов не могут указывать на одно и то же место в памяти.Исключением из этого правила являются указатели void и char.
Таким образом, в вашем коде, где вы выполняете приведение к указателю size_t, компилятор может игнорировать это.Если вы хотите получить значение с плавающей запятой в виде size_t, просто назначьте его, и число с плавающей запятой будет преобразовано (усечено, а не округлено) как таковое:
size_t size = (size_t) (е);// это работает
Обычно об этом сообщают как об ошибке, но на самом деле это функция, которая позволяет оптимизаторам работать более эффективно.
В gcc вы можете отключить это с помощью переключателя компилятора.Я верю -fno_strict_aliasing.
Это плохой код C :-)
Проблема заключается в том, что вы получаете доступ к одному объекту типа float, приводя его к целочисленному указателю и разыменовывая его.
Это нарушает правило псевдонимов.Компилятор может предположить, что указатели на разные типы, такие как float или int, не перекрываются в памяти.Вы сделали именно это.
Компилятор видит, что вы что-то вычисляете, сохраняете это в float f и больше никогда к этому не обращаетесь.Скорее всего компилятор удалил часть кода и присвоение так и не произошло.
Разыменование через указатель size_t в этом случае вернет из стека некоторый неинициализированный мусор.
Чтобы обойти эту проблему, вы можете сделать две вещи:
используйте объединение с плавающей запятой и элементом size_t и выполните приведение с помощью каламбура типов.Некрасиво, но работает.
используйте memcopy, чтобы скопировать содержимое f в ваш size_t.Компилятор достаточно умен, чтобы обнаружить и оптимизировать этот случай.
Почему вы думаете, что t должно быть равно 0?
Или, точнее говоря: «Почему вы думаете, что двоичное представление нуля с плавающей запятой будет таким же, как двоичное представление целочисленного нуля?»
Это плохой код C.Ваш приведение нарушает правила псевдонимов C, и оптимизатор может делать то, что нарушает этот код.Вы, вероятно, обнаружите, что GCC запланировал чтение size_t перед записью с плавающей запятой (чтобы скрыть задержку конвейера fp).
Вы можете установить переключатель -fno-strict-aliasing или использовать объединение или reinterpret_cast, чтобы интерпретировать значение в соответствии со стандартами.
Помимо выравнивания указателей, вы ожидаете, что sizeof(size_t)==sizeof(float).Я не думаю, что это так (в 64-разрядной версии Linux size_t должно быть 64 бита, но с плавающей запятой 32 бита), что означает, что ваш код будет читать что-то неинициализированное.
-O3 не считается «нормальным», -O2 обычно является верхним порогом, за исключением, возможно, некоторых мультимедийных приложений.
Некоторые приложения не могут зайти так далеко и умрут, если вы выйдете за пределы -O1 .
Если у вас достаточно новый GCC (у меня здесь версия 4.3), он может поддерживать эту команду.
gcc -c -Q -O3 --help=optimizers > /tmp/O3-opts
Если вы будете осторожны, вы, возможно, сможете просмотреть этот список и найти конкретную включенную вами оптимизацию, которая вызывает эту ошибку.
От man gcc
:
The output is sensitive to the effects of previous command line options, so for example it is possible to find out which
optimizations are enabled at -O2 by using:
-O2 --help=optimizers
Alternatively you can discover which binary optimizations are enabled by -O3 by using:
gcc -c -Q -O3 --help=optimizers > /tmp/O3-opts
gcc -c -Q -O2 --help=optimizers > /tmp/O2-opts
diff /tmp/O2-opts /tmp/O3-opts | grep enabled
Я протестировал ваш код с помощью:«i686-apple-darwin9-gcc-4.0.1 (GCC) 4.0.1 (Apple Inc.сборка 5465)"
и не было никаких проблем.Выход:
t should be 0 but is 0
Итак, в вашем коде нет ошибки.Это не значит, что это хороший код.Но я бы добавил returntype основной функции и «return 0»; в конце функции.