Это история о баге, который бы заставил вас рвать на себе волосы. Из-за такого бага вы можете подумать: «Но это невозможно, должно быть, компилятор сломался, других вариантов нет!»

А баг компилятора — это серьёзно: за двенадцать лет программирования на C++ я обнаружил (и написал отчёт) всего... об одном. И могу сказать, что перед отправкой отчёта о баге GCC я максимально тщательно протестировал и проверил его, чтобы не выглядеть идиотом.

Впрочем, ладно, вот моя история.

Открытие

Апрель 2023 года. Выпуск игры запланирован на 15 мая, поэтому это достаточно напряжённый период, я упорно старался уложиться в дедлайн. В основном я работал над «художественной» частью (графика, музыка), но иногда добавлял/исправлял небольшие фрагменты кода.

В то время я по привычке компилировал версию для Windows 32-битным вариантом MinGW: эта привычка появилась у меня из-за инерции старых версий Windows, не поддерживавших 64-битную архитектуру; на самом деле, теперь эта привычка больше особо не нужна. Как только я собрался тестировать игру в Windows, внезапно возник баг. Игра просто начала вылетать. Хм.

Я сравнил исполнение программы с версией для GNU/Linux и быстро нашёл, где она крашилась: различия в исполнении возникали, когда я вычислял граф пола, описывающий то пространство в комнате, где персонаж может ходить.

Говоря простым языком, на этом этапе я создавал из пиксельных значений контур пола, представленный в виде графа. Для этого я начинал с описания краёв пола при помощи достаточно сложной полилинии, описывающей границы пикселей, а затем выполнял алгоритм упрощения полилинии.

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

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

Я осознал, что по какой-то странной причине спустя несколько итераций алгоритм в Windows начинает отличаться от алгоритма в GNU/Linux (он удаляет другую вершину, в конечном итоге удаляя вершины, которые не должны быть удалены, приводя таким образом к вылету). И это какое-то безумие, ведь алгоритм детерминирован: если передавать ему одно и то же входное изображение, то он должен возвращать одну и ту же полилинию, удалив точно такие же вершины в точно том же порядке.

В то время я немного ругался на Windows/MinGW, потому что многократно перечитав и перепроверив свой код, я подозревал наличие бага компилятора. Но осознав потом основную разницу (мой MinGW был 32-битным, а GCC в Gnunux был 64-битным) я попытался скомпилировать код в GNU/Linux, но в 32-битном режиме... и вуаля. Баг появился. Я попробовал Clang. Тот же баг.

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

Ядро бага

Моя очередь с приоритетами на самом деле представлена std::set (множеством уникальных элементов, хранящихся в отсортированном виде), что позволяет мне удалять элементы, не находящиеся в начале очереди. Тонкость заключается в том, что мы добавляем set адаптированную функцию сравнения.

Сравнение — это первое действие для отклонения, о котором я говорил: когда мы вставляем вершину в set, мы хотим, чтобы вершины с наименьшим отклонением находились в начале set. Давайте сделаем это :

bool operator() (const GVertex& a, const GVertex& b)
{
  double da = deviation(a);
  double db = deviation(b);
  return da < db;
});

Но внимание, опасность! Две вершины могут иметь одинаковое отклонение, что в std::set, гарантирующем уникальность элементов, означает, что в очередь с приоритетами будет вставлена только первая вершина. Очевидно, мы должны иметь возможность добавлять в очередь с приоритетами две вершины с одним отклонением. Разумеется, в таком случае нам неважно, какая из вершин удаляется первой, поэтому мы просто возьмём первую вершину в графе (вершины сравниваются согласно их индексам), чтобы процесс был детерминированным.

Таким образом, функция приобретает следующий вид:

bool operator() (const GVertex& a, const GVertex& b)
{
  double da = deviation(a);
  double db = deviation(b);
  if (da == db)
    return a < b;
  return da < db;
});

Я быстро понял, что проблема находится в этой функции: в какой-то момент if() возвращает true в 64-битном режиме и false в 32-битном.

Отступление о проверках равенства для double

Здесь я должен сделать отступление. Мне пришла в голову плохая идея выложить этот фрагмент кода онлайн. Вы не поверите, сколько я получил комментариев, смысл которых сводился к следующему:

Ну конечно, ты выполняешь проверку равенства для double! Никогда так не делай! В этом и заключается проблема!

Пф-ф-ф, никогда В ЖИЗНИ не проверяй равенство double: если хочешь сравнивать, сравнивай разность с небольшим допуском!

Два double никогда не равны! Разумеется, если ты выполняешь проверку равенства double, то напрашиваешься на неприятности.

Этот список можно продолжать долго.

Что ж.

Меня немного вывела из себя необходимость отвечать на пятнадцать сообщений от людей, которые просто выучили, что «равенство double = опасность» и набросились на меня, не попробовав разобраться, что делает код. И не подумав, что, возможно, я понимаю в этом немного лучше, и о том, что НЕТ, причина была не в этом.

Для протокола: прежде чем стать разработчиком видеоигр, я написал диссертацию по вычислительной геометрии, а потом шесть лет работал в компании, разрабатывавшей ПО для вычислительной геометрии. В частности, я использовал такие библиотеки, как MPFR, которые позволяют выполнять вычисления с многократной точностью, чтобы избежать связанных с double проблем при работе с вещественными числами. Пожалуйста, поверьте мне, когда я говорю, что знаю, на что иду, когда сравниваю double.

Ну да ладно. Если вы тоже увидели проверку равенства double и подумали «лол, нуб, разумеется, проверки равенства с double не работают», то я должен высказаться прямо:

  1. Проверки равенства с double допустимы, это не табу и не Волан-де-морт, у вас есть право писать их, мир не рухнет, если вы сделаете это, вам разрешается это делать. Конечно, если вы знаете, что делаете.

  2. Проверка равенства double вызывает проблемы, только если вы думаете, что проверяете равенство между вещественными числами (в математическом смысле). Да, при работе с double 1.0/2.0 необязательно равно 0.5, потому что конечная точность означает, что другое описание одного вещественного числа может дать немного отличающиеся значения. С другой стороны (и я настаиваю на этом): 1.0/2.0 гарантированно равно 1.0/2.0; 0.5 гарантированно равно 0.5; да, ДАЖЕ в случае double. Я имею в виду, что если у вас есть два double с одинаковым значением (в компьютерном смысле, то есть с одинаковыми битами в одинаковых байтах), вычисленные абсолютно одинаковым образом, то проверка равенства между этими двумя double вернёт TRUE. И спасибо Вселенной за это!

  3. В моём случае всё было даже проще: нас не интересует случай равенства двух double, это просто особый случай, который мы подготовили, чтобы обезопасить себя, потому что если два double различаются, то всё ещё проще. Представим, что вы правы и что «два double никогда не равны» (что, разумеется, совершенно неверно): в таком случае проблем нет, потому что if никогда не будет true, и алгоритм всегда будет использовать в качестве приоритета отклонение. На практике, поскольку полилинии составляются из обычных пикселей, разумеется, существует множество равных отклонений...

  4. Использовать допуск? Простите, вы или не прочитали код, или совсем его не поняли. Что произойдёт, если вместо сравнения da и db я выполню do if(std::fabs(da-db)<epsilon)? Если две вершины имеют близкие отклонения, то вместо сортировки их по отклонению мы отсортируем их по индексам. ОТЛИЧНО. В чём смысл, кроме как в снижении оптимальности алгоритма?

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

Отступление завершено.

Сдаюсь

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

  1. Отключив оптимизации компилятора

  2. Добавив к сравнениям std::cerr (баг Шрёдингера: он исчезает, когда ты за ним наблюдаешь)

  3. Предварительно сохраняя значения отклонений, а не вычисляя их на ходу (нет, они не меняются, я проверял)

  4. Целой кучей других странных действий (например, объявлением ещё одной переменной посередине сравнения).

Если вкратце: 64 бит? Без проблем. 32 бит без оптимизаций? Без проблем. 32 бит с оптимизациями? Бум, ошибка.

На этом этапе я видел в отладчике буквально следующее:

  1. Программа определяет равенство между double как false (а это ошибка)

  2. Не входит в if()

  3. Затем определяет неравенство между double как false (что верно, но противоречит пункту 1.)

Всё это пахнет багом компилятора. Только, как я сказал, ошибка возникала и в GCC, и в Clang. Два разных компилятора с одним багом? Маловероятно.

Я потерял день, но не добился никакого прогресса. Более того, баг возникал только в development-версии игры (и в 32-битной версии, которую я вообще не собирался распространять): в продаваемой версии этот граф пола вычисляется предварительно. То есть в целом этот баг ни на что не повлияет, но меня ужасно раздражало, что я его не понимаю.

В конечном итоге я сдался: я потратил на это слишком много времени, а мне нужно было закончить игру. Было решено, что я вернусь к багу после релиза.

Возвращаемся к багу

Я уложился в дедлайн и выпустил игру 15 мая.

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

Большая проблема этого бага заключалась в том, что он находился посередине очень большой программы (игрового движка) со множеством сложных файлов и зависимостей. Поэтому в этом случае нужно было максимально изолировать часть с багом, чтобы убедиться, что он не возникает из-за чего-то более запутанного (например, утечка памяти в другой части игры могла бы приводить к записи в очередь с приоритетами мусорных данных).

Я работал над ним так:

  1. Вытащил вычисление графа из игрового движка: мы загружаем изображение и выполняем только вычисление графа, устранив бесполезные зависимости (LZ4, YAML)

  2. Максимально уменьшил изображение до маленького графа

  3. В конечном итоге я просто прописал граф в коде (мне удалось воспроизвести баг всего с 16 вершинами вместо 3900), поэтому загрузка изображения больше не требовалась и я удалил зависимость от SDL

  4. Я избавился от своей геометрической мини-библиотеки, оставив две или три полезные функции из неё.

  5. Максимально уменьшил очередь с приоритетами, ошибка начала возникать после вставки второй вершины.

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

#include <array>
#include <cmath>
#include <iostream>
#include <set>

using double2 = std::array<double, 2>;

struct Comparator
{
  static double deviation (double2 p)
  {
    const double2 p0 { 1, 0 };
    const double2 vp0p1 { -1 / std::sqrt(2), 1 / std::sqrt(2) };
    const double2 vp0p { p[0] - 1, p[1]};
    const double dotprod = vp0p1[0] * vp0p[0] + vp0p1[1] * vp0p[1];
    const double2 proj { 1 + dotprod * vp0p1[0], p0[1] + dotprod * vp0p1[1] };
    return std::sqrt ((proj[0] - p[0]) * (proj[0] - p[0]) +
                      (proj[1] - p[1]) * (proj[1] - p[1]));
  }

  bool operator() (const double2& a, const double2& b) const
  {
    const double da = deviation(a);
    const double db = deviation(b);
    if (da == db)
      return a < b;
    return da < db;
  }
};

void insert (std::set<double2, Comparator>& set, double2 point)
{
  const double deviation = Comparator::deviation(point);

  std::cerr << "Inserting " << std::defaultfloat << point[0] << " " << point[1]
            << " with deviation = " << deviation << " / hex="
            << std::hexfloat << deviation << std::endl;
  if (set.insert (point).second)
    std::cerr << " -> Success" << std::endl;
  else
    std::cerr << " -> Failure" << std::endl;
}

int main (int, char**)
{
  std::cerr.precision(18);
  std::set<double2, Comparator> set;
  insert(set, { 0, 0 });
  insert(set, { 1, 1 });
  return EXIT_SUCCESS;
}

