Чому C не має підписаних плавців?


131

Я знаю, питання видається дивним. Програмісти іноді занадто багато думають. Будь ласка, читайте далі ...

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

Мені подобаються ці попередження. Вони допомагають мені правильний код.

Чому б ми не мали однакової розкоші для поплавців? Квадратний корінь точно ніколи не поверне від’ємне число. Є й інші місця, де від'ємне значення поплавця не має значення. Ідеальний кандидат для непідписаного поплавця.

Btw - Я не дуже захоплююсь єдиною додатковою точністю точності, яку я міг би отримати, видаливши шматочок знака з поплавків. Я дуже задоволений floats s, як вони зараз. Я просто хотів би позначити float часом без підпису і отримувати такі ж попередження, які я отримую з цілими числами.

Мені не відома жодна мова програмування, яка підтримує неподписані числа з плаваючою комою.

Будь-яка ідея, чому їх не існує?


Редагувати:

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


4
Цікаво, чи можете ви розмістити приклад випадку, коли перевірка типу підпису була корисною?

лампа, чи був ваш коментар спрямований на мене? якщо так, я не отримую цього

Так, Iraimbilanja так: fabs не може повернути від'ємне число, оскільки воно повертає абсолютне значення його аргументу
Йоханнес Шауб - litb

Right.i не запитав, як гіпотетичний непідписаний плавець може допомогти corectness. Що я запитував: у якій ситуації pipenbrinck вважає корисним перевірку підпису Int (що веде його шукати той самий механізм для поплавців). що стосується безпеки безпеки

1
Існує безпідписана мікрооптимізація для перевірки точки в діапазоні: ((неподписана) (р-хв)) <(макс-хв), яка має лише одну гілку, але, як завжди, найкраще в профілі, щоб побачити, чи це дійсно допомагає (я в основному використовував його на 386 ядрах, тому я не знаю, як справляються сучасні процесори).
Skizz

Відповіді:


114

Чому C ++ не підтримує неподписані плавці, тому що для процесора не існує еквівалентних операцій машинного коду. Тож було б дуже неефективно його підтримувати.

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

Отже, питання полягає в тому, чому не підтримують це програмні засоби. І я думаю, що відповідь на це полягає в тому, що не було визначеного неподписаного стандарту float спочатку. Оскільки мови люблять бути зворотними сумісними, навіть якщо вони були додані, мови не могли ним користуватися. Щоб побачити специфікацію з плаваючою точкою, слід переглянути стандарт IEEE 754 з плаваючою точкою .

Ви можете обійти, не маючи неподписаного типу з плаваючою точкою, хоча створивши непідписаний поплавковий клас, який інкапсулює поплавок або подвійний і кидає попередження, якщо ви спробуєте передати негативне число. Це менш ефективно, але, ймовірно, якщо ви не використовуєте їх інтенсивно, вам не буде байдуже ця незначна втрата продуктивності.

Я, безумовно, бачу корисність наявності неподписаного поплавця. Але C / C ++, як правило, обирає ефективність, яка найкраще працює для всіх над безпекою.


17
C / C ++ не вимагає конкретних операцій машинного коду для реалізації мови. Ранні компілятори C / C ++ можуть генерувати код з плаваючою комою для 386 - ЦП без FPU! Компілятор генерує бібліотечні виклики для імітації інструкцій FPU. Тому ufloat можна зробити без підтримки процесора
Skizz

10
Скізз, хоча це правильно, Брайан вже вирішив це - тому, що немає еквівалентного машинного коду, продуктивність буде жахливою для порівняння.
Ентоні

2
@Brian R. Bondy: Я втратив вас тут: "тому що не існує еквівалентних операцій машинного коду для виконання процесора ...". Чи можете ви поясніть, простіше кажучи?
Lazer

2
Причина, за якою OP хотіла підтримку непідписаних плавців, полягала в попереджувальних повідомленнях, тому насправді це не має нічого спільного з фазою генерації коду компілятора - лише з тим, як він попередньо перевіряє тип - тому підтримка їх у машинному коді не має значення. і (як було додано в нижній частині питання) звичайні інструкції з плаваючою комою можуть використовуватися для фактичного виконання.
Джо F

2
Я не впевнений, що бачу, чому це має вплинути на продуктивність. Так само, як і у ints, всі перевірки типу знаків можуть відбуватися під час компіляції. ОП пропонує, що це 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: якщо представлення є проблемою, ви маєте на увазі, якщо хтось може розробити метод збереження плаваючих файлів, який настільки ж ефективний, як 2's complementне матиме неподписаних плавців?
Lazer

3
value >> shift for signed values leave the top bit unchanged (sign extend) Ви впевнені в цьому? Я вважав, що це поведінка, визначена реалізацією, принаймні для негативних знаків, що підписуються.
Дан

@Dan: Просто переглянув останній стандарт, і він дійсно стверджує, що його визначено реалізацією - я думаю, це на всякий випадок, якщо є процесор, який не має права змінити інструкцію щодо розширення знаків.
Skizz


1
плаваюча точка традиційно насичує (до - / + інф) замість обгортання. Можливо, ви очікуєте, що переливання без підписання віднімається для насичення 0.0, або, можливо, до інф або NaN. Або просто бути невизначеною поведінкою, як запропонована в редакції запитання ОП. Re: триггерні функції: тому не визначайте версії без підпису sinта інше, і обов'язково трактуйте їх повернене значення як підписане. Питання не пропонувало замінити float непідписаним float, просто додавши його unsigned floatяк новий тип.
Пітер Кордес

9

(Убік, Perl 6 дозволяє писати

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

і тоді ви можете використовувати так Nonnegative::Floatсамо, як і будь-який інший тип.)

Немає апаратної підтримки для безпідписаних операцій з плаваючою точкою, тому C не пропонує її. C здебільшого розроблений як "переносна збірка", тобто настільки ж близька до металу, як ви можете бути, не прив'язуючись до певної платформи.

[редагувати]

C - це як збірка: те, що ти бачиш, саме те, що ти отримуєш. Неявне "Я перевірте, чи цей поплавок не є негативним для вас" суперечить його філософії дизайну. Якщо ви дійсно хочете цього, можете додати assert(x >= 0)або подібне, але це потрібно зробити явно.


svn.perl.org/parrot/trunk/languages/perl6/docs/STATUS каже "так", але of ...не аналізує.
ефемія

8

Я вважаю, що непідписаний int був створений через необхідність більшої вартості, ніж підписаний Int.

Поплавок має набагато більший запас, тому ніколи не було фізичної потреби в неподписаному поплавці. І як ви самі себе вказуєте у своєму питанні, додаткова 1-бітова точність не є для чого вбивати.

Редагувати: Прочитавши відповідь Брайана Р. Бонді , я повинен змінити свою відповідь: Він, безумовно, правий, що в центральних процесорах не було операцій з безпідписною плаваючою операцією. Однак я переконаний, що це було дизайнерське рішення, виходячи з причин, про які я говорив вище ;-)


2
Крім того, додавання і віднімання цілих чисел - це те ж саме підписане або непідписане - плаваюча точка, не так багато. Хто зробив би додаткову роботу для підтримки як підписаних, так і неподписаних плавців, враховуючи відносно низьку граничну корисність такої функції?
ефемієнт

7

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

Тож принаймні для цілих чисел потреба в окремому неподписаному типі сильніша, ніж у простому попередженні. Усі вищезазначені моменти не потрібно вважати плавцями. Отже, я думаю, реальної потреби в апаратній підтримці для них немає, і C вже не підтримує їх.


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;
}

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


Так, але ви викликаєте функцію з іншим іменем, якщо ви працюєте зі складними числами. Також тип повернення відрізняється. Хороший момент, хоча!
Nils Pipenbrinck

4
Результат sqrt (i) - складне число. А оскільки складні числа не впорядковані, ви не можете сказати, що складне число є негативним (тобто <0)
квінмар

1
quinmars, впевнений, що це не csqrt? чи ти говориш про математику замість C? Я погоджуюсь все-таки, що це вдалий момент :)
Йоханнес Шауб - ліб

Дійсно, я говорив про математику. Я ніколи не мав справу зі складними числами в c.
квінмар

1
"Квадратний корінь напевно ніколи не поверне від'ємне число." -> sqrt(-0.0)часто виробляє -0.0. Звичайно -0,0 не є негативним значенням .
chux

4

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

Стаття у Вікіпедії про номери IEEE-754 з плаваючою комою

Редагувати: Крім того, як зазначають інші, більшість апаратних засобів не підтримує негативні поплавці, тому звичайний тип плаваючих елементів є більш ефективним, оскільки існує апаратне забезпечення.


C був введений задовго до появи стандарту IEEE-754
phuclv

@phuclv Не було звичним обладнанням з плаваючою точкою. Він був прийнятий у стандарт С "через кілька років". Напевно, в Інтернеті про нього лежить якась документація. (Також у статті вікіпедії згадується С99).
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 з'явилося покоління пізніше, з деякою технологією, яка була поколінням позаду.
ефемія

3

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


1

IHMO це тому, що підтримка як підписаних, так і неподписаних типів з плаваючою комою або в апаратному, або в програмному забезпеченні буде занадто клопіткою

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

  • Арифметичний та логічний зсув потребують лише незначної зміни наповнювача для верхніх бітів
  • Розширення множення може використовувати одне і те ж апаратне забезпечення для основної частини, а потім деяку окрему логіку для коригування результату для зміни ознаки . Мало того, що він використовується у справжніх множниках, але це можливо зробити
  • Підписане порівняння можна легко перетворити на неподписане порівняння і навпаки легко, перемикаючи верхній біт або додаючи йогоINT_MIN . Також теоретично можливо, він, ймовірно, не використовується на апаратному забезпеченні, але він корисний у системах, які підтримують лише один тип порівняння (наприклад, 8080 або 8051)

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

На жаль, ми не є такою розкішшю для типів з плаваючою комою. Просто звільнивши бітовий знак, ми отримаємо безпідписану версію. Але тоді для чого ми повинні використовувати цей біт?

  • Збільшити діапазон, додавши його до показника
  • Підвищити точність, додавши її до мантіси. Це часто корисніше, оскільки ми, як правило, потребуємо більшої точності, ніж дальності

Але обидва варіанти потребують більшої суми суми, щоб пристосуватися для ширшого діапазону значень. Це збільшує складність логіки, в той час як верхній біт додатка сидить там невикористану більшу частину часу. Ще більше схеми знадобиться для множення, поділу чи інших складних операцій

У системах, що використовують програмне забезпечення з плаваючою комою, вам потрібні 2 версії для кожної функції, що не очікувалося за той час, коли пам’ять була настільки дорогою, або вам доведеться знайти якийсь «хитрий» спосіб ділитися частинами підписаних і непідписаних функцій

Однак апаратне забезпечення з плаваючою комою існувало задовго до того, як був винайдений C , тому я вважаю, що вибір у C був пов'язаний з відсутністю апаратної підтримки через причину, яку я згадав вище

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


0

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


Чи були у базових процесорів гарні способи поводження з підписаними номерами з плаваючою комою? C набував популярності, коли допоміжні процесори з плаваючою комою були ідіосинкратичними та навряд чи універсальними.
Девід Торнлі

1
Я не знаю всіх історичних термінів, але з'явилася нова апаратна підтримка підписаних плавців, хоча і рідкість, як ви вказуєте. Мовні дизайнери можуть включити підтримку для нього, тоді як програмні засоби компіляції мали різний рівень підтримки в залежності від цільової архітектури.
Брайан Енсінк

0

Гарне питання.

Якщо, як ви кажете, це стосується лише попереджень під час компіляції і не змінюється в їхній поведінці, інакше це не впливає на базове обладнання, і як таке воно буде лише зміною C ++ / Compiler.

Раніше я те саме обмотував, але річ така: це мало би допомогло. У кращому випадку компілятор може знайти статичні завдання.

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

Або мінімалістично довше

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

Але саме про це. З типовими цілими числами без підпису ви також отримуєте визначене обгортання, а саме воно поводиться як модульна арифметика.

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

після цього 'uc' має значення 255.

Тепер, що буде робити компілятор із тим самим сценарієм, надаючи неподписаний float-тип? Якщо значення невідомі під час компіляції, потрібно створити код, який спочатку виконує обчислення, а потім робить перевірку знаків. Але що, коли результатом такого обчислення буде сказати «-5,5» - яке значення слід зберігати у поплавці, оголошеному неподписаним? Можна спробувати модульну арифметику, як для інтегральних типів, але це пов'язано зі своїми проблемами: Найбільше значення - це безперечно нескінченність .... що не працює, не можна мати "нескінченність - 1". Переміщення за найбільшу цінність, яку він може мати, також не спрацює, оскільки там ви натрапите на її точність. "NaN" був би кандидатом.

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.