«Небезопасная» функция С# — *(с плавающей запятой*)(&результат) против (с плавающей запятой)(результат)

Может ли кто-нибудь объяснить простым способом приведенные ниже коды:

public unsafe static float sample(){    
      int result = 154 + (153 << 8) + (25 << 16) + (64 << 24);

      return *(float*)(&result); //don't know what for... please explain
}

Примечание. приведенный выше код использует небезопасную функцию.

Для приведенного выше кода мне трудно, потому что я не понимаю, в чем разница между его возвращаемым значением по сравнению с возвращаемым значением ниже:

return (float)(result);

Нужно ли использовать небезопасную функцию, если вы возвращаете *(float*)(&result)?


person gchimuel    schedule 04.10.2012    source источник
comment
Где вы нашли образец? Разве у него не было никаких объяснений того, что он должен был вернуться?   -  person Damien_The_Unbeliever    schedule 04.10.2012
comment
это неправильно..result не является указателем.. я думаю, вы не можете преобразовать его адрес в указатель с плавающей запятой   -  person Anirudha    schedule 04.10.2012
comment
Я нашел пример кода с использованием ILSpy в файле mscorlib/System/BitConvert/ToSingle. Никаких объяснений не было дано. Мне нужно понять, что такое поток, потому что мне нужно преобразовать его в PHP.   -  person gchimuel    schedule 04.10.2012
comment
Последняя строка: да, вы можете использовать * и &, только если у вас есть модификатор unsafe.   -  person Marc Gravell    schedule 04.10.2012
comment
почему возврат модификатора unsafe отличается от обычного возврата, в котором нет * и &. Я сравнил два возврата функции, использующей небезопасный модификатор, и обычный. Почему?   -  person gchimuel    schedule 04.10.2012
comment
return идентичен - это просто float. Вы можете представить себе лишнюю строку: float tmp = *(float*)(&result); return tmp;. Любая разница не имеет никакого отношения к return.   -  person Marc Gravell    schedule 04.10.2012
comment
Относительно разницы между return (float)result; - это преобразование - оно преобразует целое число 123 в число с плавающей запятой 123.0F - но это не повторная интерпретация; байты для 123 и 123.0F совершенно разные. Приведение типа re-interpretive просто говорит, что здесь 4 байта; теперь относитесь к ним как к float   -  person Marc Gravell    schedule 04.10.2012
comment
Обратите внимание, что метод не зависит ни от каких входных данных, поэтому все вызовы к нему можно заменить константой (2.4f, как объяснил Мартин).   -  person OrangeDog    schedule 04.10.2012
comment
Такая переинтерпретация данных с помощью указателей называется каламбуром типов.   -  person OrangeDog    schedule 04.10.2012
comment
Мне любопытно, единственный ли это способ перевести 4 байта в число с плавающей запятой. Я знаю, что это на C, но там гораздо чаще используются указатели.   -  person Earlz    schedule 04.10.2012
comment
@earlz Вы можете сделать это без использования небезопасного кода, используя структуру с явным макетом, которая помещает значения int и float в одно и то же место в памяти: stackoverflow .com/a/5981382/85661   -  person Dan Is Fiddling By Firelight    schedule 04.10.2012
comment
@gchimuel Чтобы сделать это в PHP, см. stackoverflow.com/ questions/2624869/bytes-convert-to-float-php вместо того, чтобы пытаться реконструировать то, что делает язык, который работает совершенно по-другому. Если у вас уже есть массив байтов, просто используйте распаковку.   -  person Random832    schedule 04.10.2012
comment
Спасибо за ссылку. Это действительно помогает мне.   -  person gchimuel    schedule 05.10.2012


Ответы (6)


В .NET float представляется с помощью бинарного32 IEEE числа одинарной точности с плавающей запятой, хранящегося с использованием 32 биты. Очевидно, код создает это число, собирая биты в int, а затем преобразуя его в float, используя unsafe. Приведение - это то, что в терминах C++ называется reinterpret_cast, где при выполнении приведения не выполняется преобразование - биты просто переинтерпретируются как новый тип.

