Нарушает ли это приведение указателя строгое правило сглаживания?

Это быстрая реализация метода вычисления обратного квадратного корня из Quake III Arena:

float Q_rsqrt( float number )
{
        long i;
        float x2, y;
        const float threehalfs = 1.5F;

        x2 = number * 0.5F;
        y  = number;
        i  = * ( long * ) &y;                       // evil floating point bit level hacking
        i  = 0x5f3759df - ( i >> 1 );               // what?
        y  = * ( float * ) &i;
        y  = y * ( threehalfs - ( x2 * y * y ) );   // 1st iteration
//      y  = y * ( threehalfs - ( x2 * y * y ) );   // 2nd iteration, this can be removed

        return y;
}

Я заметил, что long int i принимает разыменованное значение по адресу (преобразованному в long *) float y. Затем код выполняет операции с i перед сохранением разыменованного значения по адресу (приведенному к float *) i в y.

Нарушит ли это правило строгого псевдонима, поскольку i не того же типа, что и y?

Я думаю, что, возможно, это не так, поскольку значение разыменовано и скопировано; поэтому операции выполняются с копией, а не с оригиналом.


person Vilhelm Gray    schedule 04.04.2013    source источник
comment
Я думаю, что это действительно так, по крайней мере, пока sizeof(long) != sizeof(float).   -  person    schedule 04.04.2013
comment
sizeof не имеет ничего общего с нарушением псевдонима.   -  person R.. GitHub STOP HELPING ICE    schedule 04.04.2013
comment
В написанном коде предполагается, что long - 32-битное значение, как и float. Приведение указателя кажется изворотливым, хотя я не уверен, что это запрещено, поскольку компилятор может видеть, что происходит (в отличие от сценария, где указатель где-то хранится, а затем используется неожиданным образом). Я думаю, что проблемы с псевдонимом можно решить с помощью промежуточных преобразований в (unsigned char*), без генерации кода в случаях, которые в любом случае сработали бы правильно. Я мог бы использовать вместо этого union. Все еще не совсем переносимый, но компиляторы должны ожидать, что объединение будет псевдонимом.   -  person supercat    schedule 04.04.2013
comment
@supercat: преобразования указателей и разыменование результирующих указателей не определены стандартом C согласно C 2011 (n1570) 6.3.2.3 7. Реализация может определять поведение. Преобразование через unsigned char * не устранит этот недостаток. (Только преобразования указателя без разыменования в некоторой степени определены при условии, что выравнивание совместимо, что определяется реализацией.)   -  person Eric Postpischil    schedule 04.04.2013


Ответы (4)


Да, этот код сильно поврежден и вызывает неопределенное поведение. В частности, обратите внимание на эти две строки:

    y  = number;
    i  = * ( long * ) &y;                       // evil floating point bit level hacking

Поскольку объект *(long *)&y имеет тип long, компилятор может предположить, что он не может использовать псевдоним объекта типа float; таким образом, компилятор может переупорядочить эти две операции относительно друг друга.

Чтобы исправить это, следует использовать штуцер.

person R.. GitHub STOP HELPING ICE    schedule 04.04.2013

Да, это нарушает правила псевдонима.

В современном C вы можете изменить i = * (long *) &y; на:

i = (union { float f; long l; }) {y} .l;

и y = * (float *) &i; в:

y = (union { long l; float f; }) {i} .f;

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

person Eric Postpischil    schedule 04.04.2013
comment
Между прочим, long определенно неподходящий тип для использования здесь. Это должно быть uint32_t, или, если вы хотите использовать модели ILP32 / LP64, достаточно int или unsigned. long определенно не работает на любой реальной 64-битной целевой машине, кроме Windows. - person R.. GitHub STOP HELPING ICE; 05.04.2013
comment
@R .: long, будучи 64 битами, нарушает ограничение «у вас есть гарантии, что в используемой реализации C long и float имеют подходящие размеры и представления». - person Eric Postpischil; 05.04.2013
comment
Я согласен. Я комментировал плохой исходный процитированный код, а не ваш ответ. Извините, если это было непонятно. - person R.. GitHub STOP HELPING ICE; 05.04.2013

Да, это нарушает правила псевдонима.

Самым чистым исправлением для таких вещей, как i = * ( long * ) &y;, было бы следующее:

  memcpy(&i, &y, sizeof(i)); // assuming sizeof(i) == sizeof(y)

Это позволяет избежать проблем с выравниванием и наложением имен. А при включенной оптимизации вызов memcpy() обычно следует заменять всего несколькими инструкциями.

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

