Как стать автором
Обновить

Комментарии 33

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

Это вы говорите о программной реализации операций с плавающей точкой? А если реализация «железная»?

необходимость введения денормализованных чисел

Как будто это не необходимость. Вполне можно считать нулём все числа с нулевым порядком, и некоторые реализации так и делают.
Если реализация «железная» — значит вы уже переплатили 5$ за ваш процессор плюс команда выполняется на несколько тактов дольше, чем могла бы…
вы уже переплатили 5$ за ваш процессор

Вот тебе раз. Откуда вы знаете, для чего мне был нужен мой процессор?

плюс команда выполняется на несколько тактов дольше, чем могла бы

Какая команда? Почему дольше? Дольше по сравнению с чем?
В железе всё это тоже нужно реализовывать — а это и лишняя площать, лишнее тепло, лишняя сложность разводки кристалла.
Практика показывает, что специализированное железо эффективнее справляется с задачами на которые оно рассчитано, чем программная реализация на универсальном железе.
Естественно. Но тут речь про то, что железо вынуждено делать лишнюю работу, следовать более сложному алгоритму. Понятно, что это оно сделает и сделает быстрее, чем программная эмуляция. Но на это уходят ресурсы. Бесплатно ничего не бывает.
А зачем эту работу делать — не очень понятно.
железо вынуждено делать лишнюю работу

Я не вижу обоснования того, что эта работа лишняя.
При выполнении сложений/вычитаний в любом случае придётся делать относительную нормализацию операндов, так, чтобы их порядки совпадали, либо отличались на 1. Для нормализованных чисел это делается только на основании значения порядка, для ненормализованных потребуется дополнительно смотреть на мантиссу.
При умножении и делении нормализованный результат содержит наибольшее количество значащих цифр.
Для сложения и вычитания как раз всё просто. В любом случае порядки могу отличаться и надо приводить к одному порядку.
А вот для умножения и деления действительно всё сложнее. Сходу не берусь судить, но нормализовать скорее всего придётся. Возможно проще делать это именно лениво — перед делением или умножением.
Проблема со сложением-вычитанием в том, что надо определить, к какому именно порядке приводить. У нормализованных чисел — всегда к большему. У чисел автора — придётся анализировать мантиссы.

Пример: 0.0001*2^0 + 0.0001*2^4 (мантисса — 5 разрядов). Если приводить к большему порядку, то первое слагаемое обнулится. А правильный ответ 1.0001 * 2^0. Если поразмыслить, то можно придти к выводу что максимальня точность будет, когда порядки выравниваются так, что у большего слагаемого старший разряд 1. Что и есть нормализация!
Всё верно.
В случае нормализации сразу — сдвигать нужно как максимум 3 раза — две нормализации + приведение к большей мантиссе.
Если не делать нормализацию сразу — 2 приведения. Вычислять позицию старшей единички в любом слечае нужно 2 раза.
Это вы говорите о программной реализации операций с плавающей точкой? А если реализация «железная»?
Извините, я тут нечаянно не ту клавишу нажал. Выше была цитата:).Простите чайника.

Мой ответ: Не зависимо, как реализуется алгоритм, программно или в железе, сложный алгоритм реализуется сложно.
Денормализованные числа — это конечно необходимость. Именно поэтому за них так много «платят». А если убрать нормализацию, как показано в статье, то они ничем не отличаются от прочих, «нормальных» чисел.
Денормализованные числа — это конечно необходимость.

У вас есть какие-нибудь подтверждения? Я в своём комментарии привёл ссылочку на раздел «Disabling denormal floats at the code level» в статье о денормализованных числах, где утверждается ровно обратное.
Говоря, что денормализованные числа, это необходимость, я имел ввиду, что необходимостью является решение проблемы работы с малыми числами. Одной из решения такой проблемы является просто отказаться от их использования. Но ведь, как я показал в статье, отказавшись от ненужной операции нормализации, мы бесплатно получаем возможность работы с малыми числами.
как я показал в статье, отказавшись от ненужной операции нормализации, мы бесплатно получаем возможность работы с малыми числами.