IEEE одинарная точность с плавающей запятой

Собранное число равно 4019999A в шестнадцатеричном формате или 01000000 00011001 10011001 10011010 в двоичном:

  • Знаковый бит равен 0 (это положительное число).
  • Биты показателя степени равны 10000000 (или 128), в результате чего показатель степени 128 - 127 = 1 (дробь умножается на 2^1 = 2).
  • Биты дроби — это 00110011001100110011010, которые, по крайней мере, имеют почти узнаваемую структуру нулей и единиц.

Возвращаемое число с плавающей запятой имеет те же самые биты, что и 2.4, преобразованные в число с плавающей запятой, и всю функцию можно просто заменить литералом 2.4f.

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


Так в чем же разница между обычным броском и этим странным «небезопасным броском»?

Предположим, следующий код:

int result = 0x4019999A // 1075419546
float normalCast = (float) result;
float unsafeCast = *(float*) &result; // Only possible in an unsafe context

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

Второй бросок более зловещий (и может выполняться только в небезопасном контексте). &result берет адрес result, возвращая указатель на место, где хранится целое число 1075419546. Затем можно использовать оператор разыменования указателя * для извлечения значения, на которое указывает указатель. Использование *&result извлечет целое число, хранящееся в ячейке, однако при первом приведении указателя к float* (указатель на float) вместо этого из ячейки памяти извлекается число с плавающей запятой, в результате чего число с плавающей запятой 2.4f назначается unsafeCast. Таким образом, описание *(float*) &result таково: дайте мне указатель на result и предположите, что указатель является указателем на float, и извлеките значение, на которое указывает указатель.

В отличие от первого приведения, второе приведение не требует никаких вычислений. Он просто помещает 32-битное значение, хранящееся в result, в unsafeCast (которое, к счастью, также является 32-битным).

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

person Martin Liversage    schedule 04.10.2012
comment
Последний ноль, который как бы ломает битовый шаблон дроби, возможно, нужен для того, чтобы число с плавающей запятой имело точное десятичное представление? не точно само по себе - подумайте, как в десятичном виде мы округляем 2/3 до 0,66667, а не 0,66666 - person Random832; 04.10.2012
comment
+! за использование слова зловещий - person Nahum; 04.10.2012
comment
@ Random832: Вы правы в том, что 2.4 нельзя точно представить с помощью 32-битного числа с плавающей запятой (сначала я этого не осознавал), но существуют числа, которые не являются бесконечными дробями как в десятичной, так и в двоичной системе счисления. Например. 11/4 = 2,75 десятичного = 10,11 двоичного. - person Martin Liversage; 04.10.2012
comment
@MartinLiversage Я просто объяснял, почему он сделал ..999A вместо ..9999 - та же причина для 7 вместо 6 в конце 2/3 в десятичной дроби. - person Random832; 04.10.2012
comment
@ Random832: Извините, неправильно понял, но пример кажется настолько надуманным, что я предполагаю, что кто-то создал случайное число с плавающей запятой, используя битовый шаблон, а затем скорректировал последнюю цифру, чтобы сделать возможным обратный путь к литералу с плавающей запятой. - person Martin Liversage; 04.10.2012
comment
@MartinLiversage, ха-ха, хороший ответ. Ты подтолкнул меня на это! Я точно знал ответ, как только увидел заголовок вопроса (хорошее название вопроса, кстати), но я не проснулся в тот час, когда этот вопрос был задан впервые! Хороший ответ. - person Trevor Boyd Smith; 05.10.2012
comment
Не знаю, хорошо ли, что я сразу узнал ответ... или плохо... лол. (в прошлом мне нужно было брать необработанные байты и повторно интерпретировать их как значение с плавающей запятой!) - person Trevor Boyd Smith; 05.10.2012

Если я правильно интерпретирую то, что метод делает, это безопасный эквивалент:

public static float sample() {    
   int result = 154 + (153 << 8) + (25 << 16) + (64 << 24);

   byte[] data = BitConverter.GetBytes(result);
   return BitConverter.ToSingle(data, 0);
}