По сути, эта программа вставляет две точки в std::set с использованием функции сравнения: эти точки отличаются ((0,0) и (1,1)), но имеют одинаковое значение отклонения (равное корню из двух, делённому на два).

После компиляции этой программы в 64-битном режиме результат оказывается таким:

Inserting 0 0 with deviation = 0.707106781186547573 / hex=0x1.6a09e667f3bcdp-1
-> Success
Inserting 1 1 with deviation = 0.707106781186547573 / hex=0x1.6a09e667f3bcdp-1
-> Success

И это нужный результат.

Компилируем программу в 32 битах со включенным хотя бы -O1 (-O2 при использовании Clang), и получаем:

Inserting 0 0 with deviation = 0.707106781186547573 / hex=0x1.6a09e667f3bcdp-1
-> Success
Inserting 1 1 with deviation = 0.707106781186547573 / hex=0x1.6a09e667f3bcdp-1
-> Failure

Вторая точка не вставляется, тут есть баг.

Это совершенно точно.

На этом этапе я уже был более уверен в том, что мой код верен, а баг в компиляторе. Я и чётко определил, в чём же ошибка.

Затем я загуглил «c++ O1 optimization double comparison bug branch». И получил ответ.

Печально известный баг 323 GCC

Баг GCC номер 323 под названием «оптимизированный код даёт странные результаты работы с плавающей запятой» датируется ещё 14 июня 2000 года. На момент написания моей статьи под ним уже 229 комментариев, 101 из которых помечен как дубликат (другой баг, который запостил кто-то другой, но на самом деле являющийся тем же). Неплохое начало.

Если этот баг с нами уже так долго, то на самом деле это не баг компилятора: это баг процессора. Да, всё верно.

Если точнее, то это поведение FPU (Floating-Point Unit), вызывающее неожиданное поведение при оптимизации кода.

Смысл в том, что вычисления над числами с плавающей запятой могут выполняться с разной точностью: в ОЗУ числа float имеют 32-битную точность, уже известные нам double — точность 64 бит (и в самом деле, двойная точность)... а вычисления в FPU используют регистры процессора с точностью 80 бит. Эта дополнительная точность во время вычислений минимизирует погрешность округления.

Что же происходит в моём коде? Когда код не оптимизируются, FPU вычисляет значение отклонения для каждой из двух вершин с точностью 80 бит (в регистрах процессора), а затем сохраняет их в 64-битную переменные double в ОЗУ (а потому округляет их, ведь точность снижается) для каждой из двух вершин. Пока всё хорошо.

Но когда код оптимизируется, компилятор понимает, что при вычислении второго отклонения нет смысла помещать его в ОЗУ (что затратно с точки зрения времени исполнения), потому что оно сразу же будет использовано: его вполне можно оставить в регистре процессора и использовать напрямую. Только оно не будет преобразовано в 64 бита, а значит, имеет бОльшую точность, чем отклонение первой вершины. То есть и другое значение. Вот и всё: значения, которые должны быть равными, становятся различными, потому что одно округлили до 64 бит, а другое нет.

Это объясняет, почему баг пропадает, если выполняю в коде, казалось бы, не связанные с ним действия (объявляю ещё одну переменную, добавляю std::cerr и так далее): заставив процессор выполнять в это время что-то ещё, мы просто заставляем сохранить второе отклонение в ОЗУ, что решает проблему.

Очевидно, это поведение было исправлено в последующих поколениях FPU, чтобы соответствовать стандарту IEEE-754, требующему, чтобы вычисления с плавающей запятой были детерминированными. Но поскольку проблема находится внутри процессора, этот баг встречается и в GCC, и в Clang (а также, вероятно, и в других компиляторах). И этот знаменитый баг 323 продолжают регулярно комментировать и дублировать, хотя, строго говоря, это не баг GCC.

Я во многом согласен с этим комментарием:

Вызов тривиальной функции с одинаковым параметром может вернуть значение меньшее, равное или большее чем оно само (в зависимости от распределения регистров в контексте вызовов). Это крайне неприятное недетерминированное поведение. Разве компиляторы и высокоуровневые языки изобрели не для того, чтобы решать именно такие проблемы с аппаратными зависимостями?

И, наконец, небольшой комментарий, который рассмешил меня, потому что он как будто обращается напрямую ко мне:

Мне хотелось бы поприветствовать новых участников сообщества бага 323, в которое приходят умирать все ошибки x87 с плавающей точкой в gcc! Сюда принимают все ошибки с плавающей запятой при использовании x87, несмотря на то, что многие из них легко устранить, а многие нет! Мы — одна большая семья, совершившая очевидную ошибку, возжелав точности от самого точного FPU общего назначения на рынке!

Как это исправить?

Разобравшись, почему может возникать такое поведение, мы должны ответить на практический вопрос: как помешать коду выполнять такое поведение?

Что ж, есть сложное решение: можно воспользоваться опцией -ffloat-store времени компиляции. Эта опция в буквальном смысле заставляет компилятор сохранять все float в ОЗУ (таким образом, делая ошибку невозможной). Да, конечно, это решает проблему, но не позволяет использовать множество потенциально интересных оптимизаций (возможность не сохранять float в ОЗУ, когда это не нужно, даёт очень много преимуществ и в 99% случаев безопасно).

Гораздо более уточнённый способ (потому что его можно интегрировать только в потенциально проблематичные места кода, обычно в проверки равенства double) — это использование ключевого слова «volatile». Процитирую Википедию : «это ключевое слово предотвращает оптимизацию компилятором последовательных операций чтения или записи». Всё довольно просто: оптимизации деактивируются локально, а не глобально.

Для этого достаточно лишь изменить функцию сравнения следующим образом:

  bool operator() (const double2& a, const double2& b) const
  {
    const volatile double da = deviation(a);
    const volatile double db = deviation(b);
    if (da == db)
      return a < b;
    return da < db;
  }

После этого баг пропадёт. Можете попробовать сами: теперь код работает даже в 32-битном режиме с оптимизациями.

ПОБЕДА!

Заключение

Я думал, что баги компилятора — это худшее, что может случиться в программировании (потому что их ожидаешь меньше всего): я ошибался, баги процессора хуже.

Несмотря на то, что я потратил на этот баг много часов, меня радует одно: я был достаточно упорен, чтобы наконец-то разобраться в происходящем. А вишенкой на торте стало простое и оптимальное исправление (использование volatile).

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