Вы показали математическую эквивалентность результатов арифметических операций при отсутствии нормализации и при её наличии на конкретном примере. Из этого не следует ненужность нормализации, поскольку она может быть полезна по другим соображениям. Т.е. фразе «Любая нормализация числа является операцией, приводящей к непродуктивному использованию вычислительных ресурсов.» не хватает обоснования. Таким обоснованием мог бы быть программный код реализующий арифметику без нормализации и работающий не медленнее кода, реализующего арифметику с нормализацией, при сохранении численного равенства результатов для всех возможных значений операндов.
Из этого не следует ненужность нормализации, поскольку она может быть полезна по другим соображениям.


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

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


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


Некоторое время назад я разрабатывал эмулятор 80-битной арифметики (а её особенность в том, что 80-битное представление сохраняет единичный бит целой части в мантиссе) для 16-битного процессора. Из опыта этой разработки я могу отметить следующие моменты, приходящие в голову в связи с нормализацией:

— умножение: нормализованные аргументы можно умножать сразу, результат имеет понятное выравнивание. Т.е. умножая 1 на 1 в двоичном виде мы умножаем старшие разряды 0x8000 на 0x8000 и получаем 0x40000000. В итоговом результате надо проверить только старший бит, и если он нулевой — сдвинуть всё на один бит влево и увеличить порядок.
В отсутствие нормализации придётся либо искать значащие цифры результата, либо нормализовать два аргумента.

— сложение/вычитание: всегда сдвигается максимум один аргумент, всегда вправо.
В отсутствие нормализации может возникнуть ситуация, когда придётся сдвигать один аргумент влево, а другой вправо.

чтобы сравнить, как быстро работает сопроцессор FPU,

Для эффективности железной реализации будут другие мерки — сколько потребуется транзисторов и какова будет максимальная достижимая частота. Но тут у меня нет соображений, поскольку железом на этом уровне я почти не занимаюсь.
Т.е. умножая 1 на 1 в двоичном виде мы умножаем старшие разряды 0x8000 на 0x8000 и получаем 0x40000000. В итоговом результате надо проверить только старший бит, и если он нулевой — сдвинуть всё на один бит влево и увеличить порядок.


Поправьте меня, если я ошибаюсь. В Вашем примере 1 — это мантисса числа. В числе, возможно, присутствует характеристика 2^h_1. Т.е. первое число равно 1∙ 2^(h_1). Второе число равно 1∙ 2^(h_2). Умножаем первое на второе и получаем: 1∙ 2^(h_1)∙1∙ 2^(h_2) = 1∙ 2^(h_1+h_2). Мантисса здесь не изменилась. Изменился только порядок характеристики. Если порядок не вызвал переполнения, то он записывается в машинное слово без изменений.
Предположим теперь, что числа не нормализованы. По условию ненормализованных по стандарту чисел, мантисса машинного слова может быть только дробным числом. Тогда оба наших числа из Вашего примера оказываются за пределами разрядной сетки машинной мантиссы. Числа, которые выходят за область определения разрядной сетки машинной мантиссы должны быть приведены к необходимому виду, как при условии нормализации так и без нее. В таком виде они и должны храниться.В нашем случае числа должны быть приведены к виду, когда они меньше 0. Это похоже на нормализацию. Но условия нормализации другие. Где находится старший разряд в мантиссе слагаемого не имеет значения. Важно, чтобы полученный результат не вышел за пределы области машинной мантиссы. Если нет переполнения разрядной сетки машинной мантиссы, то результат будет верен и записывается в машинное слово в том виде, как получен при вычислении.

— сложение/вычитание: всегда сдвигается максимум один аргумент, всегда вправо.
В отсутствие нормализации может возникнуть ситуация, когда придётся сдвигать один аргумент влево, а другой вправо.

