Чи порівняння рівності чисел з плавкою вводить в оману молодших розробників, навіть якщо в моєму випадку не виникає помилка округлення?


31

Наприклад, я хочу показати список кнопок від 0,0,5, ... 5, який переходить на кожні 0,5. Для цього я використовую цикл і мають інший колір на кнопці STANDARD_LINE:

var MAX=5.0;
var DIFF=0.5
var STANDARD_LINE=1.5;

for(var i=0;i<=MAX;i=i+DIFF){
    button.text=i+'';
    if(i==STANDARD_LINE){
      button.color='red';
    }
}

У цьому випадку не повинно бути помилок округлення, оскільки кожне значення є точним в IEEE 754. Але я намагаюся змінити його, щоб уникнути порівняння рівності з плаваючою точкою:

var MAX=10;
var STANDARD_LINE=3;

for(var i=0;i<=MAX;i++){
    button.text=i/2.0+'';
    if(i==STANDARD_LINE/2.0){
      button.color='red';
    }
}

З одного боку, оригінальний код для мене більш простий і передовий. Але є одне, що я розглядаю: чи == STANDARD_LINE вводить в оману молодших товаришів по команді? Чи приховує той факт, що числа з плаваючою комою можуть мати помилки округлення? Прочитавши коментарі до цієї публікації:

https://stackoverflow.com/questions/33646148/is-hardcode-float-precision-if-it-can-be-represented-by-binary-format-in-ieee-754

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


23
Поведінка цих двох кодових списків не рівнозначна. 3 / 2.0 - це 1,5, але iв будь-якому іншому списку будуть лише цілі числа. Спробуйте видалити другу /2.0.
candied_orange

27
Якщо вам абсолютно потрібно порівняти два FP для рівності (що не потрібно, як інші вказували у своїх точних відповідях, оскільки ви можете просто порівняти порівняння циклу з цілими числами), але якщо ви це зробили, то коментаря повинно вистачити. Особисто я давно працюю з IEEE FP, і я все ще збився б з пантелику, коли побачив, скажімо, пряме порівняння SPFP без будь-яких коментарів або чогось іншого. Це просто дуже делікатний код - варто коментувати принаймні кожен раз IMHO.

14
Незалежно від того, який ви обираєте, це один із тих випадків, коли коментар, що пояснює, як і чому, є абсолютно важливим. Пізніше розробник може навіть не розглянути тонкощі без коментаря, щоб довести їх до відома. Крім того, мене сильно відволікає той факт, що buttonне змінюється ніде у вашому циклі. Як здійснюється доступ до списку кнопок? Через індекс в масив чи якийсь інший механізм? Якщо це доступ через індекс до масиву, це ще один аргумент на користь переходу на цілі числа.
jpmc26

9
Напишіть цей код. Поки хтось не подумає, що 0,6 буде кращим розміром кроків і просто не змінить цю константу.
tofro

11
"... ввести в оману молодших розробників" Ви також введете в оману старших розробників. Незважаючи на обмір думки, яку ви вклали в це, вони припускають, що ви не знали, що ви робите, і, швидше за все, змінить це на цілу версію все одно.
GrandOpener

Відповіді:


116

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

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


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

10
Наприклад, припустимо, що вимоги змінюються так, що замість 11 рівних розміщених кнопок від 0 до 5 із "стандартною лінією" на 4-й кнопці у вас 16 кнопок з рівним інтервалом від 0 до 5 із "стандартною лінією" на 6-й кнопка. Отже, хто успадкував цей код від вас, змінює 0,5 на 1,0 / 3,0 і змінює 1,5 на 5,0 / 3,0. Що ж відбувається тоді?
Девід К

8
Так, мені незручно з думкою, що зміна того, що здається довільним числом (настільки ж "нормальним", як може бути число) на інше довільне число (яке здається однаково "нормальним") насправді вносить дефект.
Олександр - Відновіть Моніку

7
@Alexander: правильно, вам знадобиться коментар, який сказав DIFF must be an exactly-representable double that evenly divides STANDARD_LINE. Якщо ви не хочете писати цей коментар (і покладаєтесь на всіх майбутніх розробників, щоб достатньо знати про плаваючу крапку IEEE754 binary64, щоб зрозуміти це), тоді не пишіть код таким чином. тобто не пишіть код таким чином. Тим більше, що це, мабуть, не є навіть більш ефективним: додавання FP має більшу затримку, ніж ціле додавання, і це залежність, що переноситься циклом. Крім того, компілятори (навіть компілятори JIT?), Ймовірно, краще роблять цикли з цілими лічильниками.
Пітер Кордес

39

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

Це дещо суб'єктивно, але, на мою скромну думку, якщо ви можете переписати цикл, щоб використовувати цілий індекс для циклу фіксовану кількість разів, ви повинні зробити це. Тож розглянемо таку альтернативу:

var DIFF=0.5;                           // pixel increment
var MAX=Math.floor(5.0/DIFF);           // 5.0 is max pixel width
var STANDARD_LINE=Math.floor(1.5/DIFF); // 1.5 is pixel width

for(var i=0;i<=MAX;i++){
    button.text=(i*DIFF)+'';
    if(i==STANDARD_LINE){
      button.color='red';
    }
}

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


3
Ви також можете розглянути питання про округлення замість підлоги в завданнях, залежно від того, що ви хочете. Якщо поділ повинен дати цілий результат, підлогу може викликати сюрпризи, якщо ви натрапите на цифри, де поділ трохи вимкнене.
ilkkachu

1
@ilkkachu Правда. Мої думки полягали в тому, що якщо ви встановлюєте 5,0 як максимальну кількість пікселів, то, заокруглюючи їх, ви бажаєте бути на нижній частині 5,0, а не трохи більше. 5,0 фактично було б максимумом. Хоча округлення може бути кращим відповідно до того, що вам потрібно зробити. У будь-якому випадку це мало має значення, якщо поділ все одно створить ціле число.
Ніл

4
Я категорично не згоден. Найкращий спосіб зупинити цикл - це умова, яка найбільш природно виражає ділову логіку. Якщо бізнес-логіка полягає в тому, що вам потрібно 11 кнопок, цикл повинен зупинятися на ітерації 11. Якщо бізнес-логіка полягає в тому, що кнопки знаходяться на відстані 0,5, поки рядок не заповнений, цикл повинен зупинятися, коли рядок заповнений. Є й інші міркування, які можуть підштовхнути вибір до того чи іншого механізму, але відсутні ці міркування, виберіть механізм, який найбільше відповідає вимогам бізнесу.
Відновіть Моніку

Ваше пояснення було б абсолютно правильним для Java / C ++ / рубін / Python / ... Але Javascript не цілі числа, так iі STANDARD_LINEтільки виглядають як цілі числа. Примусу взагалі немає, і DIFF, MAXі STANDARD_LINEвсі вони просто Numbers. Numbers, використовувані як цілі числа, повинні бути безпечними внизу 2**53, хоча вони все ще мають цифри з плаваючою точкою.
Ерік Думініл

@EricDuminil Так, але це половина. Інша половина - читабельність. Я згадую це як основну причину цього робити, а не для оптимізації.
Ніл

20

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

Ваш код "знає", що наявні ширини рядків є точно кратними 0,5 від 0 до 5,0. Чи слід? Схоже, це рішення інтерфейсу користувача, яке може легко змінитись (наприклад, можливо, ви хочете, щоб розриви між наявними ширинами зростали, як ширини. 0,25, 0,5, 0,75, 1,0, 1,5, 2,0, 2,5, 3,0, 4,0, 5,0 чи щось таке).