person Alexey Frunze    schedule 04.04.2013
comment
Проблема в том, что Q_rsqrt должен быть быстрым обратным квадратным корнем, поэтому обращение к memcpy нежелательно. - person RunHolt; 13.08.2013
comment
@RunHolt Сегодняшние компиляторы не имеют проблем с встраиванием таких вещей, как memcpy(). Но я уже упоминал об этом в ответе. - person Alexey Frunze; 13.08.2013
comment
@AlexeyFrunze: Даже если компиляторы могут оптимизировать memcpy для инструкций загрузки и сохранения, я все же думаю, что есть что-то обратное в идее о том, что код, который хочет выполнить простую операцию, должен, чтобы его функциональность не была разрушена оптимизатором, запрашивать больше сложная (и более трудная для чтения в исходном коде) операция, и надеюсь, что оптимизатор превратит ее в код, который не хуже того, что произвел бы неоптимизирующий компилятор. - person supercat; 15.06.2015
comment
@supercat Ну, вы либо играете по правилам (некоторые из которых сомнительны), либо устанавливаете свои собственные правила, если можете (например, в gcc есть некоторые параметры командной строки, помогающие с неправильным псевдонимом (-fno-strict-aliasing) и другими вещами). AFAIUI, союзы не гарантируют (в соответствии с языковым стандартом) решение проблем с псевдонимом, но на практике они работают (как общее расширение). - person Alexey Frunze; 16.06.2015
comment
@AlexeyFrunze: Я думал, что в более поздних стандартах языка было указано, что если объединение foo содержит bar, то компилятор будет рассматривать запись в foo как запись в bar; Я не знаю, есть ли какие-нибудь компиляторы, которые не выполнили такие ожидания, когда они не требовались и не были обновлены для этого. Тем не менее, я не могу не думать, что Стандарт должен определять что-то более чистое, чем memcpy, например указание, что если указатель на bar разыменован и записан, предполагается, что он потенциально перезаписывает объекты типа bar, , даже если он был приведен до разыменования. - person supercat; 16.06.2015
comment
По такому правилу компилятор должен был бы согласиться с тем, что *((int*)(float*)&foo)=someIntVar; может перезаписать любой int или float, адрес которого был занят. Такой синтаксис, IMHO, будет не только чище, но и более дружественен к оптимизации, чем memcpy(&foo,&someIntVar,4);, поскольку последний потребует от компилятора принятия того, что любой тип переменной может быть перезаписан, а не только float и int. - person supercat; 16.06.2015
comment
@supercat C99 перечисляет в разделе J.1 Неопределенное поведение: The value of a union member other than the last one stored into. В нем также говорится (в разделе 6.7.2.1 Спецификаторы структуры и объединения, пункт 14): The value of at most one of the members can be stored in a union object at any time. В прошлом компиляторы с плохими возможностями оптимизации не возились с псевдонимами или объединениями. Современный gcc действительно возится с псевдонимом, потому что он может законно и способен использовать его для оптимизации. - person Alexey Frunze; 16.06.2015
comment
@supercat memcpy () - это стандартная функция, известная компилятору, и у компилятора есть все основания ожидать, что memcpy(&someFloatVar, &someIntVar, 4); перезапишет только someFloatVar. - person Alexey Frunze; 16.06.2015
comment
@AlexeyFrunze: Если foo является глобальной переменной типа long*, будет ли компилятор иметь право предполагать, что после float *fp = (float*)unknownFunction(); вызов memcpy(fp, &someFloat, sizeof (float)); не изменит значение *foo? Насколько я понимаю, было бы разрешено сделать такое предположение, если бы memcpy был заменен на *fp=someFloat, но сохранение неизвестного указателя через memcpy требует, чтобы любые глобальные переменные были сброшены из регистров, независимо от их типа, поскольку memcpy может законно изменить их. Я ошибаюсь? - person supercat; 16.06.2015
comment
@supercat Конечно, если компилятор не может отследить указатель на переменную, он должен предположить, что разыменование указанного указателя через присваивание или через memcpy () может изменить любую переменную. Однако ограничения на псевдонимы по-прежнему применяются к присваиванию, а к memcpy () - нет. Это означает, что с присваиванием вы можете получить либо более эффективный код (псевдонимы в порядке), либо сломанный код (нарушается псевдоним). - person Alexey Frunze; 16.06.2015
comment
@AlexeyFrunze: Моя точка зрения заключалась в том, что если бы существовало правило, указывающее, что (float*)(int*)fp = someValue; будет считаться записывающим и в float, и в int, тогда компилятор должен будет только сбрасывать значения этих типов из регистров, а не сбрасывать указатели, double значения и т. д. Кроме того, синтаксис с двойным приведением делает более понятным для всех, кто читает код, почему обычного присваивания недостаточно (memcpy отличается от обычного присваивания не только тем, как он обрабатывает псевдонимы, но и значения ловушки и т. Д.) - person supercat; 16.06.2015
comment
@supercat Я бы предположил, что компилятор отбросит эти избыточные приведения (если они всего лишь избыточные) и будет рассматривать только фактический тип lvalue, участвующий в назначении (кстати, вы можете получить неверно выровненный указатель из приведенного указателя, который UB). Вот как я интерпретирую An object shall have its stored value accessed only by an lvalue expression that has one of the following types в разделе 6.5 «Выражения», пункт 7. IOW, *(type1*)(type2*) не является типом доступа. *(type1*) есть. - person Alexey Frunze; 17.06.2015
comment
@AlexeyFrunze: Единственная заявленная причина, указанная в Обосновании наличия правил псевдонима, заключалась в том, чтобы четко указать, что компиляторы не должны запрещать делать оптимизацию, которая была бы неправильной, если бы вещи были псевдонимами, при отсутствии доказательств того, что они будут это делать, т.е. случаи, когда возможность могла рассматриваться как действительно сомнительная. Обоснование признает, что для большинства разумных приложений требуется, чтобы компиляторы вели себя эффективно в большем количестве случаев, чем требуется стандартом, и что компилятор может одновременно соответствовать требованиям, но бесполезен. - person supercat; 24.10.2017
comment
@AlexeyFrunze: Если бы компилятор был больше заинтересован в поддержке существующего кода, чем в объявлении его сломанным, они могли бы легко распознать несколько конструкций в дополнение к тем, которые предписаны Стандартом, таким образом допуская многие оптимизации, которые невозможны с -fno-strict-aliasing, но пока быть совместимым со многими программами, которые в противном случае потребовали бы использования этого флага. - person supercat; 24.10.2017

i = * ( long * ) &y;

Это нарушает правила псевдонима и, следовательно, вызывает неопределенное поведение.

Вы обращаетесь к объекту y с типом, отличным от float, или к подписанному / неподписанному варианту char.

y = * ( float * ) &i;

Этот оператор выше также нарушает правила псевдонима.

person ouah    schedule 04.04.2013