Я не могу придумать такой ситуации. При сложении двух чисел, всегда может возникнуть ситуация, когда появляется еще одна старшая единица. И если она не выходит за пределы разрядной сетки области машинной мантиссы, то результат должен быть записан в машинное слово без изменения. Если же есть переполнение, то конечно результат должен быть сдвинут на один разряд, а порядок увеличен тоже на 1 разряд.
Извините, опять напутал с выделением цитат.
Ок, давайте я распишу свои примеры более подробно:

Умножение: умножаем 1 на 1. В нормализованном виде это 2^0 * 1, в двоичном представлении: порядок 0x3fff, мантисса: 0x80000000_00000000. При умножении 64-битных мантисс получается 128-битное произведение, из которого нужно выбрать 64-битный результат. Мы выбираем старшие 64 бита (с 64го по 127й) если 127й бит равен 1 (дополнительно увеличивая экспоненту результата на 1) либо старшие 64 бита (с 63го по 126й) в противном случае.
Т.е. 0x80000000_00000000 * 0x80000000_00000000 = 0x40000000_00000000_00000000_00000000, результат: биты с 63го по 126й, мантисса: (0x3fff — 0x3fff) + (0x3fff — 0x3fff) + 0x3fff = 0x3fff.

Теперь представьте, что числа ненормализованные, т.е. единица находится в произвольном месте мантиссы.
Для того, чтобы выбрать 64 бита результата нужно просканировать произведение в поисках самого старшего установленного бита и взять 64 бита начиная с него, т.е. фактически выполнить нормализацию. «Начиная с него», а не «начиная с какого-нибудь раньше него, но так чтобы захватить его», потому что там будут значащие биты произведения, которых может быть все 128.

Я не могу придумать такой ситуации.

Речь об аргументах и их взаимной нормализации. Например вычтем (или прибавим, в данном случае не важно) из ненормализованной 1, представленной в двоичном виде как порядок: 0x403e, мантисса: 0x00000000_00000001 ненормализованную 1/4, представленную как порядок: 0x3ffd, мантисса: 0x80000000_00000000.
Для выполнения этого действия мантиссу 1 придётся сдвинуть налево, а мантиссу 1/4 — направо, так, чтобы обе единицы остались в пределах 64 бит, но старший бит 1 был бы левее старшего бита 1/4. Сдвигом только одной мантиссы этого добиться нельзя.
Чтобы огромное количество нулей не затрудняло понимания вопроса, давайте масштабируем Ваш пример:

умножаем 1 на 1. В нормализованном виде это 2^0 * 1, в двоичном представлении: порядок 0x3fff, мантисса: 0x80000000_00000000. При умножении 64-битных мантисс получается 128-битное произведение, из которого нужно выбрать 64-битный результат. Мы выбираем старшие 64 бита (с 64го по 127й) если 127й бит равен 1 (дополнительно увеличивая экспоненту результата на 1) либо старшие 64 бита (с 63го по 126й) в противном случае.
Т.е. 0x80000000_00000000 * 0x80000000_00000000 = 0x40000000_00000000_00000000_00000000, результат: биты с 63го по 126й, мантисса: (0x3fff — 0x3fff) + (0x3fff — 0x3fff) + 0x3fff = 0x3fff.


следующим образом.

Умножаем 1 на 1. В нормализованном виде это 2^0 * 1. В двоичном представлении: порядок 0x0, мантисса: 0x08. При умножении 4-битных мантисс получается 8-битное произведение, из которого нужно выбрать 4-битный результат. Мы выбираем старшие 4 бита (с 4 го по 7й) если 7й бит равен 1 (дополнительно увеличивая экспоненту результата на 1) либо старшие 4 бита (с 3го по 6й) в противном случае.
Т.е. 0x08 * 0x08 = 0x40, результат: биты с 3го по 6й, порядок равен 0.

Здесь я дословно привел Ваш пример в другом масштабе, только порядки представил без смещения. Мне кажется, что суть от этого не изменилась.