Ваш код "знає", що всі наявні ширини рядків мають "приємні" зображення як числа з плаваючою комою, так і десяткові. Це також здається чимось, що може змінитися. (Можливо, у якийсь момент ви захочете 0,1, 0,2, 0,3, ...)

Ваш код "знає", що текст, який потрібно надіти на кнопки, - це просто те, що Javascript перетворює ці значення з плаваючою комою. Це також здається чимось, що може змінитися. (Наприклад, можливо, у якийсь день ви захочете ширини на зразок 1/3, яку ви, мабуть, не хотіли б відображати як 0,33333333333333 чи будь-яку іншу. Або, можливо, ви хочете побачити "1,0" замість "1" для узгодженості з "1,5" .)

Усі вони відчувають мене як прояви єдиної слабкості, яка є свого роду змішуванням шарів. Ці числа з плаваючою комою є частиною внутрішньої логіки програмного забезпечення. Текст, показаний на кнопках, є частиною інтерфейсу користувача. Вони повинні бути більш відокремленими, ніж тут у коді. Поняття на кшталт "який із них є типовим, який слід виділити?" є питаннями інтерфейсу користувача, і вони, ймовірно, не повинні прив'язуватися до цих значень з плаваючою комою. І ваша петля тут справді (або принаймні має бути) петлею над кнопками , а не за шириною рядків . Написане таким чином, спокуса використовувати змінну циклу, що приймає не цілі значення, ви просто зникнете: ви просто використовуєте послідовні цілі числа або для ... в / для ... циклу.

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


8

Один кодовий запах - це використання поплавків у циклі.

Цикли можуть бути зроблені багатьма способами, але в 99,9% випадків ви повинні дотримуватися приросту в 1 або, безумовно, буде плутанина не тільки молодших розробників.


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

3

Так, ви хочете цього уникнути.

Числа з плаваючою комою є однією з найбільших пасток для підозрілого програміста (це, на мій досвід, майже всіх). Залежно від тестів на рівність плаваючої крапки, до представлення грошей як плаваючої точки, все це велика трясовина. Додавання одного поплавця на іншого є одним з найбільших порушників. Існують цілі томи наукової літератури про подібні речі.

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

У вашому випадку, буде , по Murphys закону, настане момент , коли керівництво хоче , щоб ви не маєте 0,0, 0,5, 1,0 ... 0,0 , але, 0,4, 0,8 ... або що - то; ви будете негайно навантажені, і ваш молодший програміст (або ви самі) будете налагоджувати довго і наполегливо, поки не знайдете проблему.

У вашому конкретному коді я б дійсно мала цілу змінну циклу. Він представляє ith кнопку, а не номер, що працює.

І я, мабуть, задля додаткової ясності не писав, i/2але i*0.5це дає чітко зрозуміти, що відбувається.

var BUTTONS=11;
var STANDARD_LINE=3;

for(var i=0; i<BUTTONS; i++) {
    button.text = (i*0.5)+'';
    if (i==STANDARD_LINE) {
      button.color='red';
    }
}

