Если вы выбрали C++ в качестве языка программирования, то учить его придётся всю жизнь. Смиритесь. Или выбирайте другой язык.
Чего только стоят новые стандарты, появляющиеся каждые 3 года. И каждый раз с какими-то полезными нововведениями в синтаксисе! Как им только это удаётся?
Есть разные способы совершенствоваться в С++. Кто-то читает Страуструпа от корки до корки, а потом почитывает стандарты. Кто-то ничего не читает и программирует по наитию - тупиковый путь, на мой взгляд.
Мне же нравится читать крупицы мудрости, бережно сформулированные для понимания простых смертных каким-нибудь гуру программирования.
Одна из моих любимых таких вещей - Guru of the Week (GotW) Херба Саттера.
Написано остроумно. И сколько пользы для программистки! Некоторые статьи уже морально устарели: кому нужен auto_ptr в наши дни? Но большинство ценно и сегодня.
Приведу здесь перевод выпуска №67 "Double or Nothing" от 29 февраля 2000 года. Моё любимое место — про тепловую смерть конечно
"Сложность 4/10
Нет. Этот выпуск не об азартных играх. Он, впрочем, о разных видах "float" так сказать, и даёт вам возможность проверить навыки касающиеся базовых операций над числами с плавающей точкой в C и C++.
Проблема
Вопрос Йуного Гуру (JG)
1) В чём разница между "float" и "double"?
Вопрос Гуру
2) Допустим следующая программа выполняется за 1 секунду, что неудивительно для современного настольного компьютера:
int main() {
double x = 1e8;
while (x > 0) { --x; }
return 0;
}
Как долго по-вашему она будет выполняться, если заменить "double" на "float"? Почему?
Решение
1) В чём разница между "float" и "double"?
Цитата из секции 3.9.1/8 стандарта C++:
Существует три типа чисел с плавающей точкой: float, double и long double. Тип double предоставляет по меньшей мере такую же точность как float, а тип long double — по меньшей мере такую же точность как double. Множество значений типа float является подмножеством множества значений типа double; множество значений типа double является подмножеством множества значений типа long double.
Колесо Времени
2) Как долго по вашему программа из вопроса 2 будет выполняться, если заменить "double" на "float"? Почему?
Она будет выполняться или примерно 1 секунду (в конкретных реализациях float'ы могут быть быстрее, такими же быстрыми или медленнее, чем double'ы) или бесконечно, в зависимости от того может или нет тип float представлять все целые числа от 0 до 1e8 включительно.
Цитата из стандарта выше означает, что могут существовать такие значения, которые могут быть представлены типом double, но не могут быть представлены типом float. В частности, на многих популярных платформах и во многих компиляторах double может точно представить все целые числа в диапазоне [0, 1e8], а float — не может.
Что если float не может точно представить все целые числа в диапазоне от 0 до 1e8? Тогда изменённая программа начнёт обратный отсчёт, но в конце концов достигнет значения N, которое не может быть представлено и для которого N-1==N (из-за недостаточной точности чисел с плавающей точкой) ... и тогда цикл застрянет на этом значении пока машина, на которой запущена программа не разрядится (из-за проблем в локальной энергосети или ограничений батареи), её операционная система не сломается (некоторые платформы более подвержены этому, чем другие), Солнце не превратится в переменную звезду и спалит внутренние планеты или вселенная не погибнет тепловой смертью — любое, что произойдёт раньше*.
Слово о Сужающих Преобразованиях
Некоторые люди могут интересоваться: "Хорошо, кроме всеобщей тепловой смерти нет ли здесь другой проблемы? Константа 1e8 имеет тип double. Тогда если мы просто заменим "double" на "float", программа не скомпилируется по причине сужающего преобразования, верно?"
Хорошо, давайте процитируем стандарт ещё раз, на этот раз секцию 4.8/1:
rvalue [выражение справа от знака "=" - прим. переводчицы] типа с плавающей точкой может быть преобразовано к rvalue другого типа с плавающей точкой. Если исходное значение может быть точно представлено целевым типом данных, результатом преобразования будет являться то точное представление. Если исходное значение лежит между двух последовательных значений целевого типа данных, результат преобразования определяется реализацией в качестве одного из этих двух значений. Иначе поведение не определено.
Это означает, что константа типа double может быть неявно (то есть молчаливо) преобразована в константу типа float, даже если это действие приводит к потере точности (то есть данных). Этому было позволено остаться как есть по причине совместимости с C и удобства использования. Но стоит держать это в уме, когда делаете работу с типами с плавающей точкой.
Качественный компилятор предупредит вас, если вы попытаетесь сделать что-то, поведение чего не определено. Например, присвоить значение типа double, которое меньше минимального или больше максимального значения, представимого типом float, к сущности типа float. По-настоящему хороший компилятор выдаст опциональное предупреждение, если вы попытаетесь сделать что-то, поведение чего может быть определено, но что влечёт потерю информации. Например, присвоить значение типа double, которое лежит между минимальным и максимальным значением типа float, но не может быть в точности представлено типом float, к сущности типа float.
Примечания
*Действительно, раз программа оставляет компьютер работать впустую, она также впустую увеличивает энтропию вселенной, таким образом приближая упомянутую тепловую смерть. Вкратце, такая программа довольно недружественна к окружающей среде и должна считаться угрозой для нашего биологического вида. Не пишите так код**.
** Конечно, выполнение любой дополнительной работы, людьми ли или машинами, также увеличивает энтропию вселенной, приближая тепловую смерть. Это хороший аргумент, который нужно иметь ввиду, когда работодатель требует переработок."
(c) Херб Саттер, GotW
Комментарии (31)
staticmain
23.08.2021 17:53+1Для справедливости, вышеприведенная программа должна выполняться за две инструкции (xor + ret). Даже на O0.
К сожалению не все компиляторы это делают.molnij
23.08.2021 17:58почему именно xor+ret?
staticmain
23.08.2021 17:59+2main:
xor eax, eax;
ret;byman
23.08.2021 18:24На моем DSP (для случая float конечно)
main:
000004d4: 71080000 JUMP main;;
Это если с -О2. А если без оптимизации, то тоже бесконечный цикл, но уже со всеми операторами :)
staticmain
23.08.2021 18:37-1Вообще удивительно откуда бесконечный цикл. Ведь целая часть флоата так или иначе должна досчитаться. Там только канитель с экспонентой может быть, когда точности для единиц целых не хватает.
byman
23.08.2021 18:55+1из-за большой разницы в экспонентах при выполнении операции -1 превращается в ноль. Компилятор на базе Clang.
staticmain
23.08.2021 19:05gcc.godbolt.org/z/aTY4K6vqc
Нормальный кланг уже на О1 понимает что там белиберда.byman
23.08.2021 20:48но вот если его немножко надуть
float x; // = 1e8;
x = get_N(); // возвращает из ассемблера 1e8
то все равно подвиснет. Хотя умный компилятор увидел бы, что здесь всего-лишь задержка и если от него просят максимум скорости, то её (задержку) можно было бы и убрать :)
Если поменять float на int, то результат такой
CALL _get_N
J8 = 0; return;;т.е. к float больше уважения :)
she_codes Автор
24.08.2021 13:27Статья не ради одного этого примера же написана. Она для общего понимания принципов работы чисел с плавающей точкой.
Дело в том, о при удалении от нуля увеличиваются интервалы между соседними числами с плавающей точкой. И в зависимости от модели округления (round to zero, round to nearest, ...), может так случиться, что при вычитании единицы, получившееся число станет непредставимо во float и округлится обратно к исходному значению.
byman
24.08.2021 17:21это понятно. Но тут есть момент что кроме правил работы с ПТ, в жизненном процессе еще участвует и компилятор. И порой он очень старается помочь считать точнее и быстрее.
byman
24.08.2021 18:15очень хорошее замечание про режим округления. Для случая round to zero зацикливания не будет.
zuko3d
23.08.2021 22:38+5Извините, а вы точно знаете смысл флага O0? Кажется странным сообщать компилятору "ничего не оптимизируй" и потом говорить "К сожалению не все компиляторы умеют оптимизировать такой код".
rogoz
24.08.2021 14:29Ну на практике это не «ничего не оптимизируй», а скорее всё-таки уровень оптимизации.
staticmain
24.08.2021 15:41-3O0 не означает «ничего не оптимизируй». «Ничего не оптимизируй» — это volatile.
zuko3d
25.08.2021 00:09+1Нет, volatile это "within a single thread of execution, volatile accesses cannot be optimized out or reordered with another visible side effect that is sequenced-before or sequenced-after the volatile access. "
titbit
23.08.2021 22:51Плавающая точка очень коварна. Она зависит даже от опций (оптимизаций) компилятора — он может как использовать классический FPU (для x86), так и SSE. А там и разное время выполнения, даже если тип в программе вообще не менять.
she_codes Автор
24.08.2021 13:34Так точно. Напишу ещё какие-нибудь про модели округления, денормалы и прочие весёлые вещи.
cyberzx23
24.08.2021 01:49+2А это точно проблема С/С++? Другие языки однозначно выполняют этот цикл на любой платформе?
she_codes Автор
25.08.2021 22:36Я вам не скажу за всю Одессу.
Но, вероятно, есть и другие языки и платформы, для которых сохраняется это поведение.
Ну и даже С++ выполняет этот цикл по-разному в зависимости от опций компиляции, и не только уже упомянутых -O0, -O1 и т.д.
Zifix
24.08.2021 03:25-4Если вы выбрали C++ в качестве языка программирования, то учить его придётся всю жизнь. Смиритесь. Или выбирайте другой язык.
На самом деле нет, если использовать Qt, то можно без малейших неудобств до сих пор сидеть на С++11. Не вижу ни одной причины зачем постоянно учить новые стандарты, если и так всё работает. Только когнитивную нагрузку на ровном месте увеличивать.
monah_tuk
24.08.2021 04:02+1Как минимум всякие мелочи, типа auto в лябдах, более ослабленные требования к constexpr функциям, новые атрибуты, типа maybe_unused и так далее. Никто не заставляет брать всё.
Zifix
24.08.2021 05:16-1Беда в том, что полностью С++ мало кто знал и во времена С++03, а если каждый программист из каждого нового стандарта возьмет только какое-то уникальное подмножество, то сильно вырастает шанс выстрелить себе в ногу во время командной разработки сотрудниками с разными скиллами и опытом.
quaer
Однажды сделал тест собрав один и тот же код VC6 и VC10 и запустив на Celeron.
Собранный VC6 код быстрее крутился с float, собранный с VC10 - с double. Причем цифры были очень похожи, но с точностью до наоборот. Разгадки тогда так и не нашёл. В ассемблер было лень лезть смотреть.