Есть ли у меня ошибка оптимизации 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"

Достаточно интересно, clang, новый интерфейс C для llvm (llvm.org) действительно выводит правильный ответ.

Keltia 06.04.2009 11:45

Помимо нарушения псевдонима, печать выражения size_t со спецификатором формата %d - это UB. Вам нужен %zu, иначе вам нужно преобразовать его в int. Кроме того, size_t, вероятно, больше, чем float (например, на 64-битных машинах), поэтому вам, вероятно, следует использовать uint32_t, который всегда имеет тот же размер, что и float в реализациях, совместимых с IEEE 754.

R.. GitHub STOP HELPING ICE 03.02.2011 11:17

Помните, Это всегда твоя вина

Tom Ritter 17.09.2008 18:46
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
10
3
6 080
9
Перейти к ответу Данный вопрос помечен как решенный

Ответы 9

Ответ принят как подходящий

Используйте флаг компилятора -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);

Это не стоит нового ответа, но, возможно, вы могли бы отредактировать свой ответ, чтобы указать, что соблюдение строгих правил псевдонима в стандарте ANSI начинается с gcc-4

Thomas Kammeyer 17.09.2008 18:46

Да, почти все, что предшествует 3.4, будет жаловаться на это, но только после того, как вы действительно попытаетесь разыменовать его.

Tim Post 06.04.2009 11:40

Почему вы думаете, что t должно быть 0?

Или, точнее говоря: «Почему вы думаете, что двоичное представление нуля с плавающей запятой будет таким же, как двоичное представление целого нуля?»

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

bortzmeyer 01.03.2009 21:09

-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

Это больше не разрешено в соответствии с правилами C99 по сглаживанию указателей. Указатели двух разных типов не могут указывать на одно и то же место в памяти. Исключением из этого правила являются указатели void и char.

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

size_t size = (size_t) (f); // это работает

Обычно об этом сообщают как об ошибке, но на самом деле это функция, позволяющая оптимизаторам работать более эффективно.

В gcc вы можете отключить это с помощью переключателя компилятора. Я верю -fno_strict_aliasing.

Это плохой код C. Ваше приведение нарушает правила псевдонима C, и оптимизатор может делать то, что нарушает этот код. Вы, вероятно, обнаружите, что GCC запланировал чтение size_t перед записью с плавающей запятой (чтобы скрыть задержку конвейера fp).

Вы можете установить переключатель -fno-strict-aliasing или использовать union или reinterpret_cast для переинтерпретации значения в соответствии со стандартами.

Это плохой код на C :-)

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

Это нарушает правило сглаживания. Компилятор может предположить, что указатели на разные типы, такие как float или int, не перекрываются в памяти. Вы сделали именно это.

Компилятор видит, что вы что-то вычисляете, сохраняете это в float f и больше к нему не обращаетесь. Скорее всего, компилятор удалил часть кода, и назначение так и не произошло.

В этом случае разыменование через указатель size_t вернет из стека неинициализированный мусор.

Вы можете сделать две вещи, чтобы обойти это:

  1. используйте объединение с элементом с плавающей запятой и членом size_t и выполните приведение с помощью каламбура типов. Неприятно, но работает.

  2. используйте memcopy, чтобы скопировать содержимое f в ваш size_t. Компилятор достаточно умен, чтобы обнаружить и оптимизировать этот случай.

Извините за придирчивость, но это тип каламбура, а не обрезка в обходном пути 1.

James Caccese 01.03.2009 07:04

Я тестировал ваш код с помощью: "i686-apple-darwin9-gcc-4.0.1 (GCC) 4.0.1 (Apple Inc., сборка 5465)"

и проблем не было. Выход:

t should be 0 but is 0

Так что в вашем коде нет ошибки. Это не значит, что это хороший код. Но я бы добавил тип возврата главной функции и "return 0;" в конце функции.

Тот факт, что код обеспечивает ожидаемый результат на единой платформе, никоим образом не является доказательством его правильности!

Joachim Sauer 01.03.2009 17:36

Эта ошибка не возникает в OS X. Я сталкивался с ней в Linux, но Windows и OS X работают нормально.

akauppi 06.04.2009 11:21

В стандарте C99 это покрывается следующим правилом 6.5-7:

An object shall have its stored value accessed only by an lvalue expression that has one of the following types:73)

  • a type compatible with the effective type of the object,

  • a qualified version of a type compatible with the effective type of the object,

  • a type that is the signed or unsigned type corresponding to the effective type of the object,

  • a type that is the signed or unsigned type corresponding to a qualified version of the effective type of the object,

  • an aggregate or union type that includes one of the aforementioned types among its members (including, recursively, a member of a subaggregate or contained union), or

  • a character type.

Последний пункт - почему работает приведение сначала к (char *).

Помимо выравнивания указателя, вы ожидаете, что sizeof (size_t) == sizeof (float). Я не думаю, что это так (в 64-битном Linux size_t должно быть 64 бита, но с плавающей точкой 32 бита), то есть ваш код будет читать что-то неинициализированное.

Другие вопросы по теме