Рассмотрим теперь эту последовательность действий на шестнадцатеричных и двоичных числах:
0х08*0х08 = 0х40 = 1000*1000 = 0100 0000.

Далее дословно (с учетом масштабирования):
мы выбираем старшие 4 бита (с 4 го по 7й) если 7й бит равен 1 (дополнительно увеличивая экспоненту результата на 1) либо старшие 4 бита (с 3го по 6й) в противном случае.

У нас 7й бит равен 0, значит выбираем второй вариант.

Второй шаг равносилен сдвигу разрядов полученного числа влево на единицу. Число увеличилось в 2 раза, следовательно порядок должен быть уменьшен на 1. Будем иметь:
0100 0000 = 1000 0000*2^(-1). В старшем 7м разряде появилась единица. Следовательно, далее, по вашему алгоритму, преобразования должны завершиться. Надо выбрать 4 старших разряда и
порядок увеличить на 1. Получаем число, записанное в машинном слове, как 0х08 и порядок, равный нулю.

Рассмотрим теперь, как выглядят преобразования, когда числа не нормализуются. Поскольку мантисса машинного слова — дробная, число 1 будет представлено как 0.1*2^1. В области машинной мантиссы будет записано число 1000, а в области машинного порядка будет записано число 1. умножим это число само на себя и получим:
1000*1000 = 01000000. С учетом того, что мантисса у нас дробная, ее значение будет равно 0.01.
Порядок полученного числа будет равен 1+1=2. Сдвигаем полученное число влево до тех пор пока в старшем разряде области машинной мантиссы не появится 1. Всего нужно произвести 1 сдвиг. На такое же число уменьшаем порядок, который становится равным 2-1=1. В машинную область мантиссы записываем только 4 старших разряда. Больше не поместится. Таким образом, в результате, в машинном слове будет записано число 1000, а в области машинного порядка число 1. Что соответствует числу 0.1*2^1.
Является ли это нормализацией? Я бы назвал это оптимизацией. При больших числах нормализация и оптимизация алгоритмически совпадают. Но при маленьких числах нормализация дает совсем другой результат.

Речь об аргументах и их взаимной нормализации. Например вычтем (или прибавим, в данном случае не важно) из ненормализованной 1, представленной в двоичном виде как порядок: 0x403e, мантисса: 0x00000000_00000001 ненормализованную 1/4, представленную как порядок: 0x3ffd, мантисса: 0x80000000_00000000.
Для выполнения этого действия мантиссу 1 придётся сдвинуть налево, а мантиссу 1/4 — направо, так, чтобы обе единицы остались в пределах 64 бит, но старший бит 1 был бы левее старшего бита 1/4. Сдвигом только одной мантиссы этого добиться нельзя.


Вы правы, сдвигом мантиссы только одного числа этого не добиться.

Здесь надо подумать.
Сдвигаем полученное число влево до тех пор пока в старшем разряде области машинной мантиссы не появится 1.