Примітка: як зазначено в коментарях, JavaScript насправді не має окремого типу для цілих чисел. Але цілі числа до 15 цифр гарантовано є точними / безпечними (див. Https://www.ecma-international.org/ecma-262/6.0/#sec-number.max_safe_integer ), отже, для таких аргументів ("це більш заплутаною / помилкою, схильною до роботи з цілими чи не цілими числами)) це доцільно близько до того, щоб мати окремий тип "по духу"; при щоденному використанні (циклі, координати екрану, індекси масиву тощо) не буде сюрпризів із цілими числами, представленими Numberяк JavaScript.


Я б змінив назву BUTTONS на щось інше - це все-таки 11 кнопок, а не 10. Можливо FIRST_BUTTON = 0, LAST_BUTTON = 10, STANDARD_LINE_BUTTON = 3. Крім того, так, саме так ви повинні це зробити.
gnasher729

Це правда, @EricDuminil, і я трохи додав про це у відповідь. Дякую!
AnoE

1

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

function precisionRound(number, precision) {
  let factor = Math.pow(10, precision);
  return Math.round(number * factor) / factor;
}

var maxButtonValue = 5.0;
var buttonSpacing = 0.5;

let countEstimate = precisionRound(maxButtonValue / buttonSpacing, 5);
var buttonCount = Math.floor(countEstimate) + 1;

var highlightPosition = 3;
var highlightColor = 'red';

for (let i=0; i < buttonCount; i++) {
    let buttonValue = i / buttonSpacing;
    button.text = buttonValue.toString();
    if (i == highlightPosition) {
        button.color = highlightColor;
    }
}

Це може бути більше коду, але він також більш читабельний і надійніший.


0

Ви можете уникнути всього, обчисливши показане вами значення, а не використовуючи лічильник циклу як значення:

var MAX=5.0;
var DIFF=0.5
var STANDARD_LINE=1.5;

for(var i=0; (i*DIFF) < MAX ; i=i+1){
    var val = i * DIFF

    button.text=val+'';

    if(val==STANDARD_LINE){
      button.color='red';
    }
}

-1

Арифметика з плаваючою точкою повільна, а арифметика з цілим числом швидка, тому коли я використовую плаваючу крапку, я б не використовував її без необхідності там, де можна використовувати цілі числа. Корисно завжди думати про цифри з плаваючою комою, навіть константи, як приблизні, з невеликою помилкою. Під час налагодження дуже корисно замінювати нативні числа з плаваючою точкою на об'єкти з плаваючою точкою плюс / мінус, де ви розглядаєте кожне число як діапазон замість точки. Таким чином ви виявляєте прогресивні зростаючі неточності після кожної арифметичної операції. Отже, "1,5" слід вважати "деяким числом між 1,45 і 1,55", а "1,50" слід вважати "деяким числом між 1,455 і 1,55".


5
Різниця в ефективності між цілими числами і плавцями важлива при написанні коду С для невеликого мікропроцесора, але сучасні процесори, отримані x86, мають плаваючу крапку так швидко, що будь-яке покарання легко затьмарюється накладними витратами динамічної мови. Зокрема, чи фактично Javascript не представляє кожне число як плаваючу крапку, використовуючи корисну навантаження NaN при необхідності?
Ліворуч

1
"Арифметика з плаваючою точкою повільна, а ціла арифметика швидка" - це історичний істинний текст, який не слід зберігати, коли Євангеліє рухається вперед. Щоб додати те, що сказав @leftaroundabout, це не просто правда, що штраф буде майже неактуальним, ви можете виявити, що операції з плаваючою комою будуть швидшими, ніж їх еквівалентні цілі операції, завдяки магії автовекторизації компіляторів та наборів інструкцій, які можуть стискати велика кількість поплавців за один цикл. Для цього питання це не актуально, але основне припущення "ціле число швидше, ніж плаваюче" припущення не було істинним довгий час.
Jeroen Mostert

1
@JeroenMostert SSE / AVX мають векторизовані операції як для цілих чисел, і для плавців, і ви, можливо, зможете використовувати менші цілі числа (оскільки жодні біти не витрачаються на експонент), тому в принципі часто можна все-таки видавити більшу ефективність із високооптимізованого цілого коду ніж з поплавками. Але знову ж таки, це не стосується більшості застосунків і, безумовно, не для цього питання.
близько

1
@leftaroundabout: Звичайно. Моя думка полягала не в тому, який з них, безумовно, швидший у будь-якій ситуації, просто те, що "я знаю, що FP повільний, а ціле число швидко, тому я буду використовувати цілі числа, якщо це можливо", це не є хорошою мотивацією, перш ніж ви вирішите питання про те, чи потрібна річ, яку ви робите, оптимізація.
Jeroen Mostert
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.