Как уже было сказано, он интерпретирует значение int как float.

person Bradley Smith    schedule 04.10.2012
comment
Умная конверсия! Интересно, как производительность безопасной версии по сравнению с небезопасной версией. - person Rune Grimstad; 04.10.2012
comment
@RuneGrimstad Вероятно, не так уж и здорово, учитывая, что данные копируются, а не просто приводятся к указателю другого типа. Тем не менее, это, вероятно, самый простой способ сделать это без использования небезопасного кода. - person Bradley Smith; 04.10.2012
comment
Согласованный. Если также может оказаться, что простое выполнение вычислений с плавающей запятой будет быстрее. Все еще очень хорошее решение! - person Rune Grimstad; 04.10.2012
comment
@Rune также включает дополнительное выделение byte[], если вы используете GetBytes() - person Marc Gravell; 04.10.2012
comment
Обидно, что нужен промежуточный byte[]. Существует метод BitConverter.Int64BitsToDouble(), который делает то же самое с long и double, но без метода Int32BitsToSingle()... - person Bradley Smith; 04.10.2012
comment
Вы также можете сделать это безопасно, используя союзы, то есть используя StructLayout(LayoutKind.Explicit). - person Mark Hurd; 10.10.2012
comment
Я должен сказать, что вы не можете безопасно объединиться с byte[], но вы можете перечислить конкретные байты. - person Mark Hurd; 10.10.2012
comment
@MarkHurd Можете ли вы рассказать об этом подробнее? - person Display Name; 24.09.2013

Это похоже на попытку оптимизации. Вместо выполнения вычислений с плавающей запятой вы выполняете целочисленные вычисления для целочисленного представления числа с плавающей запятой.

Помните, что числа с плавающей запятой хранятся как двоичные значения, как и целые.

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

Это не то же самое, что приведение значения к типу с плавающей запятой. Это превратит значение int 1 в число с плавающей запятой 1.0. В этом случае вы превращаете значение int в число с плавающей запятой, описываемое двоичным значением, хранящимся в int.

Объяснить нормально довольно сложно. поищу пример. :-)

Изменить: посмотрите здесь: http://en.wikipedia.org/wiki/Fast_inverse_square_root

Ваш код в основном делает то же самое, что описано в этой статье.

person Rune Grimstad    schedule 04.10.2012

Re: Что делает?

Он принимает значение байтов, хранящихся в int, и вместо этого интерпретирует эти байты как число с плавающей запятой (без преобразования).

К счастью, числа с плавающей запятой и целые имеют одинаковый размер данных из 4 байт.

person StuartLC    schedule 04.10.2012

Поскольку сержант Борш спросил, вот эквивалент «Союза»:

[StructLayout(LayoutKind.Explicit)]
struct ByteFloatUnion {
  [FieldOffset(0)] internal byte byte0;
  [FieldOffset(1)] internal byte byte1;
  [FieldOffset(2)] internal byte byte2;
  [FieldOffset(3)] internal byte byte3;
  [FieldOffset(0)] internal float single;
}

public static float sample() {
   ByteFloatUnion result;
   result.single = 0f;
   result.byte0 = 154;
   result.byte1 = 153;
   result.byte2 = 25;
   result.byte3 = 64;

   return result.single;
}
person Mark Hurd    schedule 24.09.2013
comment
Потрясающий! Кроме того, я слышал об этом в книге (CLR через C#), но не знал, что его можно использовать таким образом. - person Display Name; 24.09.2013

Как уже описывали другие, он обрабатывает байты int, как если бы они были числами с плавающей запятой.

Вы можете получить тот же результат, не используя такой небезопасный код:

public static float sample()
{
    int result = 154 + (153 << 8) + (25 << 16) + (64 << 24);
    return BitConverter.ToSingle(BitConverter.GetBytes(result), 0);
}

Но тогда это уже не будет очень быстро, и вы могли бы также использовать float/double и математические функции.

person Matthew Watson    schedule 04.10.2012
comment
Ваш ответ точно такой же, как у Брэдли Смита. - person Guillaume; 04.10.2012