Именно. При умножении номализованных аргументов сдвигов всегда либо 0, либо 1. При умножении ненормализованных — здесь цикл сканирования битов.
Согласен, нормализация дает положительный эффект при проведении математических операций. Но, видимо, до тех пор, пока речь не идет о малых числах. О таких числах, когда порядок числа достигает своего минимального значения и приходиться переходить к денормализованным числам. Если нормализацию заменить оптимизацией, т.е. числа представлять так, чтобы старший единичный разряд мантиссы числа всегда, когда это возможно, занимал крайнее левое положение, то тем самым мы получаем все преимущества нормализованных чисел. Но, в тех случаях, когда оптимизация приводит к антипереполнению, числа должны записываться без изменений. Признаком таких малых чисел является наличие нуля в старшем разряде мантиссы числа.
Чтобы сложить или вычесть два числа надо выравнять их порядки. В случае нормализованных чисел всегда сдвигается вправо мантисса числа с меньшим порядком. Влево нормализованное число сдвигаться не может по определению.
Для чисел ненормализованных, чтобы обеспечить оптимальное сложение, надо, наоборот, в числе с большим порядком сдвигать мантиссу влево и одновременно уменьшать значение порядка до тех пор пока, либо порядки не выравняются, либо, пока в старшем разряде мантиссы ни появится единица. В последнем случае, если порядки слагаемых не сравнялись, надо сдвигать мантиссу числа с меньшим порядком вправо, увеличивая значение порядка этого числа, пока порядки слагаемых не сравняются. Пример. 0.001*2^(2)+0.01*2(-1). Порядок первого слагаемого больше порядка второго слагаемого. Будем сдвигать мантиссу первого слагаемого влево: 0.001*2^(2)=0.01*2^(1)=0.1*2^(0). Дальнейшее смещение мантиссы невозможно. Будем смещать мантиссу второго слагаемого вправо: 0.01*2(-1)=0.001*2(0). Порядки сравнялись, можно найти сумму: 0.1*2^(0)+0.001*2^(0)=0.101*2^(0). Мы здесь обошлись без нормализации, а результат автоматически получился записанным оптимальным образом, т.е. дробная мантисса в старшем разряде имеет единицу.

Нормализация придумана прежде всего для решения проблемы неоднозначности представления чисел в машинном слове. Экономия же одного бита, это побочный положительный эффект, ради которого вряд-ли стали бы городить огород.
А умножение денормализованных чисел не окажется сильно дороже, чем нормализованных? А сравнение? Нормализованные положительные числа можно сравнивать так же, как целые с той же двоичной записью — они идут в том же порядке. А у денормализованных? Считать разность порядков, выполнять сдвиг, и потом сравнивать сдвинутые значения? Да это почти так же дорого, как сложение нормализованных чисел.
Всё-таки, каждое число создаётся один раз, а в операциях может участвовать многократно. Не лучше ли потратить немного времени при создании, чем вставлять дополнительную обработку аргументов каждой операции?
По правилам арифметики приведем порядки слагаемых к одинаковому значению и сложим преобразованные таким образом числа:

Вам придётся учитывать значение мантисс при этой операции, то есть фактически нормализовывать. Или будут переполнения\потери точности.
Учитывать значения мантисс при любой арифметической операции конечно необходимо, но зачем для этого нормализовывать числа? Вы же сами говорите, что учет значений мантисс, чтобы не было переполнения/потери точности, это и есть фактически нормализация. Тогда зачем нормализовывать уже нормализованное число?
При работе с большими числами нормализация происходит автоматически, при их правильной математической обработке. Проблемы возникают с малыми числами. Когда малые числа нормализуют, тогда и вылезают все неприятности. И нулем приходится жертвовать и вводить искусственно класс денормализованных чисел со всеми вытекающими…
Ну, здесь разработчикам решать, что для них важнее — добавить лишние 15 десятичных порядков в диапазон (для double это всего 2.5% от всего диапазона), или объявить, что числа, для которых требуется денормализация — это машинный нуль, и избавиться от всей этой головной боли.
Здесь я согласен! Когда у тебя есть тяжелый микроскоп, зачем еще нужен молоток.
Поскольку 13.07.15 я был off-line и не мог вовремя реагировать. Хотелось бы ответить господину jcmvbkbc относительно проблемы нуля.
В нормализованных числах есть проблема получения нуля в явном виде. Отсюда возникает проблема сравнения на равенство двух чисел.
Для нормализованных чисел два числа считаются равными, если они отличаются друг от друга на «бесконечно» малую величину. Но, согласитесь, что с математической точки зрения, нуль и число, близкое к нулю, вещи разные.
Если, как предложено в статье, отказаться от нормализации чисел, то эта проблема с нулем решается, т.к. нуль представлен в явном виде… Одновременно появляется возможность оперировать с таким понятием, как бесконечно малое число.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории