Почему в C нет беззнаковых чисел с плавающей запятой?

131

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

В CI много использую signedи unsignedцелые числа. Мне нравится, что компилятор предупреждает меня, если я делаю такие вещи, как присвоение целого числа со знаком беззнаковой переменной. Я получаю предупреждения, если сравниваю подписанные с целыми числами без знака и многое другое.

Мне нравятся эти предупреждения. Они помогают мне поддерживать правильный код.

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

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

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

Есть идеи, почему их не существует?


РЕДАКТИРОВАТЬ:

Я знаю, что FPU x87 не имеет инструкций по работе с беззнаковыми числами с плавающей запятой. Давайте просто воспользуемся подписанными инструкциями с плавающей запятой. Неправильное использование (например, опускание ниже нуля) может рассматриваться как неопределенное поведение точно так же, как переполнение целых чисел со знаком не определено.

Нильс Пипенбринк
источник
4
Интересно, можете ли вы опубликовать пример случая, когда проверка типов подписи была полезной?
litb, ваш комментарий был адресован мне? если так, то я не понимаю
Iraimbilanja, да :) fabs не может возвращать отрицательное число, потому что возвращает абсолютное значение своего аргумента
Йоханнес Шауб - litb
Правильно. Я не спрашивал, как гипотетический unsignedfloat может помочь в корректности. Я спросил: в какой ситуации pipenbrinck счел полезной проверку типов подписи Int (заставившую его искать тот же механизм для float). Причина, по которой я спрашиваю, заключается в том, что я считаю неподписанные совершенно бесполезными Что касается типовой безопасности
1
Существует беззнаковая микрооптимизация для проверки точки в диапазоне: ((unsigned) (p-min)) <(max-min), которая имеет только одну ветвь, но, как всегда, лучше всего профилировать, чтобы увидеть, это действительно помогает (я в основном использовал его на 386 ядрах, поэтому не знаю, как справляются современные процессоры).
Skizz 05

Ответы:

114

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

Если бы C ++ поддерживал это, вы бы иногда использовали беззнаковое число с плавающей запятой и не осознавали, что ваша производительность просто упала. Если бы C ++ поддерживал это, то каждую операцию с плавающей запятой нужно было бы проверять, чтобы узнать, подписана она или нет. А для программ, которые выполняют миллионы операций с плавающей запятой, это неприемлемо.

Таким образом, возникает вопрос, почему разработчики оборудования не поддерживают его. И я думаю, что ответ на этот вопрос заключается в том, что изначально не существовало стандарта беззнаковых чисел с плавающей запятой. Поскольку языки любят быть обратно совместимыми, даже если они были добавлены, языки не могли их использовать. Чтобы увидеть спецификацию с плавающей запятой, вы должны взглянуть на стандарт IEEE 754 Floating-Point .

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

Я определенно вижу полезность беззнакового числа с плавающей точкой. Но C / C ++ предпочитает безопасность, которая работает лучше всего для всех.

Брайан Р. Бонди
источник
18
C / C ++ не требует определенных операций машинного кода для реализации языка. Ранние компиляторы C / C ++ могли генерировать код с плавающей запятой для 386 - ЦП без FPU! Компилятор будет генерировать вызовы библиотеки для эмуляции инструкций FPU. Следовательно, ufloat может быть выполнен без поддержки процессора
Skizz
10
Скиз, хотя это и верно, Брайан уже обратился к этому - поскольку нет эквивалентного машинного кода, производительность по сравнению с ним будет ужасной.
Энтони
2
@ Брайан Р. Бонди: Я потерял вас здесь: «потому что нет эквивалентных операций машинного кода для выполнения ЦП ...». Не могли бы вы объяснить попроще?
Lazer
2
Причина, по которой OP нуждалась в поддержке беззнаковых чисел с плавающей запятой, заключалась в предупреждающих сообщениях, поэтому на самом деле это не имеет ничего общего с фазой генерации кода компилятора - только с тем, как он выполняет предварительную проверку типа - поэтому поддержка их в машинном коде не имеет отношения и (как было добавлено в конце вопроса) для фактического выполнения могут использоваться обычные инструкции с плавающей запятой.
Joe F
2
Я не уверен, что понимаю, почему это должно влиять на производительность. Как и в случае с int, вся проверка типов, связанная со знаком, может происходить во время компиляции. OP предполагает, что unsigned floatэто будет реализовано как обычное floatс проверками во время компиляции, чтобы гарантировать, что некоторые бессмысленные операции никогда не выполняются. Полученный машинный код и производительность могут быть идентичными, независимо от того, подписаны ли ваши числа с плавающей запятой или нет.
xanderflood
14

Существует значительная разница между целыми числами со знаком и без знака в C / C ++:

value >> shift

Значения со знаком оставляют верхний бит неизменным (расширение знака), значения без знака очищают верхний бит.

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

float a = 2.0f, b = 10.0f, c;
c = a - b;

Какое значение имеет c? -8. Но что бы это значило в системе без отрицательных чисел. FLOAT_MAX - 8 возможно? На самом деле, это не работает, поскольку FLOAT_MAX - 8 - это FLOAT_MAX из-за эффектов точности, так что все еще хуже. Что, если бы это было частью более сложного выражения:

float a = 2.0f, b = 10.0f, c = 20.0f, d = 3.14159f, e;
e = (a - b) / d + c;

Это не проблема для целых чисел из-за природы системы дополнения до 2.

Также рассмотрите стандартные математические функции: sin, cos и tan будут работать только для половины их входных значений, вы не можете найти журнал значений <1, вы не можете решить квадратные уравнения: x = (-b +/- root ( bb - 4.ac)) / 2.a и так далее. Фактически, это, вероятно, не сработает для какой-либо сложной функции, поскольку они, как правило, реализуются как полиномиальные приближения, которые будут где-то использовать отрицательные значения.

Итак, беззнаковые числа с плавающей точкой бесполезны.

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

Skizz
источник
@Skizz: если представление - это проблема, вы имеете в виду, что если кто-то может разработать метод для хранения чисел с плавающей запятой, который 2's complementбудет столь же эффективным, как не возникнет проблем с наличием чисел с плавающей запятой без знака?
Lazer
3
value >> shift for signed values leave the top bit unchanged (sign extend) Вы уверены, что? Я думал, что это поведение определяется реализацией, по крайней мере, для отрицательных значений со знаком.
Дэн
@Dan: Только что взглянул на недавний стандарт, и он действительно утверждает, что он определен реализацией - я думаю, это на всякий случай, если есть процессор, у которого нет сдвига вправо с инструкцией расширения знака.
Skizz 05
1
с плавающей запятой традиционно насыщается (до - / + Inf) вместо переноса. Вы можете ожидать, что переполнение беззнакового вычитания перейдет в насыщение 0.0или, возможно, до Inf или NaN. Или просто используйте Undefined Behavior, как OP, предложенный в редактировании вопроса. Re: функции триггера: поэтому не определяйте версии беззнакового ввода sinи т. Д., И убедитесь, что их возвращаемое значение обрабатывается как подписанное. Вопрос не в том, чтобы заменить float на unsigned float, а просто добавить его unsigned floatкак новый тип.
Питер Кордес,
9

(Кстати, Perl 6 позволяет писать

subset Nonnegative::Float of Float where { $_ >= 0 };

а затем вы можете использовать так Nonnegative::Floatже, как и любой другой тип.)

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

[редактировать]

C похож на сборку: то, что вы видите, именно то, что вы получаете. Неявное выражение «Я проверю, что это значение для вас неотрицательно» противоречит его философии дизайна. Если вы действительно этого хотите, вы можете добавить что- assert(x >= 0)то подобное, но сделать это нужно явно.

ephemient
источник
svn.perl.org/parrot/trunk/languages/perl6/docs/STATUS говорит "да", но of ...не выполняет синтаксический анализ.
ephemient
8

Я считаю, что unsigned int был создан из-за необходимости большего запаса значения, чем может предложить подписанный int.

Плавающий имеет гораздо больший запас, поэтому в беззнаковом поплавке никогда не было «физической» необходимости. И, как вы указываете на себя в своем вопросе, дополнительная точность в 1 бит не за что.

Изменить: прочитав ответ Брайана Р. Бонди , я должен изменить свой ответ: он определенно прав в том, что базовые процессоры не имели операций с плавающей запятой без знака. Тем не менее, я считаю, что это было дизайнерское решение, основанное на причинах, которые я указал выше ;-)

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

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

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

Йоханнес Шауб - litb
источник
5

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

C99 поддерживает комплексные числа и типичную форму sqrt, поэтому sqrt( 1.0 * I)будет отрицательным.


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

#include <complex.h>
#include <tgmath.h>

int main () 
{
    complex double a = 1.0 + 1.0 * I;

    double f = sqrt(a);

    return 0;
}

Он также содержит мозговое пукание, поскольку действительная часть sqrt любого комплексного числа положительна или равна нулю, а sqrt (1.0 * I) равна sqrt (0.5) + sqrt (0.5) * I не -1.0.

Пит Киркхэм
источник
Да, но вы вызываете функцию с другим именем, если работаете с комплексными числами. Также другой тип возврата. Хороший момент!
Нильс Пипенбринк,
4
Результат sqrt (i) - комплексное число. А поскольку комплексные числа не упорядочены, нельзя сказать, что комплексное число отрицательно (т.е. <0)
quinmars
1
quinmars, а это не csqrt? или вы говорите о математике вместо C? в любом случае я согласен, что это хорошее замечание :)
Йоханнес Шауб - литб
Действительно, я говорил о математике. Я никогда не имел дела с комплексными числами в c.
quinmars,
1
"квадратный корень определенно никогда не вернет отрицательное число". -> sqrt(-0.0)часто производит -0.0. Конечно, -0.0 не является отрицательным значением .
chux - Восстановить Монику
4

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

Статья в Википедии о числах с плавающей запятой IEEE-754

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

