Денормализованные числа С#

Недавно я столкнулся с денормализованным определением, и я понимаю, что есть некоторые числа, которые не могут быть представлены в нормализованной форме, потому что они слишком малы, чтобы вписаться в соответствующий тип. Согласно IEEE

введите описание изображения здесь

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

private bool IsDenormalizedNumber(float number)
{
    return Math.Pow(2, -149) <= number && number<= ((2-Math.Pow(2,-23))*Math.Pow(2, -127)) ||
           Math.Pow(-2, -149) <= number && number<= -((2 - Math.Pow(2, -23)) * Math.Pow(2, -127));
}

Является ли моя интерпретация правильной?

Ответ 1

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

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

Что касается того, почему он работает... Показатель хранится в офсетной нотации. 8 бит экспоненты могут принимать значения от 1 до 254 (0 и 255 зарезервированы для особых случаев), затем они сдвигаются с шагом -127, что дает нормализованный диапазон от -126 (1-127) до 127 (254-127). Экспоненту присваивается значение 0 в денормальном случае. Я думаю, что это требуется только потому, что .NET не хранит ведущий бит в значении. Согласно IEEE 754, он может храниться в любом случае. Похоже, что С# решила отказаться от него в пользу знакового бита, хотя у меня нет конкретных подробностей, чтобы поддержать это наблюдение.

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

ПРИМЕЧАНИЕ.. При обсуждении комментариев этот код основывается на деталях реализации платформы (x86_64 в этом тестовом примере). Как отметил @ChiuneSugihara, CLI не обеспечивает такого поведения и может отличаться на других платформах, таких как ARM.

using System;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("-120, denormal? " + IsDenormal((float)Math.Pow(2, -120)));
            Console.WriteLine("-126, denormal? " + IsDenormal((float)Math.Pow(2, -126)));
            Console.WriteLine("-127, denormal? " + IsDenormal((float)Math.Pow(2, -127)));
            Console.WriteLine("-149, denormal? " + IsDenormal((float)Math.Pow(2, -149)));
            Console.ReadKey();
        }

        public static bool IsDenormal(float f)
        {
            // when 0, the exponent will also be 0 and will break
            // the rest of this algorithm, so we should check for
            // this first
            if (f == 0f)
            {
                return false;
            }
            // Get the bits
            byte[] buffer = BitConverter.GetBytes(f);
            int bits = BitConverter.ToInt32(buffer, 0);
            // extract the exponent, 8 bits in the upper registers,
            // above the 23 bit significand
            int exponent = (bits >> 23) & 0xff;
            // check and see if anything is there!
            return exponent == 0;
        }
    }
}

Вывод:

-120, denormal? False
-126, denormal? False
-127, denormal? True
-149, denormal? True

Источники:
извлечение мантиссы и экспонента из двойника в С#
https://en.wikipedia.org/wiki/IEEE_floating_point
https://en.wikipedia.org/wiki/Denormal_number
http://csharpindepth.com/Articles/General/FloatingPoint.aspx

Код, адаптированный из:
извлечение мантиссы и экспонента из double в С#

Ответ 2

Из моего понимания денормализованные числа в некоторых случаях помогают в недополнении (см. ответ на Денормализованные числа - плавающая точка IEEE 754).

Итак, чтобы получить денормализованное число, вам нужно будет явно создать его или вызвать недоиспользование. В первом случае маловероятно, что буквальное денормализованное число будет указано в коде, и даже если кто-то его попробовал, я не уверен, что .NET это позволит. Во втором случае, если вы находитесь в контексте checked, вы должны получить OverflowException для любого переполнения или underflow в арифметическом вычислении, чтобы предотвратить возможность получения денормализованного числа. В контексте unchecked я не уверен, приведет ли underflow к денормализованному номеру, но вы можете попробовать его и посмотреть, хотите ли вы выполнять вычисления в unchecked.

Короче говоря, вы не можете беспокоиться об этом, если вы работаете в checked, и попробуйте использовать нижний поток и посмотрите в unchecked, если вы хотите запустить в этом контексте.

ИЗМЕНИТЬ

Я хотел обновить свой ответ, так как комментарий не ощущался достаточно существенным. Во-первых, я удалил комментарий, который я сделал о контексте checked, поскольку это относится только к вычислениям без плавающей запятой (например, int), а не к float или double. Это была моя ошибка в этом.

Проблема с денормализованными номерами заключается в том, что они не являются согласованными в CLI. Обратите внимание, как я использую "CLI", а не "С#", потому что для понимания проблемы нам нужно идти ниже уровня, чем просто С#. Из общей инфраструктуры языка Аннотированная стандартная часть я Раздел 12.1.3 вторая записка (стр. 125 книги) гласит:

В этом стандарте не указывается поведение арифметических операций с денормализованными числами с плавающей запятой, а также не указывается, когда или должны создаваться такие представления. Это соответствует IEC 60559: 1989. Кроме того, в этом стандарте не указывается, как получить доступ к точной схеме бит созданных NaN, а также к поведению при преобразовании NaN между 32-битным и 64-битным представлением. Все это поведение преднамеренно исключено для реализации.

Таким образом, на уровне CLI обработка денормализованных чисел намеренно оставлена ​​для конкретной реализации. Кроме того, если вы посмотрите на документацию для float.Epsilon (найдено здесь), которая является наименьшим положительным числом, представляемым поплавком, вы получите денормализованное число на большинстве машин, которое соответствует тому, что указано в документации (примерно 1,4e-45). Это то, что @Kevin Burdett, скорее всего, видел в его ответе. Если вы прокрутите страницу вниз, вы увидите следующую цитату в разделе "Заметки о платформе"

В системах ARM значение константы Epsilon слишком мало для обнаружения, поэтому оно равно нулю. Вы можете определить альтернативное значение эпсилона, равное 1.175494351E-38.

Таким образом, есть проблемы с переносимостью, которые могут вступить в игру, когда вы имеете дело с ручным обращением с денормализованными номерами даже для .NET CLR (это реализация CLI). На самом деле это специфическое значение ARM является интересным, поскольку оно выглядит как нормированное число (я использовал функцию из @Kevin Burdett с IsDenormal(1.175494351E-38f) и вернул false). В самой CLI проблемы более серьезны, поскольку стандартизация их обработки по дизайну не выполняется в соответствии с аннотацией стандарта CLI. Таким образом, это оставляет вопросы о том, что произойдет с одним и тем же кодом в Mono или Xamarin, например, это разностная реализация CLI, чем .NET CLR.

В конце концов я вернусь к моему предыдущему совету. Просто не беспокойтесь о денормализованных числах, они там, чтобы помочь вам, и трудно представить, почему вам нужно будет специально выделить их. Также, как сказал @HansPassant, вы, скорее всего, даже не столкнетесь. Трудно представить, как бы вы шли под наименьшим положительным нормализованным номером в double, что абсурдно мало.