Если вы выбрали 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)


  1. quaer
    23.08.2021 17:52

    Однажды сделал тест собрав один и тот же код VC6 и VC10 и запустив на Celeron.

    Собранный VC6 код быстрее крутился с float, собранный с VC10 - с double. Причем цифры были очень похожи, но с точностью до наоборот. Разгадки тогда так и не нашёл. В ассемблер было лень лезть смотреть.


  1. staticmain
    23.08.2021 17:53
    +1

    Для справедливости, вышеприведенная программа должна выполняться за две инструкции (xor + ret). Даже на O0.

    К сожалению не все компиляторы это делают.


    1. molnij
      23.08.2021 17:58

      почему именно xor+ret?


      1. staticmain
        23.08.2021 17:59
        +2

        main:
        xor eax, eax;
        ret;


        1. byman
          23.08.2021 18:24

          На моем DSP (для случая float конечно)

          main:

          000004d4: 71080000 JUMP main;;

          Это если с -О2. А если без оптимизации, то тоже бесконечный цикл, но уже со всеми операторами :)


          1. staticmain
            23.08.2021 18:37
            -1

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


            1. byman
              23.08.2021 18:55
              +1

              из-за большой разницы в экспонентах при выполнении операции -1 превращается в ноль. Компилятор на базе Clang.


              1. staticmain
                23.08.2021 19:05

                gcc.godbolt.org/z/aTY4K6vqc

                Нормальный кланг уже на О1 понимает что там белиберда.


                1. byman
                  23.08.2021 20:48

                  но вот если его немножко надуть

                  float x; // = 1e8;

                  x = get_N(); // возвращает из ассемблера 1e8

                  то все равно подвиснет. Хотя умный компилятор увидел бы, что здесь всего-лишь задержка и если от него просят максимум скорости, то её (задержку) можно было бы и убрать :)

                  Если поменять float на int, то результат такой

                  CALL _get_N
                  J8 = 0; return;;

                  т.е. к float больше уважения :)


                1. kovserg
                  23.08.2021 23:47

                  Ага clang он такой. Вы поставьте double x = 1e16; вот тогда оптимизации жгут.


                  1. byman
                    24.08.2021 12:31

                    Поставил для float :) При -О2 получил

                    Fatal[L0]: INTERNAL ERROR. PLEASE CONTACT TECHNICAL SUPPORT.

                    Очень похоже на подсказку, что так делать не стоит :)


            1. she_codes Автор
              24.08.2021 13:27

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

              Дело в том, о при удалении от нуля увеличиваются интервалы между соседними числами с плавающей точкой. И в зависимости от модели округления (round to zero, round to nearest, ...), может так случиться, что при вычитании единицы, получившееся число станет непредставимо во float и округлится обратно к исходному значению.


              1. byman
                24.08.2021 17:21

                это понятно. Но тут есть момент что кроме правил работы с ПТ, в жизненном процессе еще участвует и компилятор. И порой он очень старается помочь считать точнее и быстрее.


              1. byman
                24.08.2021 18:15

                очень хорошее замечание про режим округления. Для случая round to zero зацикливания не будет.


    1. zuko3d
      23.08.2021 22:38
      +5

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


      1. rogoz
        24.08.2021 14:29

        Ну на практике это не «ничего не оптимизируй», а скорее всё-таки уровень оптимизации.


      1. staticmain
        24.08.2021 15:41
        -3

        O0 не означает «ничего не оптимизируй». «Ничего не оптимизируй» — это volatile.


        1. 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. "


    1. cyberzx23
      24.08.2021 01:54

      нет, на O0 всегда будет настоящий цикл. проверьте на godbolt.org


  1. byman
    23.08.2021 18:27

    Для double

    main:

    J8 = 0; return;;


  1. titbit
    23.08.2021 22:51

    Плавающая точка очень коварна. Она зависит даже от опций (оптимизаций) компилятора — он может как использовать классический FPU (для x86), так и SSE. А там и разное время выполнения, даже если тип в программе вообще не менять.


    1. pvsur
      24.08.2021 09:45

      Двойная точность — вежливость королей программистов. Еще со студенчества…


      1. she_codes Автор
        25.08.2021 17:29
        +1

        Как ML инженер не могу согласиться. Достаточная точность – лучшее решение. Иногда и half precision хватает.

        Но всегда нужно понимать как работает плавающая точка.


        1. pvsur
          25.08.2021 18:22

          Согласен с вами. Кому то и интов хватает...


    1. she_codes Автор
      24.08.2021 13:34

      Так точно. Напишу ещё какие-нибудь про модели округления, денормалы и прочие весёлые вещи.


  1. cyberzx23
    24.08.2021 01:49
    +2

    А это точно проблема С/С++? Другие языки однозначно выполняют этот цикл на любой платформе?


    1. she_codes Автор
      25.08.2021 22:36

      Я вам не скажу за всю Одессу.

      Но, вероятно, есть и другие языки и платформы, для которых сохраняется это поведение.

      Ну и даже С++ выполняет этот цикл по-разному в зависимости от опций компиляции, и не только уже упомянутых -O0, -O1 и т.д.


  1. Zifix
    24.08.2021 03:25
    -4

    Если вы выбрали C++ в качестве языка программирования, то учить его придётся всю жизнь. Смиритесь. Или выбирайте другой язык.

    На самом деле нет, если использовать Qt, то можно без малейших неудобств до сих пор сидеть на С++11. Не вижу ни одной причины зачем постоянно учить новые стандарты, если и так всё работает. Только когнитивную нагрузку на ровном месте увеличивать.


    1. monah_tuk
      24.08.2021 04:02
      +1

      Как минимум всякие мелочи, типа auto в лябдах, более ослабленные требования к constexpr функциям, новые атрибуты, типа maybe_unused и так далее. Никто не заставляет брать всё.


      1. Zifix
        24.08.2021 05:16
        -1

        Беда в том, что полностью С++ мало кто знал и во времена С++03, а если каждый программист из каждого нового стандарта возьмет только какое-то уникальное подмножество, то сильно вырастает шанс выстрелить себе в ногу во время командной разработки сотрудниками с разными скиллами и опытом.


        1. monah_tuk
          25.08.2021 04:08
          +1

          Во время командной разработки достаточно быстро знания выравниваются