Тобиас Верре
источник
C был представлен задолго до появления стандарта IEEE-754
phuclv
@phuclv Также не было обычным оборудованием с плавающей запятой. Через несколько лет он был принят в стандарт C. Вероятно, в Интернете есть какая-то документация по этому поводу. (Также в статье в Википедии упоминается C99).
Tobias Wärre
Я не понимаю о чем ты. В вашем ответе нет «оборудования», и IEEE-754 родился после C, поэтому типы с плавающей запятой в C не могут зависеть от стандарта IEEE-754, если только эти типы не были введены в C намного позже
phuclv
@phuclv C также известен как портативная сборка, поэтому он может быть довольно близок к аппаратному обеспечению. Языки с годами приобретают новые возможности, даже если (до меня) float был реализован на C, это, вероятно, было программной операцией и довольно дорого. Во время ответа на этот вопрос я, очевидно, лучше понимал то, что пытался объяснить, чем сейчас. И если вы посмотрите на принятый ответ, вы поймете, почему я упомянул стандарт IEE754. Я не понимаю, что вы придрались к ответу 10-летней давности, который не является принятым?
Tobias Wärre
3

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

Ферруччо
источник
4
PDP-7, первая платформа C, имела дополнительный аппаратный модуль с плавающей запятой. PDP-11, следующая платформа C, имела аппаратное 32-битное число с плавающей запятой. 80x86 появился поколением позже, с некоторыми технологиями, которые отстали от поколения.
ephemient 05
3

Целочисленные типы без знака в C определены таким образом, чтобы подчиняться правилам абстрактного алгебраического кольца. Например, для любых значений X и Y добавление XY к Y даст X. Целочисленные типы без знака гарантированно подчиняются этим правилам во всех случаях, которые не включают преобразование в любой другой числовой тип или из него [или типы без знака разных размеров] , и эта гарантия - одна из важнейших особенностей таких типов. В некоторых случаях стоит отказаться от возможности представлять отрицательные числа в обмен на дополнительные гарантии, которые могут предоставить только беззнаковые типы. Типы с плавающей запятой, независимо от того, подписаны они или нет, не могут соответствовать всем правилам алгебраического кольца [например, они не могут гарантировать, что X + YY будет равно X], и действительно, IEEE этого не делает. t даже позволяют им соблюдать правила класса эквивалентности [требуя, чтобы определенные значения сравнивались не равными самим себе]. Я не думаю, что «беззнаковый» тип с плавающей запятой может подчиняться каким-либо аксиомам, которых не может придерживаться обычный тип с плавающей запятой, поэтому я не уверен, какие преимущества он может предложить.

Supercat
источник
1

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

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

  • Арифметический и логический сдвиг требует лишь небольшого изменения заполнителя для старших битов.
  • Расширяющее умножение может использовать одно и то же оборудование для основной части, а затем некоторую отдельную логику для корректировки результата, чтобы изменить знак . Не то чтобы это использовалось в реальных множителях, но это возможно
  • Сравнение со знаком можно легко преобразовать в сравнение без знака и наоборот, переключив верхний бит или добавивINT_MIN . Также теоретически возможно, он, вероятно, не используется на оборудовании, но он полезен в системах, которые поддерживают только один тип сравнения (например, 8080 или 8051).

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

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

  • Увеличьте диапазон, добавив его к экспоненте
  • Увеличьте точность, добавив его к мантиссе. Это часто бывает более полезно, поскольку нам обычно требуется больше точности, чем диапазон

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

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

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

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

phuclv
источник
0

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

Брайан Энсинк
источник
Были ли у базовых процессоров хороший способ работы с числами с плавающей запятой со знаком? C становился популярным, когда вспомогательные процессоры с плавающей запятой были своеобразными и вряд ли универсальными.
Дэвид Торнли,
1
Я не знаю всех исторических графиков, но появлялась аппаратная поддержка подписанных плавающих объектов, хотя, как вы отметили, редко. Разработчики языка могли включить его поддержку, в то время как серверные части компилятора имели различные уровни поддержки в зависимости от целевой архитектуры.
Брайан Энсинк,
0

Хороший вопрос.

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

Раньше я искал то же самое, но дело в том, что это мало поможет. В лучшем случае компилятор может находить статические назначения.

unsigned float uf { 0 };
uf = -1f;

Или минималистично длиннее

unsigned float uf { 0 };
float f { 2 };
uf -= f;

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

unsigned char uc { 0 };
uc -= 1;

после этого 'uc' содержит значение 255.

Теперь, что бы компилятор сделал с тем же сценарием, учитывая беззнаковый тип с плавающей запятой? Если значения не известны во время компиляции, потребуется сгенерировать код, который сначала выполняет вычисления, а затем выполняет проверку знака. Но что, если результатом такого вычисления будет «-5,5» - какое значение должно быть сохранено в объявлении с плавающей запятой без знака? Можно было бы попробовать модульную арифметику, как для целочисленных типов, но это приходит со своими проблемами: Самое большое значение неоспоримо бесконечность .... это не работает, вы не можете иметь «бесконечность - 1». Поиск самого большого отличного значения, которое он может удерживать, также не будет работать, так как вы столкнетесь с его точностью. "NaN" будет кандидатом.

Наконец, это не будет проблемой с числами с фиксированной точкой, поскольку там модуль хорошо определен.

ABaumstumpf
источник