Комментарии (112)


  1. kipar
    17.08.2023 05:59
    +13

    я правильно понимаю что если бы он сделал как ему сразу написали, т.е. сравнивал double не на равенство а с допуском, то баг бы ушёл?


    1. ultrinfaern
      17.08.2023 05:59
      +1

      Да.

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


      1. Leetc0deMonkey
        17.08.2023 05:59
        -2

        А какая разница?


        1. Tiriet
          17.08.2023 05:59
          +12

          собственно- вся статья как раз о том, какая разница между предвычисленными и налету- в том, что именно сравнивает процессор в своих регистрах- fp64 или fp80 и в какой момент происходит переход от одних чисел к другим.


      1. avost
        17.08.2023 05:59

        Но нет, тут на лету сравниваются значения вычислений.

        Вряд ли кто-то обязан знать все особенности работы оптимизатора в сочетании с определёнными моделями fpu в неактуальном режиме работы процессора. Тем более, как автор пишет, что это поведение устранили в новых процессорах на аппаратном уровне (интересно, как?).
        Так что хоть сравнение по классике и устранило бы проблему, но это было бы именно обходом бага процессора/оптимизатора, а не устранением бага программы. Просто случайное совпадение.
        Там не исключено (не проверял, просто предположение), что если в качестве эпсилон брать ноль, то тоже бы сработало (и, например, вылезло бы снова на более высоких уровнях оптимизатора).


        1. vesper-bot
          17.08.2023 05:59

          это поведение устранили в новых процессорах на аппаратном уровне (интересно, как?)

          Как я понимаю, просто заставили процессор работать на 64-битных вещественных числах формата double, не более того.


        1. cher-nov
          17.08.2023 05:59
          +6

          Тем более, как автор пишет, что это поведение устранили в новых процессорах на аппаратном уровне (интересно, как?).

          Сейчас для вычислений с IEEE-754 числами на платформах x86 компиляторы стараются генерировать код с применением SSE / SSE2, а не команд x87 FPU. Однако поскольку 32-битный код компиляторы по умолчанию (когда не указана целевая платформа) выдают совместимым аж с i386 / i686, то заметным это становится лишь в 64-битном режиме, где присутствие SSE2 уже можно гарантировать ввиду его наличия во всех известных x86-64 процессорах.

          Именно поэтому у автора работала 64-битная сборка, но не работала 32-битная. А если бы он подал компилятору ключ -mfpmath=387, то сломалась бы и она - ничто не мешает использовать инструкции x87 и в 64-битном режиме, и порой это даже происходит само собой. Но для этого операционная система должна сохранять контекст сопроцессора между переключениями задач.

          К слову, куда более простым и правильным решением проблемы было бы использование не volatile, а обычного long double, который поддерживается к тому же ещё начиная с C89.


          1. funny_falcon
            17.08.2023 05:59

            MSVC вроде приравнивает long double к double


            1. cher-nov
              17.08.2023 05:59

              MSVC вроде приравнивает long double к double

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


          1. unicrus
            17.08.2023 05:59
            -1

            Можно было сделать и так:

            return da < db ? true : (da > db? false : a < b);


    1. nextdesu
      17.08.2023 05:59
      +17

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

      1. Использовать допуск? Простите, вы или не прочитали код, или совсем его не поняли. Что произойдёт, если вместо сравнения da и db я выполню do if(std::fabs(da-db)<epsilon)? Если две вершины имеют близкие отклонения, то вместо сортировки их по отклонению мы отсортируем их по индексам. ОТЛИЧНО. В чём смысл, кроме как в снижении оптимальности алгоритма?


      1. Tiriet
        17.08.2023 05:59
        +9

        в описанном случае с epsilon тоже вопрос: какую епсилон брать? она должна быть не в абсолютных значениях, а в относительных, то есть, для аккуратного сравнения надо использовать что-то типа

        if(std::fabs(da-db)<epsilon*(fabs(da)+fabs(db)))

        мне нужно сравнение, не зависящее от порядка сравнения точек, не зависящее от масштабирования (увеличив все размеры в тысячу раз я должен получить тот же результат), но теперь надо проверять отдельно ситуацию, когда da==db==0.0- чтоб не было ложных срабатываний, и я все равно получаю возможность такого же бага при epsilon<1.0e-15. Объем вычислений и количество проверок растут, сложность кода растет, а возможность бага все равно сохраняется: без оптимизации результат будет отличаться чем с оптимизацией- потому что без оптимизации da-db==0 после округления 80битных флоатов до 64х, а с оптимизацией da-db~~ 1.0e-15- отличается в последних битах 80битного числа. И даже если я выберу большой епсилон- это не исключает вероятности того, что мне повезет найти такую пару рациональных точек, что их разница модулей будет очень близка к epsilon и при этом оптимизированный и неоптимизированный алгоритмы будут давать разные результаты. Конкретно в рассматриваемом случае просто автор специально выбрал очень близкие da&db и eps<1.0e-15, (потому что сравнение da==db равносильно fabs(da-db) < 2.2e-16 - с учетом экспонент конечно чуть сложнее, но суть такая).


        1. avost
          17.08.2023 05:59

          Без условия возможности увеличения размеров можно брать эпсилон - минимально представимое по абсолютной величине в fpu число - единичка в младшем бите мантиссы и минус максимальный порядок. Тогда и масштабирование, кажется, будет детерминированым (ну, ок - в половине случаев :) ). По сложности - это плюс одна загрузка регистра и плюс одно сранение/вычитание.
          А, кажется, нет - мантисса же нормированная должна быть? Тогда недетерминированность сильно возрастает...
          Но авторское решение в любом случае изящнее.


          1. slonopotamus
            17.08.2023 05:59
            +2

            минимально представимое по абсолютной величине в fpu число

            Если что, это называется 1 ulp.


      1. Tiriet
        17.08.2023 05:59
        +4

        ну да, написал. а потом всю статью пытался объяснить суть происходящего в его коде, и в конце концов разобрался- его код da==db фактически тоже сравнивает значения с относительной точностью, равной точности округления чисел (2.2e-16), но между первым и вторым сравнением в оптимизированном коде есть ооочень небольшое изменение значений, вызванное логикой работы процессора, и это изменение как раз перекрывает использованную им точность сравнения, поэтому результат da<db не согласован с результатом da==db. Я сам напрыгался по граблям в похожих задачах (пересечение численных сеток) и ооочень хорошо понимаю автора. С практической точки зрения проще, быстрее и спокойнее, если ты сам всегда и полностью контролируешь погрешность сравнения флоатов и держишь ее на два порядка большей, чем у процессора в худшем случае- но это приводит к усложнению вроде бы понятных алгоритмов и куче идиотских проверок, перепроверок и перепроверок перепроверок. А тут- ну да, код простой и формально логичный, но привел к багу 323, где уже и так толпа людей.


  1. webhamster
    17.08.2023 05:59
    -7

    bool operator() (const GVertex& a, const GVertex& b)

    Что же имел в виду тут автор? Надо бы разобраться почему парсер съедает символы, придумать work around и показать читателю правильный код. А то ценность статьи с такими ошибками нуливая, только лишний раз убеждать начинающих программистов что синтаксис C++ непостижим.


    1. KanuTaH
      17.08.2023 05:59
      +3

      А что тут не так?


      1. webhamster
        17.08.2023 05:59

        Извиняюсь, давно на плюсах не писал. Подумал что код для операции сравнения, и он должен выглядеть как operator()<, а для этого на самом деле надо использовать operator<.

        Первый раз вижу, чтобы оператор () возвращал bool при сравнении своих аргументов.


        1. KanuTaH
          17.08.2023 05:59
          +3

          Первый раз вижу, чтобы оператор () возвращал bool при сравнении своих аргументов.

          operator() из std::less, который используется как компаратор по умолчанию в std::set, делает именно это.

          Подумал что код для операции сравнения, и он должен выглядеть как operator()<, а для этого на самом деле надо использовать operator<.

          Смысл существования function object наподобие std::less или этого Comparator у автора в том, что вы можете не захотеть в данном конкретном случае использовать именно сравнение <. А переопределив operator<() для данного типа, и заменив его логику работы, вы вынудите всю программу использовать эту новую логику вместо того, чтобы использовать ее только лишь в этом месте, а это скорее всего не то что вам нужно.


          1. webhamster
            17.08.2023 05:59

            Я лично вижу тут "вывернутую" логику. Для абстракции "точка", по сути, не сделан отдельный класс, вместо этого точка рассматривается как просто два double значения. Соответственно нет и абстракции "полилиния", которая бы состояла из "точек", и у которой был бы метод расчета отклонений точек, вместо этого голый библиотечный set. А у "точки" нет метода расчета расстояния до другой точки. Ну чтобы все было по логике вещей. Зато есть класс компаратор, который сравнивает два каких-то значения, ему все равно каких, он работает с double а не точками. Другими словами, структуры предметной области в этом коде не видно.

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


            1. 0xd34df00d
              17.08.2023 05:59

              Потому что точки можно сравнивать кучей разных способов — лексикографически, по положению в массиве, по расстоянию до прямой, и так далее. Выбор конкретного способа не является свойством точки, а попытки привязать к ней один из вариантов поведения — очередное ООП головного мозга.


  1. vesper-bot
    17.08.2023 05:59
    +4

    Вот именно из-за этого я в своё время все вычисления и хранение вел в extended. Так что нет, это таки баг компилятора, который не учитывает, что процессоры в режиме FPU386 используют 80-битные значения, и должен либо оба значения для сравнения держать на регистрах FPU, или ни одного.


    1. sergio_nsk
      17.08.2023 05:59
      +1

      Легко сказать, но сложно исправить. На всё регистров не хватит, ЦПУ самостоятельно со своим переупорядочиванием инструкций может сбросить одно из финальных значений из регистра FPU в кэш, даже если компилятор рассчитывал хранить значения в FPU до самого сравнения. Так что единственное решение - ничего из законченного не хранить в FPU, и оно есть -ffloat-store.

      Не понятно, почему такой проблемы нет в 64-битном бинарнике. ЦПУ тот же, FPU тот же, sizeof(double) тот же, а ошибки нет.


      1. Melirius
        17.08.2023 05:59
        +5

        В 64-битном режиме без специального флага 80-битный легаси FPU просто не используется, емнип.


        1. arteast
          17.08.2023 05:59
          +7

          Именно. Все 64-битные процессоры имеют как минимум поддержку SSE2, и для FP вычислений используются "нормальные" регистры FP и нормальные команды SSE, а не стековая машина и сопроцессор x87.

          В связи с этим мне непонятно, почему автор посчитал нормальным решением покалечить собственный код volatile-ами (и надеяться, что это единственное такое место и что он или кто-то другой потом вспомнит об этой баге, когда будет делать еще какое-нибудь такое сравнение), вместо того, чтобы просто добавить один флажок компилятору и компилировать 32-битную версию с использованием SSE вместо x87.


          1. voldemar_d
            17.08.2023 05:59
            +1

            компилировать 32-битную версию с использованием SSE вместо x87

            Я понимаю, что нынче все процессоры 64-битные, но если допустить, что 32-битный код будет исполняться на каком-нибудь процессоре, где нет SSE?


            1. arteast
              17.08.2023 05:59
              +3

              В 2023 году жить с процессором без поддержки SSE2 довольно грустно (это значит Vista или XP, старинные браузеры, и тд и тп). Но я могу себе представить кого-то, кто сидит на Athlon XP 20-летней давности.

              В 2023 жить с процессором без поддержки SSE - это мазохизм (для понимания - это Pentium II или более старые процессоры). Я думаю, что разработчик игрушки в 2023 может спокойно позволить себе пренебречь этим сегментом рынка.


              1. voldemar_d
                17.08.2023 05:59
                +1

                Я-то как раз это всё понимаю. Но бывает, что 32-битный код продолжает использоваться у некоторого ненулевого количества клиентов, у которых он уже 20 лет назад работал и работает сейчас (в том числе на XP и Athlon 20-летней давности). И есть ненулевая вероятность, что придется подправить какой-то древний баг, обновить у клиента код и заодно вдруг выдать кусок кода, который на его древнем процессоре не выполнится.


              1. cher-nov
                17.08.2023 05:59

                В 2023 жить с процессором без поддержки SSE - это мазохизм (для понимания - это Pentium II или более старые процессоры). Я думаю, что разработчик игрушки в 2023 может спокойно позволить себе пренебречь этим сегментом рынка.

                Проблема в том, что в SSE (первом) нет регистров двойной точности - только одинарной. А у автора в статье используется именно double.


                1. netch80
                  17.08.2023 05:59

                  Так и SSE2 уже лет 20 существует. Это надо ещё постараться найти процессор, где его нет.

                  (Да, я вижу комментарий выше про Athlon XP 20-летней давности. Но не верю. Ну реально, такое железо должно было сдохнуть по 100500 другим причинам.)


                  1. DungeonLords
                    17.08.2023 05:59
                    +1

                    Продажа процессоров AMD Geode прекратилась производителем только в 2019, а производители компьютеров используют их до сих пор
                    @voldemar_d@arteast@netch80


                    1. netch80
                      17.08.2023 05:59

                      Atom/Geode/... я и не предполагал сюда включать, там многое могло быть урезано.

                      Я думал написать исключение в скобках, но обломился.


            1. sergio_nsk
              17.08.2023 05:59

              Автор ведь чётко написал, что нет планов выпускать 32-битную версию. Так что "где нет SSE" не вариант.


              1. voldemar_d
                17.08.2023 05:59
                +5

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


          1. stuq1
            17.08.2023 05:59

            Если мне не изменяет память, то в случае с новыми версиями Visual Studio даже не придется ставить этот флаг, потому что там по дефолту SSE используется и для сборки под 32 бита


            1. unreal_undead2
              17.08.2023 05:59
              +1

              С clang'ом так же, а вот gcc всё ещё цепляется за x87.


      1. Melirius
        17.08.2023 05:59

        Ошибся веткой.


      1. Melirius
        17.08.2023 05:59
        +1

        В 64-битном режиме без специального флага 80-битный легаси FPU просто не используется, емнип.


      1. vesper-bot
        17.08.2023 05:59

        Напрочь отсутствует sizeof(extended) и все вычисления на FPU ведутся именно в double. То есть 80-битных чисел во время выполнения 64-битной программы нет нигде. А вот во время выполнения 32-битной — есть, в том, что на ассемблере видится как регистры сопроцессора, со всякими там fstp, fld и остальными опкодами для управления. Для совместимости с 80287, по-видимому.


        1. netch80
          17.08.2023 05:59

          Напрочь отсутствует sizeof(extended) и все вычисления на FPU ведутся
          именно в double. То есть 80-битных чисел во время выполнения 64-битной
          программы нет нигде.

          Лучше сказать, что FPU не используется (в терминах x86, SSE engine это не FPU, хотя в общем таки оно).

          Для совместимости с 80287, по-видимому.

          Это просто другой блок, со своей спецификой. Его и Intel и AMD делают отдельно и не объединяют. Это видно, например, по тому, как по-разному в них работают денормализованные: Intel ускорил их от древнего "до 200 тактов" для SSE сильно раньше, чем для x87 FPU.


    1. Tiriet
      17.08.2023 05:59

      не согласен с Вами. КМК это как раз иллюстрация особенностей работы с fp-числами и иллюстрация встроенной в них by design погрешности представления, которая может резко возрастать при некоторых операциях с малыми числами. Эта ошибка пьет кровь на вычислительной геометрии литрами- пересечение отрезков, параллельность прямых, сечения полигонов- она везде вылезает, когда надо сравнивать положения точек в пространстве- особенно точек пересечения и "ближе-дальше", и везде ее приходится аккуратно изолировать и обходить хитрыми способами.


  1. Melirius
    17.08.2023 05:59
    +2

    Что за фигня с комментариями? Отвечаю в ветку, мне показывает, что ответило в корень, а потом ещё дублирует сообщение.


    1. Tiriet
      17.08.2023 05:59
      +2

      похожая беда. после обновления страницы комменты вроде встали на место.


      1. voldemar_d
        17.08.2023 05:59

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


        1. Tiriet
          17.08.2023 05:59
          +2

          :-) тоже, наверное, какая-нибудь особенность работы XMM-регистров на новых процессорах так проявляется.


    1. BetsuNo
      17.08.2023 05:59
      +17

      Доброе пожаловать в клуб бага «мой коммент попал не в ту ветку» :)


      1. DrGluck07
        17.08.2023 05:59

        Не, это какой-то баг отображения. Он и вчера был.


  1. Opaspap
    17.08.2023 05:59

    ---del


  1. DrSmile
    17.08.2023 05:59
    +4

    Сколько текста, чтобы показать, что его изначальный тезис


    С другой стороны (и я настаиваю на этом): 1.0/2.0 гарантированно равно 1.0/2.0; 0.5 гарантированно равно 0.5; да, ДАЖЕ в случае double.

    в общем случае не является верным. В принципе, все как и ожидалось: автор не умеет обращаться с плавучкой, написал неустойчивый алгоритм и закономерно огреб (собственно, квадратный корень в конце функции distance тоже намекает). Результат вычисления одного и того же выражения при одних и тех же данных может различаться. А для исправления всего-то стоило бы заменить проблематичный std::set на двоичную кучу. Я уж не говорю, что у него изначально координаты целые, можно было бы, вообще, не использовать плавучку в промежуточных вычислениях.


    Производители компиляторов и дизайнеры языков программирования, к сожалению, тоже не уделяют должного внимания этой проблеме. Стоило бы ввести какие-нибудь __attribute__((fpu:deterministic)) и __attribute__((fpu:strict)), которые бы гарантировали детерминистичность и строгое соответствие стандарту IEEE, соответственно. Есть глобальные аналоги в виде опций компилятора, однако, должна быть возможность задавать их локально для конкретных функций или даже небольших блоков кода.


    1. ksbes
      17.08.2023 05:59
      +4

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

      А насчёт детерминистичности плавающей запятой - вот положа руку на сердце: на хрена? Вот ни разу за долгие годы работы над статистикой, физическими симуляциями и обработки изображений не сталкивался с необходимостью иметь именно детерминистичность. Понятно, что стандарт требует и понятно почему, но просто 99,900000000001% этого не оценят!


      1. MANAB
        17.08.2023 05:59

        0.099999999999% от 1.000.001 программистов это 1000 оценивших настоящих человек.


    1. voldemar_d
      17.08.2023 05:59
      +3

      в общем случае не является верным

      Если два значения double вычислены в точности одинаковым образом (имеется ввиду, в машинном коде процессора), и они побитно равны, с чего бы им различаться?


      1. ksbes
        17.08.2023 05:59

        Ну хотя бы потому что они могут по-разному потом хранится . Что и произошло у автора. Не говоря уже о том, что даблов бывет много, даже в рамках одного процессора.
        У меня тоже похожее было - но там шла перекодировка видео: где 12 бит, где 16 на плисине ... а джун решил отслеживать кадры по таймстемпу во флоатовских секундах ..


        1. voldemar_d
          17.08.2023 05:59

          У автора ситуация всё-таки не просто "хранится", а посложнее.


      1. unreal_undead2
        17.08.2023 05:59
        +8

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


        1. voldemar_d
          17.08.2023 05:59

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


      1. voldemar_d
        17.08.2023 05:59
        +1

        Для минусующих: это было не утверждение, а вопрос. Заданный как раз потому, что хочется разобраться. За задавание вопросов принято тоже минусы ставить?


      1. DrSmile
        17.08.2023 05:59
        +1

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


    1. Tiriet
      17.08.2023 05:59
      +4

      вот тут у него кстати, прикол! 1.0/2.0 гарантировано равно 1.0/2.0, а вот 1.0/5.0 уже не гарантировано равно 1.0/5.0! потому как первое- точно представимо конечной дробью в двоичной системе, а второе- периодической! и потому будет округлено в процессоре и черт его знает, что там в младших битах окажется.


      1. slonopotamus
        17.08.2023 05:59

        Окажется одно и то же.


        1. Tiriet
          17.08.2023 05:59

          1/2 в single, float & extended можно сравнивать друг с другом и в любых преобразованиях- и будет равенство. 1/5- во всех трех форматах разная и при сравнении можно получать разные результаты.


          1. slonopotamus
            17.08.2023 05:59
            +1

            Я не понимаю что такое "1.0/5.0 в float". В float будет "1.0f/5.0f". И понятно что это уже другое.


            1. vanxant
              17.08.2023 05:59

              Не, товарисч намекает на то, что 1/2 в двоичной записи (мантисса) это 0.100000(0)b. А 1/5 это 0.0011(0011)b - числа в скобках бесконечно повторяются и становится важно, где именно дробь будет обрезана


        1. Tiriet
          17.08.2023 05:59
          +3

          проверил только что:

          single(1/5)> double(1/5)
          extended(1/5)< double(1/5)
          extended(1/5)< single(1/5)

          single(1/2)== double(1/2)
          extended(1/2)== double(1/2)
          extended(1/2)== single(1/2)


      1. ksbes
        17.08.2023 05:59
        +1

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

        Вот только компьютер - это далеко не одна вычислительная машина!


        1. Tiriet
          17.08.2023 05:59

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


    1. netch80
      17.08.2023 05:59
      +1

      которые бы гарантировали детерминистичность и строгое соответствие стандарту IEEE, соответственно.

      Этот атрибут зовётся "используйте SSE". Потому что если в FPU работать не с long double, такое будет всегда - и это главная причина, почему MS в x86-64 вообще отказалась от FPU, а в Linux/FreeBSD/etc. мире FPU используется только по явной просьбе.

      (На самом деле частично не так. Если в FPU включить precision=double и пользоваться только даблами, или precision=single и пользоваться только single float, то эти проблемы уходят. Но это ещё надо знать. Используя SSE, устраняешь проблему в корне.)


      1. DrSmile
        17.08.2023 05:59

        Кроме x87 есть еще FMA и если окажется, что в одном случае a × b + c скомпилируется с его использованием, а в другом случае — без, то опять будет разница в результате. Или даже что-нибудь более простое, например, компилятор оптимизирует x + 1 + 1 до x + 2 и перестанет соблюдаться равенство с x + y + 1 при y = 1. Так что флаг "обеспечить детерминистичность блока кода" ничего общего не имеет с флагом "использовать SSE", особенно на каком-нибудь ARM или RISC-V.


        1. netch80
          17.08.2023 05:59

          Кроме x87 есть еще FMA

          Который таки надо включать явно (ну, -march=native я не рассматриваю тут).

          компилятор оптимизирует x + 1 + 1 до x + 2

          Без -ffast-math не разрешается.

          Так что флаг "обеспечить детерминистичность блока кода" ничего общего не
          имеет с флагом "использовать SSE", особенно на каком-нибудь ARM или
          RISC-V.

          Я согласен с этим в варианте, что, например, недопроверенный переход на RISC-V может включить FMA, но не в пределах x86.

          В общем, проблема есть, да. Но сейчас её легко контролировать.


  1. unreal_undead2
    17.08.2023 05:59
    +2

    Очевидно, мы должны иметь возможность добавлять в очередь с приоритетами две вершины с одним отклонением.

    И почему тогда не используется std::multiset?


    1. voldemar_d
      17.08.2023 05:59
      -1

      Наверное, для экономии памяти, если нет необходимости хранить более одного значения на элемент?


      1. unreal_undead2
        17.08.2023 05:59
        +1

        Вроде как танцы с бубном в функции сравнения как раз для того, чтобы заставить set хранить несколько одинаковых значений.


        1. voldemar_d
          17.08.2023 05:59

          Если так, то да, непонятно, зачем это. Но и при запихивании в std::multiset случится аналогичная проблема на 32-битном коде, как я понимаю?


          1. unreal_undead2
            17.08.2023 05:59

            Проблема из-за того, что в коде два сравнения da и db (на меньше и точное равенство), которые могут вести себя неконсистентно. С одним сравнением проблем не будет.

            Правда, задача всё таки хитрее - насколько понял после второго прочтения, полностью совпадающие вершины надо всё-таки объединить, но оставить разные вершины с одинаковым deviation. Так что просто так использовать multiset тоже нельзя...


  1. Ryppka
    17.08.2023 05:59

    Обычное дело в низкоуровневом коде, напрямую с железом и не такие чудеса случаются(


  1. Leetc0deMonkey
    17.08.2023 05:59

    А нельзя сравниватьGVertex напрямую? Не знаю что там внутрях, но подозреваю что 3 координаты и вdeviation производится проекция в 2 плюс ещё что-то.


  1. Ryppka
    17.08.2023 05:59

    Если более серьезно.

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

    Если компилятор генерит код, сравнивающий объекты с разным двоичным представлением, то с моей точки зрения это все-таки баг компилятора. Я не знакток x87, в нем сравнения только регистр-регистр или есть и регистр-память?

    Использование volatile для точечного подавления нежелательной оптимизации выглядит как элегантный трюк.


    1. ultrinfaern
      17.08.2023 05:59
      +1

      Можно сказать что двоичное представление есть только у памяти. А если это регистр то это уже не двоичное представление. Вот автор и на это и наткнулся - выгрузка/загрузка в память из регистра это преобразование с округлением.


  1. DmitryR1974
    17.08.2023 05:59
    +1

    Я бы не сказал, что это баг процессора. Вы сравниваете в коде два double, и компилятор не имеет права оптимизировать конверсию. Это примерно как если бы компилятор работал с uint8_t в тридцатидвухразрядных регистрах (в RISC например по-другому нельзя), иногда обнуляя старшие биты, а иногда - нет.
    Во-вторых 1.0/2.0 на самом деле вряд ли будет отличаться от 1.0/2.0, а вот 1.0/6.0 вычисленное и округленное от 1.0/6.0 константы, сразу вычисленной до того же знака - запросто.


    1. alcotel
      17.08.2023 05:59
      -1

      С авторством бага согласен. Компилятору же сказали: "скомпилируй для х86 и сравни это, как даблы", или хоть даже как fp32, и было бы всё детерминировано. "А он что? Пошёл, и кеды купил" (c) Захотелось ему аппаратно сравнить как fp80. Получается, можно ведь и на такую штуку нарваться:

      // Написал в одном месте
      если a > b, то делаем РАЗ
      // потом ещё какой-то код
      // а потом
      если a <= b, то делаем ДВА

      И вроде автор такого кода имел в виду, что всегда выполнится либо РАЗ, либо ДВА. И абсолютно чётко это сказал, и не пользовался "не принятыми в приличном обществе паттернами работы с fp". Но в зависимости от оптимизации и окружающего кода автор нарвётся на рандомное выполнение РАЗ и ДВА, или ни РАЗ и ни ДВА.


  1. titbit
    17.08.2023 05:59
    +4

    на самом деле это не баг компилятора: это баг процессора. Да, всё верно.

    Нет, здесь и близко нет никакого бага в процесоре. Почитайте errata на процессоры для понимания какие бывают баги именно в процессорах.

    Если точнее, то это поведение FPU (Floating-Point Unit), вызывающее неожиданное поведение при оптимизации кода.

    FPU ничего не знает ни про компилятор, ни про его оптимизации. Просто надо использовать FPU правильно, или тогда уж не использовать вообще если нет уверенности.

    А вишенкой на торте стало простое и оптимальное исправление (использование volatile).

    Мда, у нас за исправления багов с помощью расстановки volatile - больно бьют канделябром на ревью. Не надо так "исправлять" баги.


  1. adeshere
    17.08.2023 05:59
    +1

    Вопрос к знатокам: подскажите, а может ли выгрузка 80-битных real в 64-битный double и наоборот при каких-то особых условиях приводить к появлению Nan?

    Дело в том, что у меня некоторое время назад обнаружился очень похожий баг Шредингера:

    - он появлялся только в оптимизированной версии 32-битного кода;
    - баг не возникает, если работаешь под отладчиком;
    - баг может исчезать после добавления каких-то совершенно левых операторов в функцию. (Например, оператор print его с гарантией убирает);
    - баг возникает лишь на некоторых процессорах (примерно половина из протестированных).

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

    Коллективный разум в Q&A пытался мне помочь, но победа от нас ускользнула. Возможно из-за того, что я пишу на фортране, а это комьюнити не такое обширное и прокачанное, как у плюсовиков (которых к тому же поддерживают братские семейные кланы ;-).

    Так вот: после максимальной изоляции моего бага (что было не очень просто в силу перечисленных выше причин) одна из форм его проявления свелась к появлению Nan вместо ожидаемого числа в тривиальном операторе присвоения с преобразованием типов от целого к двойной точности:

    (real*8) R8 = (integer*8) I8

    Может ли это быть проявлением аналогичного эффекта? (Значение R*8 далее используется в вычислениях с плавающей точкой, оно наверняка проведет значительную часть своей жизни в FPU).

    И что посоветуете, учитывая что у меня в фортране нет возможности включать/выключать оптимизацию отдельных операторов и

    даже функций?

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

    И без оптимизации мне тоже не жизнь (разница в скорости на два+ порядка не оставляет возможности выбирать).

    Ну и на 64-битную версию я тоже не могу перейти, т.к. у многих пользователей компы

    32-битные

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


    1. cher-nov
      17.08.2023 05:59
      +1

      Попробуйте записывать в лог не только числа, которые выдаёт генератор, но и контекст x87 FPU. Для его получения используйте команду FNSAVE и затем FRSTOR из сохранённого, потому что FNSAVE переинициализирует сопроцессор. Причём делайте это как до вызова функции RANDOM(), так и после. Это как минимум даст больше информации о происходящем.


      1. adeshere
        17.08.2023 05:59

        ...команду FNSAVE и затем FRSTOR

        К сожалению, я не умею делать asm-вставки в фортрановский код. Наверно, это можно как-то реализовать под отладчиком, но при запуске из-под отладчика баг исчезает вне зависимости от каких-либо других обстоятельств


        1. cher-nov
          17.08.2023 05:59
          +1

          Скверно. Полистал интернеты - пишут, мол, в Intel Fortran возможности делать ассемблерные вставки вообще нет. Однако я покумекал также над справкой и нашёл там такое:

          https://www.intel.com/content/www/us/en/docs/fortran-compiler/developer-guide-reference/2023-2/check-the-floating-point-stack-state.html:

          If the application calls a function without defining or incorrectly defining the function's prototype, the compiler cannot determine if the function must return a floating-point value. Consequently, the return value is not popped off the floating-point stack if it is not used. This can cause the floating-point stack to overflow.

          The overflow of the stack results in two undesirable situations:

          • A NaN value gets involved in the floating‑point calculations
          The program results become unpredictable; the point where the program starts making errors can be arbitrarily far away from the point of the actual error.

          https://www.intel.com/content/www/us/en/docs/fortran-compiler/developer-guide-reference/2023-1/fp-stack-check-qfp-stack-check.html:

          This option tells the compiler to generate extra code after every function call to ensure that the floating-point (FP) stack is in the expected state.

          By default, there is no checking. So when the FP stack overflows, a NaN value is put into FP calculations and the program's results differ. Unfortunately, the overflow point can be far away from the point of the actual bug. This option places code that causes an access violation exception immediately after an incorrect call occurs, thus making it easier to locate these issues.

          Попробуйте прописать при компиляции ключ /Qfp-stack-check (насколько я понимаю, Вы собираете из-под Windows) и повторно запустить проверку. Если предположение выше верно, то она должна на первом же NaN'е вывалиться с Access Violation.


          1. adeshere
            17.08.2023 05:59

            @cher-nov, спасибо огромное!!!!

            Да, все именно так! При включенной проверке /Qfp-stack-check программа вылетает на первом же NaN'е по Access Violation.

            Так вот , в продолжение вопроса:

            Точка вылета по Access Violation оказалась совсем не там, где у меня появлялся Nan, а как раз в том модуле, который обращается к системному таймеру. (Для меня с самого начала было большой загадкой, почему Nan-ы исчезают, если к таймеру не обращаться - но теперь факт связи доказан).

            Но!

            Именно в этом модуле у меня вообще нет никаких FP-операций. Ни одной! Есть арифметика в Integer*8 и форматная запись целого числа в строку. И все. А Access Violation происходит на операторе call, причем вызывается там не функция, а подпрограмма с единственным целым параметром.

            Из приведенных цитат

            Лазить в справку по ссылкам наподобие приведенных у меня в последнее время не получается, так что буду ориентироваться на те цитаты, которые Вы привели

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

            Откуда вытекает вопрос: а может ли эта функция (с некорректным прототипом) оказаться библиотечной, а не моей? И если да, то что посоветуете? Я пока вообще не понимаю, можно ли как-то к библиотечной (встроенной в язык) функции прототип написать...

            @cher-nov


            1. cher-nov
              17.08.2023 05:59
              +1

              После вызова какой именно функции программа вываливается по Access Violation?

              Именно в этом модуле у меня вообще нет никаких FP-операций. Ни одной! Есть арифметика в Integer*8 и форматная запись целого числа в строку. И все. А Access Violation происходит на операторе call, причем вызывается там не функция, а подпрограмма с единственным целым параметром.

              Погодите, но ведь Вы же пишете сами чуть выше:

              Точка вылета по Access Violation оказалась совсем не там, где у меня появлялся Nan, а как раз в том модуле, который обращается к системному таймеру. (Для меня с самого начала было большой загадкой, почему Nan-ы исчезают, если к таймеру не обращаться - но теперь факт связи доказан).

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

              Откуда вытекает вопрос: а может ли эта функция (с некорректным прототипом) оказаться библиотечной, а не моей? И если да, то что посоветуете?

              Всякое бывает, но я бы на это не рассчитывал. Кривые прототипы, как правило, проявляются очень быстро.

              Я пока вообще не понимаю, можно ли как-то к библиотечной (встроенной в язык) функции прототип написать...

              А зачем?

              Лазить в справку по ссылкам наподобие приведенных у меня в последнее время не получается, так что буду ориентироваться на те цитаты, которые Вы привели

              Для последних времён хороший прокси-сервер или VPN уже попросту необходим.


              1. adeshere
                17.08.2023 05:59

                После вызова какой именно функции программа вываливается по Access Violation?

                Код на фортране (падает call screen_putl0_time())

                Строка percent_string - глобальная, цепляется через USE:

                c.......................................................................c
                SUBROUTINE SCREEN_PUTL0_PERCENTS(CURRENT_PERCENT)
                USE ABD_INC; USE HEADERS
                integer8, intent(in) :: current_percent

                integer8 i8
                c
                c.....Спецзначение "-1" заменяем на total0_100percents:
                i8=current_percent;

                if ((i8 < 0_8).or.(i8 > total0_100percents)) i8=total0_100percents
                call set_percents(i8) ! сформировали percent_string, тут есть целое деление i8/i8
                c
                c.....Спецзначения 0 и total0_100percents печатаем всегда, без учета таймера:
                if ((i8 == 0_8).or.(i8 >= total0_100percents)) then; call screen_putl0(percent_string)
                else; call screen_putl0_time(percent_string); end if
                end

                Дизассемблер (но тут я уже ничего понять не могу; даже ret не нашел)
                  SUBROUTINE SCREEN_PUTL0_PERCENTS(CURRENT_PERCENT) 
                  USE ABD_INC;  USE HEADERS
                  integer*8, intent(in) ::         current_percent
                

                00477910 push ebx
                00477911 mov ebx,esp
                00477913 and esp,0FFFFFFF0h
                00477916 push ebp
                00477917 push ebp
                00477918 mov ebp,dword ptr [ebx+4]
                0047791B mov dword ptr [esp+4],ebp
                0047791F mov ebp,esp
                00477921 sub esp,48h
                00477924 push eax
                00477925 push edi
                00477926 push ecx
                00477927 mov edi,ebp
                00477929 sub edi,48h
                0047792C mov ecx,12h
                00477931 mov eax,0CCCCCCCCh
                00477936 rep stos dword ptr es:[edi]
                00477938 pop ecx
                00477939 pop edi
                0047793A pop eax
                0047793B mov edx,dword ptr [ebx+8]
                0047793E mov dword ptr [ebp-14h],esi
                00477941 mov dword ptr [ebp-18h],edi

                c
                c.....Спецзначение "-1" заменяем на total0_100percents:
                i8=current_percent; if ((i8 < 0_8).or.(i8 > total0_100percents)) i8=total0_100percents
                call set_percents(i8) ! сформировали percent_string, тут есть целое деление i8/i8
                c
                00477944 mov eax,dword ptr [edx]
                00477946 mov esi,dword ptr [edx+4]
                00477949 xor edx,edx
                0047794B mov edi,esi
                0047794D mov dword ptr [ebp-8],eax
                00477950 sub edi,edx
                00477952 mov ecx,dword ptr [_ABD_INC_mp_TOTAL0_100PERCENTS (977000h)]
                00477958 mov eax,dword ptr [_ABD_INC_mp_TOTAL0_100PERCENTS+4 (977004h)]
                0047795D mov dword ptr [ebp-0Ch],ecx
                00477960 mov dword ptr [ebp-10h],eax
                00477963 jl SCREEN_PUTL0_PERCENTS+66h (477976h)
                00477965 mov edx,dword ptr [ebp-8]
                00477968 mov eax,esi
                0047796A sub edx,dword ptr [ebp-0Ch]
                0047796D sbb eax,dword ptr [ebp-10h]
                00477970 jl SCREEN_PUTL0_PERCENTS+6Eh (47797Eh)
                00477972 or edx,eax
                00477974 je SCREEN_PUTL0_PERCENTS+6Eh (47797Eh)
                00477976 mov eax,ecx
                00477978 mov esi,dword ptr [ebp-10h]
                0047797B mov dword ptr [ebp-8],eax
                c.....Спецзначения 0 и total0_100percents печатаем всегда, без учета таймера:
                0047797E xor eax,eax
                00477980 mov edx,dword ptr [ebp-10h]
                00477983 sub edx,eax
                00477985 jl SCREEN_PUTL0_PERCENTS+12Ch (477A3Ch)
                0047798B or edx,dword ptr [ebp-0Ch]
                0047798E je SCREEN_PUTL0_PERCENTS+12Ch (477A3Ch)
                00477994 mov eax,dword ptr [ebp-8]
                00477997 mov edx,esi
                00477999 sub eax,dword ptr [ebp-0Ch]
                0047799C sbb edx,dword ptr [ebp-10h]
                0047799F jge SCREEN_PUTL0_PERCENTS+98h (4779A8h)
                004779A1 mov edx,dword ptr [ebp-8]
                004779A4 mov eax,esi
                004779A6 jmp SCREEN_PUTL0_PERCENTS+9Dh (4779ADh)
                004779A8 mov edx,ecx
                004779AA mov eax,dword ptr [ebp-10h]
                004779AD mov edi,eax
                004779AF xor ecx,ecx
                004779B1 sub edi,ecx
                004779B3 jge SCREEN_PUTL0_PERCENTS+0A9h (4779B9h)
                004779B5 xor edx,edx
                004779B7 xor eax,eax
                004779B9 push eax
                004779BA push edx
                004779BB xor ecx,ecx
                004779BD push ecx
                004779BE push 64h
                004779C0 mov dword ptr [ebp-48h],ecx
                004779C3 mov dword ptr [ebp-28h],4
                004779CA mov dword ptr [ebp-24h],offset _WINABD_INC_mp_PERCENT_STRING (234A040h)
                004779D1 call _allmul (751620h)
                004779D6 push dword ptr [ebp-10h]
                004779D9 push dword ptr [ebp-0Ch]
                004779DC push edx
                004779DD push eax
                004779DE call _alldiv (751570h)
                004779E3 push 22h
                004779E5 push 97E5B4h
                004779EA mov dword ptr [ebp-20h],eax
                004779ED lea ecx,[ebp-28h]
                004779F0 push ecx
                004779F1 push 7E39ACh
                004779F6 push 8384FF01h
                004779FB mov dword ptr [ebp-1Ch],edx
                004779FE lea edi,[ebp-48h]
                00477A01 push edi
                00477A02 call _for_write_int_fmt (6C0D90h)
                00477A07 fldz
                00477A09 fldz
                00477A0B fldz
                00477A0D fldz
                00477A0F fldz
                00477A11 fldz
                00477A13 fldz
                00477A15 fldz
                00477A17 push eax
                00477A18 fnstsw ax
                00477A1A test ax,40h
                00477A1E je SCREEN_PUTL0_PERCENTS+114h (477A24h)
                00477A20 xor eax,eax
                00477A22 mov dword ptr [eax],eax
                00477A24 pop eax
                00477A25 fstp st(0)
                00477A27 fstp st(0)
                00477A29 fstp st(0)
                00477A2B fstp st(0)
                00477A2D fstp st(0)
                00477A2F fstp st(0)
                00477A31 fstp st(0)
                00477A33 fstp st(0)
                00477A35 add esp,18h
                00477A38 test eax,eax
                00477A3A jle SCREEN_PUTL0_PERCENTS+136h (477A46h)
                00477A3C mov dword ptr [_WINABD_INC_mp_PERCENT_STRING (234A040h)],2A2A2A2Ah
                if ((i8 == 0_8).or.(i8 >= total0_100percents)) then; call screen_putl0(percent_string)
                else; call screen_putl0_time(percent_string); end if
                end
                00477A46 mov eax,dword ptr [ebp-8]
                00477A49 or eax,esi
                00477A4B je SCREEN_PUTL0_PERCENTS+14Eh (477A5Eh)
                00477A4D mov eax,dword ptr [ebp-8]
                00477A50 sub eax,dword ptr [_ABD_INC_mp_TOTAL0_100PERCENTS (977000h)]
                00477A56 sbb esi,dword ptr [_ABD_INC_mp_TOTAL0_100PERCENTS+4 (977004h)]
                00477A5C jl SCREEN_PUTL0_PERCENTS+18Ah (477A9Ah)
                00477A5E push 4
                00477A60 push offset _WINABD_INC_mp_PERCENT_STRING (234A040h)
                00477A65 call SCREEN_PUTL0 (54BC10h)
                00477A6A fldz
                00477A6C fldz
                00477A6E fldz
                00477A70 fldz
                00477A72 fldz
                00477A74 fldz
                00477A76 fldz
                00477A78 fldz
                00477A7A push eax
                00477A7B fnstsw ax
                00477A7D test ax,40h
                00477A81 je SCREEN_PUTL0_PERCENTS+177h (477A87h)
                00477A83 xor eax,eax
                00477A85 mov dword ptr [eax],eax
                00477A87 pop eax
                00477A88 fstp st(0)
                00477A8A fstp st(0)
                00477A8C fstp st(0)
                00477A8E fstp st(0)
                00477A90 fstp st(0)
                00477A92 fstp st(0)
                00477A94 fstp st(0)
                00477A96 fstp st(0)
                00477A98 jmp SCREEN_PUTL0_PERCENTS+1C4h (477AD4h)

                00477A9A push 4
                00477A9C push offset _WINABD_INC_mp_PERCENT_STRING (234A040h)
                00477AA1 call SCREEN_PUTL0_TIME (54BD10h)
                00477AA6 fldz
                00477AA8 fldz
                00477AAA fldz
                00477AAC fldz
                00477AAE fldz
                00477AB0 fldz
                00477AB2 fldz
                00477AB4 fldz
                00477AB6 push eax
                00477AB7 fnstsw ax
                00477AB9 test ax,40h
                00477ABD je SCREEN_PUTL0_PERCENTS+1B3h (477AC3h)
                00477ABF xor eax,eax
                00477AC1 mov dword ptr [eax],eax
                00477AC3 pop eax
                00477AC4 fstp st(0)
                00477AC6 fstp st(0)
                00477AC8 fstp st(0)
                00477ACA fstp st(0)
                00477ACC fstp st(0)
                00477ACE fstp st(0)
                00477AD0 fstp st(0)
                00477AD2 fstp st(0)
                00477AD4 add esp,8
                C.......................................................................C
                00477AD7 mov esi,dword ptr [ebp-14h]
                00477ADA mov edi,dword ptr [ebp-18h]
                00477ADD mov esp,ebp
                00477ADF pop ebp
                00477AE0 mov esp,ebx
                00477AE2 pop ebx
                00477AE3 ret
                00477AE4 nop dword ptr [eax+eax]
                00477AE9 nop dword ptr [eax]
                c SCREEN_PUTL0_PERCENTS печатает в сохраненной по CURSOR_STORE позиции c
                c значение PERCENTS(CURRENT) (диапазон 0...100:), но не чаще 1 раза в секунду. c
                c Для проверки частоты вывода использует таймер N5 из библиотеки Clib.for. c
                c Вызов SCREEN_PUTL0_PERCENTS рекомендуется "оборачивать" в Need_Display, c
                c чтобы таймер вызывался пореже и обращения к нему не задерживали работу. c
                c NEED_DISPLAY выбирает среди всех значений переменной цикла i8 такие, что c
                c для них стоит обновить строку статуса. Вначале это все строки подряд, но по c
                c мере увеличения i8 вывод на экран происходит все реже. c
                c При I8 > $Refresh_step2 частота обновления статуса задается параметром c c $Refresh_step в ABD_INC. Но эта частота подходит для "быстрых" циклов. c c Если печатать сообщение надо почаще (в цикле есть вложенный цикл или долгая c c операция), то задайте коэффициент мультипликации - опциональный параметр c c factor, что увеличит частоту обновления в factor раз. c C...............................................................................C c C.......................................................................C SUBROUTINE SET_PERCENT_RANGE (TOTAL) 00477AF0 push ebp 00477AF1 mov ebp,esp 00477AF3 mov eax,dword ptr [TOTAL] USE ABD_INC; USE HEADERS integer8, intent(in) :: total
                total0_100percents=total
                00477AF6 mov edx,dword ptr [eax]
                00477AF8 mov ecx,dword ptr [eax+4]
                00477AFB mov dword ptr [_ABD_INC_mp_TOTAL0_100PERCENTS (977000h)],edx
                00477B01 mov dword ptr [_ABD_INC_mp_TOTAL0_100PERCENTS+4 (977004h)],ecx
                end
                00477B07 mov esp,ebp
                00477B09 pop ebp
                00477B0A ret
                00477B0B nop dword ptr [eax+eax]

                Что-то не могу разобраться, как вставить сюда код. Тащу его из редактора... но при вставке длинные строки режутся на куски. А после пометки фрагмента, как "код" некоторые короткие склеиваются. Вставил фрагментами. Сомневаюсь, что при таком форматировании можно что-то понять, но пусть останется.


              1. adeshere
                17.08.2023 05:59

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

                Очень похоже, что все именно так.

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

                Изначально я получал системное время вызовом встроенной функции DATE_AND_TIME. Она возвращает набор целых значений, и в моем коде в этом месте никаких FP не было.

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

                На данный момент у меня в рабочем варианте прицеплена функция RTC(), которая возвращает число секунд от базовой даты в виде real*8. Так что про FP-вычисления я соврал: они в моем интерфейсе к таймеру все-таки есть. Но проблема явно не в них. Баг впервые обнаружился, когда их еще не было. Судя по всему, где-то в глубине такие вычисления происходят вне зависимости от того, как именно я вызываю таймер.

                И еще вопрос про FPU-стек: подтверждает ли эту версию тот факт, что для появления Nan требуется обратиться к таймеру несколько раз? Падает не с первого раза. Но тут счет идет не на миллионы, а на единицы вызовов. (Изначально я писал про миллионы операций с Real*8, но к таймеру-то я обращаюсь гораздо реже


              1. adeshere
                17.08.2023 05:59

                >> Я пока вообще не понимаю, можно ли как-то к библиотечной (встроенной в язык) функции прототип написать...

                А зачем?

                Если дело вдруг все-таки в библиотечной функции а не в моей. Что там какой-то редкий полу-глюк, проявляющийся только в сочетании с моими (не совсем безопасными) ключами компиляции способам и ее вызова. Хотя это очень маловероятно, конечно. Да и к тому же все функции работы с таймером, которые я могу вызывать, уже описаны в файле ifport.f90 (я там тоже копал и ничего подозрительного не нашел).

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

                В общем, факт наличия бага налицо, теперь благодаря Вам даже почти понятна его природа. Непонятно только, что делать. Я уже сейчас пытаюсь менять одинарную точность на двойную и наоборот там, где это не имеет никакого значения с точки зрения алгоритма. Просто по принципу - "а ну как если вдруг?!"

                > ...прокси-сервер или VPN уже попросту необходим.

                Понимаю, но с наскока разобраться не получилось, пришлось

                пока отложить :-(

                В смутной надежде, что "новая реальность" не навсегда...


                1. cher-nov
                  17.08.2023 05:59
                  +1

                  Я здесь тогда сразу на все три комментария отвечу, чтобы их последовательность не сбивать.

                  Код на фортране (падает call screen_putl0_time())

                  Дизассемблер (но тут я уже ничего понять не могу; даже ret не нашел)

                  Ага, ну вот смотрите. Видите в дизассемблированном коде команды fldz, fnstsw и fstp ? Вот это именно то, что вставляет компилятор с командой /Qfp-stack-check.

                  Что здесь происходит:

                  1. Стек сопроцессора (8 значений) забивается нулями.

                  2. После этого в x87 FPU status word проверяется флаг "stack fault" (7ой бит, 40h).

                  3. Если он взведён, то происходит обращение по нулевому адресу, приводящее к Access Violation.

                  4. В противном же случае стек сопроцессора очищается.

                  У Вас это происходит перед вызовами следующих функций:

                  • _for_write_int_fmt

                  • SCREEN_PUTL0

                  • SCREEN_PUTL0_TIME

                  По Вашим словам, падает на третьей функции. Значит, ищите ошибку где-то в ней. Будет хорошо, если Вы приведёте здесь её прототип, исходный код и дизассемблированный вид.

                  Если дело вдруг все-таки в библиотечной функции а не в моей. Что там какой-то редкий полу-глюк, проявляющийся только в сочетании с моими (не совсем безопасными) ключами компиляции способам и ее вызова. Хотя это очень маловероятно, конечно. Да и к тому же все функции работы с таймером, которые я могу вызывать, уже описаны в файле ifport.f90 (я там тоже копал и ничего подозрительного не нашел).

                  А какая у Вас версия Intel Fortran? И пробовали ли Вы более новые - вдруг в Вашей действительно есть какой-нибудь баг кодогенератора, вроде этого?

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

                  Непонятно только, что делать. Я уже сейчас пытаюсь менять одинарную точность на двойную и наоборот там, где это не имеет никакого значения с точки зрения алгоритма. Просто по принципу - "а ну как если вдруг?!"

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

                  В смутной надежде, что "новая реальность" не навсегда...

                  Ну, под луной ничто не вечно, но и мы в том числе. Пока толстый сохнет, худой сдохнет. Держи порох сухим, готовь сани летом.


                  1. adeshere
                    17.08.2023 05:59

                    Функция screen_putl0_time(text) тоже, к сожалению, содержит несколько вложенных вызовов. Но, Ваши советы навели меня на одну мысль.

                    Вот тут сама функция и ее прототип:

                    Прототип:

                    subroutine screen_putl0_time(text)
                    character, intent(in) :: text*(*)
                    end subroutine

                    Функция:

                    C........................................C
                    SUBROUTINE SCREEN_PUTL0_TIME(TEXT)
                    USE ABD_INC; USE HEADERS
                    character, intent(in) :: text*()
                    real4 t
                    integer*4, save :: isw=0
                    c
                    c Первый вызов: isw=0: инициализация.
                    c Остальные вызовы: isw=1: подсчет интервала
                    t=timer_mm(5,isw)
                    isw=1
                    if (t < $Screen_counter_time) return
                    call screen_putl0(text)
                    t=timer_mm(5,0)
                    end

                    И вот тут у меня возникло страшное подозрение.

                    При втором вызове таймера (в предпоследней строке) возвращаемое значение t никак не используется. А не мог ли оптимизатор это заметить, и ... ? В общем,

                    вот тут дизассемблер

                    Но там какое-то жуткое спагетти из заинлайненных функций и их фрагментов :-((( И злополучную строчку "t=timer_mm(5,0)" я в этом дизассемблере так и не смог отыскать...

                    Вдобавок при вставке дизассемблера копипастой хаброредактор часть кода распознает, как разметку (?!?), и я на экране вижу такие отборные трехслойные формулы, что в прилимчном обществе стыдно скриншот приложить... Поэтому я собрал дизассемблер из кусочков "как смог", а в дополнение этому выложил его в виде Word-документа (чтобы шрифты сохранились).

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

                    C    

                    C.......................................................................C

                          SUBROUTINE SCREEN_PUTL0_TIME(TEXT)

                    0054BD10  push        ebp 

                    0054BD11  mov         ebp,esp

                    0054BD13  sub         esp,30h

                    0054BD16  push        eax 

                    0054BD17  push        edi 

                    0054BD18  push        ecx 

                    0054BD19  mov         edi,ebp

                    0054BD1B  sub         edi,30h

                    0054BD1E  mov         ecx,0Ch

                    0054BD23  mov         eax,0CCCCCCCCh

                    0054BD28  rep stos    dword ptr es:[edi]

                    0054BD2A  pop         ecx 

                    0054BD2B  pop         edi 

                    0054BD2C  pop         eax 

                    C    

                    C.......................................................................C

                          SUBROUTINE SCREEN_PUTL0 (TEXT)

                          USE HEADERS

                          character text*(*)

                          if (screen_pos_is_not_correct()) call error(-82)

                          call screen_putl(cursor_line,cursor_icol,text)

                          end

                    C    

                    C.......................................................................C

                          SUBROUTINE SCREEN_PUTL0_TIME(TEXT)

                          USE ABD_INC;  USE HEADERS

                          character, intent(in) :: text*(*)

                          real*4    t

                          integer*4, save :: isw=0

                    c

                          t=timer_mm(5,isw)   ! Первый вызов: инициализация. Остальные: подсчет

                    0054BD2D  push        offset _SAVE_CATALOG_DATA_AS_SERIES$BLK..T6002_+17Ch (1521994h)

                    C    

                    C.......................................................................C

                          SUBROUTINE SCREEN_PUTL0_TIME(TEXT)

                    0054BD32  mov         dword ptr [ebp-0Ch],ebx

                    0054BD35  mov         dword ptr [ebp-8],edi

                    C    

                    C.......................................................................C

                          SUBROUTINE SCREEN_PUTL0 (TEXT)

                          USE HEADERS

                          character text*(*)

                          if (screen_pos_is_not_correct()) call error(-82)

                          call screen_putl(cursor_line,cursor_icol,text)

                          end

                    C    

                    C.......................................................................C

                          SUBROUTINE SCREEN_PUTL0_TIME(TEXT)

                          USE ABD_INC;  USE HEADERS

                          character, intent(in) :: text*(*)

                          real*4    t

                          integer*4, save :: isw=0

                    c

                          t=timer_mm(5,isw)   ! Первый вызов: инициализация. Остальные: подсчет

                    0054BD38  push        7F4AE8h

                    C    

                    C.......................................................................C

                          SUBROUTINE SCREEN_PUTL0_TIME(TEXT)

                    0054BD3D  mov         dword ptr [ebp-4],esi

                    0054BD40  mov         ebx,dword ptr [TEXT]

                    0054BD43  mov         edi,dword ptr [.tmp..T2127__V$21da]

                          USE ABD_INC;  USE HEADERS

                          character, intent(in) :: text*(*)

                          real*4    t

                          integer*4, save :: isw=0

                    c

                          t=timer_mm(5,isw)   ! Первый вызов: инициализация. Остальные: подсчет

                    0054BD46  call        TIMER_MM (4DE250h)

                    0054BD4B  fldz            

                    0054BD4D  fldz            

                    0054BD4F  fldz            

                    0054BD51  fldz            

                    0054BD53  fldz            

                    0054BD55  fldz            

                    0054BD57  fldz            

                    0054BD59  push        eax 

                    0054BD5A  fnstsw      ax  

                    0054BD5C  test        ax,40h

                    0054BD60  je          SCREEN_PUTL0_TIME+56h (54BD66h)

                    0054BD62  xor         eax,eax

                    0054BD64  mov         dword ptr [eax],eax

                    0054BD66  pop         eax 

                    0054BD67  fstp        st(0)

                    0054BD69  fstp        st(0)

                    0054BD6B  fstp        st(0)

                    0054BD6D  fstp        st(0)

                    0054BD6F  fstp        st(0)

                    0054BD71  fstp        st(0)

                    0054BD73  fstp        st(0)

                    0054BD75  fstp        dword ptr [ebp-14h]

                    0054BD78  add         esp,8

                    0054BD7B  movss       xmm1,dword ptr [ebp-14h]

                          SUBROUTINE SCREEN_PUTL0 (TEXT)

                          USE HEADERS

                          character text*(*)

                          if (screen_pos_is_not_correct()) call error(-82)

                          call screen_putl(cursor_line,cursor_icol,text)

                          end

                    C    

                    C.......................................................................C

                          SUBROUTINE SCREEN_PUTL0_TIME(TEXT)

                          USE ABD_INC;  USE HEADERS

                          character, intent(in) :: text*(*)

                          real*4    t

                          integer*4, save :: isw=0

                    c

                          t=timer_mm(5,isw)   ! Первый вызов: инициализация. Остальные: подсчет

                          isw=1

                          if (t < $Screen_counter_time) return

                    0054BD80  movss       xmm0,dword ptr [FIND_AND_ADD_HOTKEY_REGION$BLK_debug_param_const+2D8h (7F5CD8h)]

                          isw=1

                    0054BD88  mov         dword ptr [_SAVE_CATALOG_DATA_AS_SERIES$BLK..T6002_+17Ch (1521994h)],1

                          if (t < $Screen_counter_time) return

                    0054BD92  comiss      xmm0,xmm1

                    0054BD95  ja          SCREEN_PUTL0_TIME+140h (54BE50h)

                            call screen_putl0(text)

                    0054BD9B  mov         esi,dword ptr [_WINABD_INC_mp_CURSOR_LINE (90D6ECh)]

                    0054BDA1  xor         ecx,ecx

                    0054BDA3  mov         edx,0FFFFFFFFh

                    0054BDA8  xor         eax,eax

                    0054BDAA  test        esi,esi

                    0054BDAC  cmovle      eax,edx

                    0054BDAF  cmp         esi,dword ptr [_WINABD_INC_mp_SCREEN_ROWS (90D700h)]

                    0054BDB5  mov         dword ptr [ebp-14h],edi

                    0054BDB8  mov         edi,ecx

                    0054BDBA  cmovg       edi,edx

                    0054BDBD  xor         esi,esi

                    0054BDBF  or          eax,edi

                    0054BDC1  mov         edi,dword ptr [_WINABD_INC_mp_CURSOR_ICOL (90D6E8h)]

                    0054BDC7  test        edi,edi

                    0054BDC9  cmovle      esi,edx

                    0054BDCC  cmp         edi,dword ptr [_WINABD_INC_mp_SCOLS (90D6FCh)]

                    0054BDD2  mov         edi,dword ptr [ebp-14h]

                    0054BDD5  cmovle      edx,ecx

                    0054BDD8  or          eax,esi

                    0054BDDA  or          eax,edx

                    0054BDDC  test        al,1

                    0054BDDE  jne         SCREEN_PUTL0_TIME+14Ah (54BE5Ah)

                    0054BDE0  mov         eax,offset _WINABD_INC_mp_CURSOR_LINE (90D6ECh)

                    0054BDE5  mov         edx,offset _WINABD_INC_mp_CURSOR_ICOL (90D6E8h)

                    0054BDEA  mov         ecx,ebx

                    0054BDEC  xor         esi,esi

                    0054BDEE  mov         dword ptr [esp+0Ch],esi

                    0054BDF2  mov         dword ptr [esp+10h],esi

                    0054BDF6  mov         dword ptr [esp+14h],edi

                    0054BDFA  mov         dword ptr [esp+18h],esi

                    0054BDFE  call        SCREEN_PUTL+0Ch (54AD2Ch)

                    0054BE03  fldz            

                    0054BE05  fldz            

                    0054BE07  fldz            

                    0054BE09  fldz            

                    0054BE0B  fldz            

                    0054BE0D  fldz            

                    0054BE0F  fldz            

                    0054BE11  fldz            

                    0054BE13  push        eax 

                    0054BE14  fnstsw      ax  

                    0054BE16  test        ax,40h

                    0054BE1A  je          SCREEN_PUTL0_TIME+110h (54BE20h)

                    0054BE1C  xor         eax,eax

                    0054BE1E  mov         dword ptr [eax],eax

                    0054BE20  pop         eax 

                    0054BE21  fstp        st(0)

                    0054BE23  fstp        st(0)

                    0054BE25  fstp        st(0)

                    0054BE27  fstp        st(0)

                    0054BE29  fstp        st(0)

                    0054BE2B  fstp        st(0)

                    0054BE2D  fstp        st(0)

                    0054BE2F  fstp        st(0)

                          character text*(*)

                          if (screen_pos_is_not_correct()) call error(-82)

                          call screen_putl(cursor_line,cursor_icol,text)

                          end

                    C    

                    C.......................................................................C

                          SUBROUTINE SCREEN_PUTL0_TIME(TEXT)

                          USE ABD_INC;  USE HEADERS

                          character, intent(in) :: text*(*)

                          real*4    t

                          integer*4, save :: isw=0

                    c

                          t=timer_mm(5,isw)   ! Первый вызов: инициализация. Остальные: подсчет

                          isw=1

                          if (t < $Screen_counter_time) return

                            call screen_putl0(text)

                            t=timer_mm(5,0)

                    0054BE31  mov         dword ptr [.tmp..T2127__V$21da],7F4AECh

                    0054BE38  mov         ebx,dword ptr [ebp-0Ch]

                    0054BE3B  mov         esi,dword ptr [ebp-4]

                    0054BE3E  mov         edi,dword ptr [ebp-8]

                    0054BE41  mov         dword ptr [TEXT],7F4AE8h

                    0054BE48  mov         esp,ebp

                    0054BE4A  pop         ebp 

                    0054BE4B  jmp         TIMER_MM (4DE250h)

                          end

                    0054BE50  mov         ebx,dword ptr [ebp-0Ch]

                    0054BE53  mov         edi,dword ptr [ebp-8]

                    0054BE56  mov         esp,ebp

                    0054BE58  pop         ebp 

                    0054BE59  ret             

                            call screen_putl0(text)

                    0054BE5A  push        offset FIND_AND_ADD_HOTKEY_REGION$BLK_debug_param_const+2D4h (7F5CD4h)

                    0054BE5F  call        _ERROR (4D39A0h)

                    0054BE64  fldz            

                    0054BE66  fldz            

                    0054BE68  fldz            

                    0054BE6A  fldz            

                    0054BE6C  fldz            

                    0054BE6E  fldz            

                    0054BE70  fldz            

                    0054BE72  fldz            

                    0054BE74  push        eax 

                    0054BE75  fnstsw      ax  

                    0054BE77  test        ax,40h

                    0054BE7B  je          SCREEN_PUTL0_TIME+171h (54BE81h)

                    0054BE7D  xor         eax,eax

                    0054BE7F  mov         dword ptr [eax],eax

                    0054BE81  pop         eax 

                    0054BE82  fstp        st(0)

                    0054BE84  fstp        st(0)

                    0054BE86  fstp        st(0)

                    0054BE88  fstp        st(0)

                    0054BE8A  fstp        st(0)

                    0054BE8C  fstp        st(0)

                    0054BE8E  fstp        st(0)

                    0054BE90  fstp        st(0)

                    0054BE92  add         esp,4

                    0054BE95  jmp         SCREEN_PUTL0_TIME+0D0h (54BDE0h)

                    0054BE9A  nop         word ptr [eax+eax]

                    C     

                    C.......................................................................C

                          SUBROUTINE SCREEN_PUTL1 (LINE,TEXT)

                    0054BEA0  push        ebx 

                    0054BEA1  mov         ebx,esp

                    0054BEA3  and         esp,0FFFFFFF0h

                    0054BEA6  push        ebp 

                    0054BEA7  push        ebp 

                    0054BEA8  mov         ebp,dword ptr [ebx+4]

                    0054BEAB  mov         dword ptr [esp+4],ebp

                    0054BEAF  mov         ebp,esp

                    0054BEB1  sub         esp,428h

                    0054BEB7  push        eax 

                    0054BEB8  push        edi 

                    0054BEB9  push        ecx 

                    0054BEBA  mov         edi,ebp

                    0054BEBC  sub         edi,428h

                    0054BEC2  mov         ecx,10Ah

                    0054BEC7  mov         eax,0CCCCCCCCh

                    0054BECC  rep stos    dword ptr es:[edi]

                    0054BECE  pop         ecx 

                    0054BECF  pop         edi 

                    0054BED0  pop         eax 

                    0054BED1  mov         dword ptr [ebp-10h],esi

                    0054BED4  mov         esi,dword ptr [ebx+10h]

                    0054BED7  mov         dword ptr [ebp-0Ch],edi

                          character, intent(in) :: text*(*)

                          real*4    t

                          integer*4, save :: isw=0

                    c

                          t=timer_mm(5,isw)   ! Первый вызов: инициализация. Остальные: подсчет

                          isw=1

                          if (t < $Screen_counter_time) return

                            call screen_putl0(text)

                            t=timer_mm(5,0)

                          end

                    Ну а кроме того, я завел глобальную переменную (в надежде, что оптимизатор не сможет определить, используется она где-то еще, или нет), и стал скидывать значение t в нее. Чтобы оптимизатор был вынужден ее из стека достать.

                    ПОСЛЕ ЧЕГО ПАДЕНИЯ ПРЕКРАТИЛИСЬ!!!!!!

                    Программа работает, и как минимум в этой точке больше не вылетает!!!!

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

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

                    P.S.

                    2@cher-nov

                    Еще раз огромное Вам спасибо! Пойду писать панегерик в свой вопрос в Q&A для тех, кто мог с похожим багом столкнуться.


                  1. adeshere
                    17.08.2023 05:59

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

                    P.S.

                    Пока толстый сохнет, худой сдохнет. Держи порох сухим, готовь сани летом.

                    Спасибо за понимание! Могу только добавить, что обжегшись на молоке, пуганную ворону бог бережет ;-)


    1. unreal_undead2
      17.08.2023 05:59

      Чисто практически - зачем сейчас компилировать счётную фортрановскую программу для x87?


    1. netch80
      17.08.2023 05:59
      +1

      Вопрос к знатокам: подскажите, а может ли выгрузка 80-битных real в
      64-битный double и наоборот при каких-то особых условиях приводить к
      появлению Nan?

      Вполне возможно, например, в следующем варианте:

      1. В промежуточных результатах возникает число больше представимого в double (у double 11 бит на порядок числа, у long double - 15).

      2. Два числа от этого становятся Inf.

      3. Inf - Inf == NaN.

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

      (real8) R8 = (integer8) I8

      А во что это закомпилировалось на ассемблере?

      В одном месте даже DOS-версия до сих пор работает

      А расширитель типа DOS4G для 32-битного кода не годится?

      Я вот думаю, переход на SSE с вашим компилятором возможен? Вряд ли где-то есть процессоры старее ~2002 года.


      1. adeshere
        17.08.2023 05:59

          r8=DBLE(i8);  

        А во что это закомпилировалось на ассемблере?

        00444304 fild qword ptr [edi]
        00444306 mov ecx,dword ptr [edi]
        00444308 mov eax,dword ptr [edi+4]
        0044430B mov dword ptr [ebp-10h],ecx
        0044430E mov dword ptr [ebp-0Ch],eax
        00444311 fst qword ptr [ebp-88h]
        00444317 fstp qword ptr [esi]


      1. adeshere
        17.08.2023 05:59

        А расширитель типа DOS4G для 32-битного кода не годится?

        Не знаю, не пробовал. Я уже 15 лет, как с DOS-версией завязал - не использую, не обновляю и т.д. Это скорее был пример про консервативность пользователей, которые исходят из принципа "работает-не трогай". Вопрос-то у меня сейчас стоит об отказе от 32-битной версии в пользу 64-битной. Но это пока невозможно.

        Я вот думаю, переход на SSE с вашим компилятором возможен?

        Да, мне уже советовали в этом направлении поковырять. На самом деле у меня сборка кода идет в режиме SSE3 - опытным путем

        оказалось,

        у меня алгоритмы не очень стандартные, плюс я с наборами команд не на короткой ноге. Поэтому эффект от включения/выключения разных ключей я проверял "методом тыка": собирал разные варианты и просил коллег на своих компах эти exe-шники запустить. Потом сравнивал статистику... Ну и понятно, что варианты сборки, которые у кого-то не заработали, не рассматриваются (я ж не совсем программист, мне сразу несколько вариантов сборки не под силу поддерживать)...

        что скорость счета в этом случае максимальна. В результате я пришел вот к такому набору ключей компиляции (Intel Fortran):

        <Tool Name="VFFortranCompilerTool"

        MultiProcessorCompilation="true"

        GenAlternateCodePaths="codeForSSE3"

        UseProcessorExtensions="codeExclusivelySSE3"

        Parallelization="true"

        BufferedIO="true"

        EnableEnhancedInstructionSet="codeArchSSE3"

        ByteRECL="true"

        InitLocalVarToNAN="true"

        LocalSavedScalarsZero="true" />

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

        На самом деле, тут еще один нюанс есть - у меня до сих пор компилятор 2013 года. Я надеялся, что в более новом компиляторе этот баг сам собой "рассосется". Но из-за изменившихся обстоятельств пока не получается на него перейти...

        Вряд ли где-то есть процессоры старее ~2002 года.

        В некоторых местах есть. Во многих институтах техника обновляется не путем "замены", а добавлением нового компа. При этом старое не выкидывается, а продолжает использоваться еще какое-то время.


  1. Panzerschrek
    17.08.2023 05:59
    -2

    Статья раскрывает на самом деле всю суть апологетов языка Си, включая авторов компиляторов под него. Баг известен 23 года, но так и не починен. При этом починка тривиальна и всем известна - перезагружать значения из 80-битных регистров после каждой операции (-ffloat-store).
    Но поскольку это Си, все жутко помешаны на эфемерной эффективности и посему баг не чинят, т. к. починка таки замедлит код. Производительность ценят больше корректности.

    Интересно было бы узнать, как обстоит дело с вышеописанным багом в компиляторах более новых языков, вроде Rust.


    1. ksbes
      17.08.2023 05:59

      Там LLVM всё решает. Думаю, что и там баг - есть. Как минимум при определённых оптимизациях.


    1. netch80
      17.08.2023 05:59
      +1

      Производительность ценят больше корректности.

      А вы хотели бы, чтобы работа с FPU была заведомо неэффективной?


      1. Panzerschrek
        17.08.2023 05:59
        +2

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

        Впрочем, конкретно эта проблема слабо актуальна. Использовать FPU сейчас почти нету смысла, т. к. есть SSE.


        1. vanxant
          17.08.2023 05:59

          Статья как раз на эту тему: в 64 битных процессорах SSE есть всегда (как минимум первые два)


  1. netch80
    17.08.2023 05:59

    Аналогично как-то обсуждали на RSDN. В моём примере есть зависимость от выбранного в FPU режима округления, но в C# он не управляется.


  1. Dimkama
    17.08.2023 05:59

    В misra есть правило запрещающее использование == и != при сравнении даблов и флоатов.

    Там ещё очень много полезных правил, почитайте. Гуглится по "iar misra"


  1. vanxant
    17.08.2023 05:59
    -1

    В итоге это и не баг процессора, и даже не баг компилятора, а таки баг нашего вычислительного геометра, который слишком самоуверенно сравнивал double на равенство. За что и огрёб.

    Нужно было задать себе вопрос, а какой эпсилон нужен (и правильный ответ на него исправил бы баг). Ну или тупо взять и попробовать минимальный эпсилон 1>>55 (это точность double) и посмотреть, исчезнет ли баг (он бы исчез).


    1. ksbes
      17.08.2023 05:59
      +1

      Не эпсилон нужен, а нормальная логика. Ему ж не нужны на самом деле отклонения! Ему нужна оценка качества "обрезания", по которой можно сортировать.
      А для этого, например, можно было бы просто целочисленно подсчитать количество закрашенных пикселей в прямоугольнике (или просто сумму всех пикселей целочисленно делённую на площадь). И это, возможно даже, работало бы быстрее, чем расчёт нормалей с даблами.


      1. vanxant
        17.08.2023 05:59
        -1

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

        А вот правило "сравнивать флоаты только через эпсилон" можно просто зазубрить. И оно бы спасло в данном случае.


        1. DrSmile
          17.08.2023 05:59

          А вот правило "сравнивать флоаты только через эпсилон" можно просто зазубрить.

          Это вредный совет и антипаттерн, как, впрочем, и любое другое использование взятых с потолка магических чисел. Алгоритм, требующий проверки вещественных чисел на равенство, является численно неустойчивым. Вместо добавления костылей, которые одну проблему убирают и две добавляют, надо поменять алгоритм на устойчивый (либо перейти на целые числа).


  1. MANAB
    17.08.2023 05:59

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


  1. Yami-no-Ryuu
    17.08.2023 05:59

    А как себя поведёт?

    return da < db ? true : da > db ? false : a < b;

    наивный фикс


  1. ptr128
    17.08.2023 05:59

    А баг компилятора — это серьёзно: за двенадцать лет программирования на C++ я обнаружил (и написал отчёт) всего... об одном.

    Надо быть на передовой )

    По кодогенерации для AVR я в свое время почти по десятку багов прошёлся. Впрочем, при внимательном рассмотрении, я был не первым и эти баги успели отрепортить до меня.

    А вот для sdcc пару багов сам репортил. Но там все же C, а не C++


  1. Vitya_Nikolayev
    17.08.2023 05:59

    В моменте "Отключив оптимизации компилятора" подумал "А почему бы просто не подписать volatile? :hmm_deystvitelno.jpg:" и дочитав узнал, что автор пришёл к такому же решению (Не, я не говорю что я бы понял корень проблемы быстрее, отнюдь, сам наверное увидев разницу выполнения просто добавил сравнение с эпсилоном и пошёл бы дальше, не докапываясь до истины)