Это история о баге, который бы заставил вас рвать на себе волосы. Из-за такого бага вы можете подумать: «Но это невозможно, должно быть, компилятор сломался, других вариантов нет!»
А баг компилятора — это серьёзно: за двенадцать лет программирования на 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 не работают», то я должен высказаться прямо:
Проверки равенства с double допустимы, это не табу и не Волан-де-морт, у вас есть право писать их, мир не рухнет, если вы сделаете это, вам разрешается это делать. Конечно, если вы знаете, что делаете.
Проверка равенства double вызывает проблемы, только если вы думаете, что проверяете равенство между вещественными числами (в математическом смысле). Да, при работе с double 1.0/2.0 необязательно равно 0.5, потому что конечная точность означает, что другое описание одного вещественного числа может дать немного отличающиеся значения. С другой стороны (и я настаиваю на этом): 1.0/2.0 гарантированно равно 1.0/2.0; 0.5 гарантированно равно 0.5; да, ДАЖЕ в случае double. Я имею в виду, что если у вас есть два double с одинаковым значением (в компьютерном смысле, то есть с одинаковыми битами в одинаковых байтах), вычисленные абсолютно одинаковым образом, то проверка равенства между этими двумя double вернёт TRUE. И спасибо Вселенной за это!
В моём случае всё было даже проще: нас не интересует случай равенства двух double, это просто особый случай, который мы подготовили, чтобы обезопасить себя, потому что если два double различаются, то всё ещё проще. Представим, что вы правы и что «два double никогда не равны» (что, разумеется, совершенно неверно): в таком случае проблем нет, потому что
if
никогда не будет true, и алгоритм всегда будет использовать в качестве приоритета отклонение. На практике, поскольку полилинии составляются из обычных пикселей, разумеется, существует множество равных отклонений...Использовать допуск? Простите, вы или не прочитали код, или совсем его не поняли. Что произойдёт, если вместо сравнения
da
иdb
я выполню doif(std::fabs(da-db)<epsilon)
? Если две вершины имеют близкие отклонения, то вместо сортировки их по отклонению мы отсортируем их по индексам. ОТЛИЧНО. В чём смысл, кроме как в снижении оптимальности алгоритма?
Всегда стоит быть аккуратными со сравнениями double, но лучше понимать, почему конкретно и в каких случаях. А ещё было бы здорово, если бы вы сначала поняли код, прежде чем писать комментарии по темам, которые в буквальном смысле изучают в первый год курса программирования.
Отступление завершено.
Сдаюсь
Я потратил на работу с этим багом целый день. Я торопился закончить игру, был в стрессе, но как бы ни исследовал проблему, я просто не мог в ней разобраться. Не говоря уже о том, чтобы устранить её. Ну, на самом деле, я мог. Я мог устранить её множеством способов, которые не имели никакого смысла:
Отключив оптимизации компилятора
Добавив к сравнениям
std::cerr
(баг Шрёдингера: он исчезает, когда ты за ним наблюдаешь)Предварительно сохраняя значения отклонений, а не вычисляя их на ходу (нет, они не меняются, я проверял)
Целой кучей других странных действий (например, объявлением ещё одной переменной посередине сравнения).
Если вкратце: 64 бит? Без проблем. 32 бит без оптимизаций? Без проблем. 32 бит с оптимизациями? Бум, ошибка.
На этом этапе я видел в отладчике буквально следующее:
Программа определяет равенство между double как false (а это ошибка)
Не входит в
if()
Затем определяет неравенство между double как false (что верно, но противоречит пункту 1.)
Всё это пахнет багом компилятора. Только, как я сказал, ошибка возникала и в GCC, и в Clang. Два разных компилятора с одним багом? Маловероятно.
Я потерял день, но не добился никакого прогресса. Более того, баг возникал только в development-версии игры (и в 32-битной версии, которую я вообще не собирался распространять): в продаваемой версии этот граф пола вычисляется предварительно. То есть в целом этот баг ни на что не повлияет, но меня ужасно раздражало, что я его не понимаю.
В конечном итоге я сдался: я потратил на это слишком много времени, а мне нужно было закончить игру. Было решено, что я вернусь к багу после релиза.
Возвращаемся к багу
Я уложился в дедлайн и выпустил игру 15 мая.
Перенесёмся во вчерашний день: в середине августа наступило затишье, поэтому я решил со свежей головой вернуться к этому багу.
Большая проблема этого бага заключалась в том, что он находился посередине очень большой программы (игрового движка) со множеством сложных файлов и зависимостей. Поэтому в этом случае нужно было максимально изолировать часть с багом, чтобы убедиться, что он не возникает из-за чего-то более запутанного (например, утечка памяти в другой части игры могла бы приводить к записи в очередь с приоритетами мусорных данных).
Я работал над ним так:
Вытащил вычисление графа из игрового движка: мы загружаем изображение и выполняем только вычисление графа, устранив бесполезные зависимости (LZ4, YAML)
Максимально уменьшил изображение до маленького графа
В конечном итоге я просто прописал граф в коде (мне удалось воспроизвести баг всего с 16 вершинами вместо 3900), поэтому загрузка изображения больше не требовалась и я удалил зависимость от SDL
Я избавился от своей геометрической мини-библиотеки, оставив две или три полезные функции из неё.
Максимально уменьшил очередь с приоритетами, ошибка начала возникать после вставки второй вершины.
В конце у меня получилась вызывающая баг программа из пятидесяти строк без зависимостей, за исключением 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)
webhamster
17.08.2023 05:59-7bool operator() (const GVertex& a, const GVertex& b)
Что же имел в виду тут автор? Надо бы разобраться почему парсер съедает символы, придумать work around и показать читателю правильный код. А то ценность статьи с такими ошибками нуливая, только лишний раз убеждать начинающих программистов что синтаксис C++ непостижим.
KanuTaH
17.08.2023 05:59+3А что тут не так?
webhamster
17.08.2023 05:59Извиняюсь, давно на плюсах не писал. Подумал что код для операции сравнения, и он должен выглядеть как operator()<, а для этого на самом деле надо использовать operator<.
Первый раз вижу, чтобы оператор () возвращал bool при сравнении своих аргументов.
KanuTaH
17.08.2023 05:59+3Первый раз вижу, чтобы оператор () возвращал bool при сравнении своих аргументов.
operator()
изstd::less
, который используется как компаратор по умолчанию вstd::set
, делает именно это.Подумал что код для операции сравнения, и он должен выглядеть как operator()<, а для этого на самом деле надо использовать operator<.
Смысл существования function object наподобие
std::less
или этогоComparator
у автора в том, что вы можете не захотеть в данном конкретном случае использовать именно сравнение<
. А переопределивoperator<()
для данного типа, и заменив его логику работы, вы вынудите всю программу использовать эту новую логику вместо того, чтобы использовать ее только лишь в этом месте, а это скорее всего не то что вам нужно.webhamster
17.08.2023 05:59Я лично вижу тут "вывернутую" логику. Для абстракции "точка", по сути, не сделан отдельный класс, вместо этого точка рассматривается как просто два double значения. Соответственно нет и абстракции "полилиния", которая бы состояла из "точек", и у которой был бы метод расчета отклонений точек, вместо этого голый библиотечный set. А у "точки" нет метода расчета расстояния до другой точки. Ну чтобы все было по логике вещей. Зато есть класс компаратор, который сравнивает два каких-то значения, ему все равно каких, он работает с double а не точками. Другими словами, структуры предметной области в этом коде не видно.
Я надеюсь, что отсутствие этих базовых вещей произошло из-за максимального упрощения примера, и в реальном коде у автора все что нужно есть. Но если и реальный код написан в таком же стиле, то такое построение и взаимосвязь элементов для меня выглядит странно, как бы удобно не было делать множество с заданным компаратором.
0xd34df00d
17.08.2023 05:59Потому что точки можно сравнивать кучей разных способов — лексикографически, по положению в массиве, по расстоянию до прямой, и так далее. Выбор конкретного способа не является свойством точки, а попытки привязать к ней один из вариантов поведения — очередное ООП головного мозга.
vesper-bot
17.08.2023 05:59+4Вот именно из-за этого я в своё время все вычисления и хранение вел в extended. Так что нет, это таки баг компилятора, который не учитывает, что процессоры в режиме FPU386 используют 80-битные значения, и должен либо оба значения для сравнения держать на регистрах FPU, или ни одного.
sergio_nsk
17.08.2023 05:59+1Легко сказать, но сложно исправить. На всё регистров не хватит, ЦПУ самостоятельно со своим переупорядочиванием инструкций может сбросить одно из финальных значений из регистра FPU в кэш, даже если компилятор рассчитывал хранить значения в FPU до самого сравнения. Так что единственное решение - ничего из законченного не хранить в FPU, и оно есть
-ffloat-store
.Не понятно, почему такой проблемы нет в 64-битном бинарнике. ЦПУ тот же, FPU тот же,
sizeof(double)
тот же, а ошибки нет.Melirius
17.08.2023 05:59+5В 64-битном режиме без специального флага 80-битный легаси FPU просто не используется, емнип.
arteast
17.08.2023 05:59+7Именно. Все 64-битные процессоры имеют как минимум поддержку SSE2, и для FP вычислений используются "нормальные" регистры FP и нормальные команды SSE, а не стековая машина и сопроцессор x87.
В связи с этим мне непонятно, почему автор посчитал нормальным решением покалечить собственный код volatile-ами (и надеяться, что это единственное такое место и что он или кто-то другой потом вспомнит об этой баге, когда будет делать еще какое-нибудь такое сравнение), вместо того, чтобы просто добавить один флажок компилятору и компилировать 32-битную версию с использованием SSE вместо x87.
voldemar_d
17.08.2023 05:59+1компилировать 32-битную версию с использованием SSE вместо x87
Я понимаю, что нынче все процессоры 64-битные, но если допустить, что 32-битный код будет исполняться на каком-нибудь процессоре, где нет SSE?
arteast
17.08.2023 05:59+3В 2023 году жить с процессором без поддержки SSE2 довольно грустно (это значит Vista или XP, старинные браузеры, и тд и тп). Но я могу себе представить кого-то, кто сидит на Athlon XP 20-летней давности.
В 2023 жить с процессором без поддержки SSE - это мазохизм (для понимания - это Pentium II или более старые процессоры). Я думаю, что разработчик игрушки в 2023 может спокойно позволить себе пренебречь этим сегментом рынка.
voldemar_d
17.08.2023 05:59+1Я-то как раз это всё понимаю. Но бывает, что 32-битный код продолжает использоваться у некоторого ненулевого количества клиентов, у которых он уже 20 лет назад работал и работает сейчас (в том числе на XP и Athlon 20-летней давности). И есть ненулевая вероятность, что придется подправить какой-то древний баг, обновить у клиента код и заодно вдруг выдать кусок кода, который на его древнем процессоре не выполнится.
cher-nov
17.08.2023 05:59В 2023 жить с процессором без поддержки SSE - это мазохизм (для понимания - это Pentium II или более старые процессоры). Я думаю, что разработчик игрушки в 2023 может спокойно позволить себе пренебречь этим сегментом рынка.
Проблема в том, что в SSE (первом) нет регистров двойной точности - только одинарной. А у автора в статье используется именно
double
.netch80
17.08.2023 05:59Так и SSE2 уже лет 20 существует. Это надо ещё постараться найти процессор, где его нет.
(Да, я вижу комментарий выше про Athlon XP 20-летней давности. Но не верю. Ну реально, такое железо должно было сдохнуть по 100500 другим причинам.)
DungeonLords
17.08.2023 05:59+1Продажа процессоров AMD Geode прекратилась производителем только в 2019, а производители компьютеров используют их до сих пор
@voldemar_d@arteast@netch80netch80
17.08.2023 05:59Atom/Geode/... я и не предполагал сюда включать, там многое могло быть урезано.
Я думал написать исключение в скобках, но обломился.
sergio_nsk
17.08.2023 05:59Автор ведь чётко написал, что нет планов выпускать 32-битную версию. Так что "где нет SSE" не вариант.
voldemar_d
17.08.2023 05:59+5Если так рассуждать, автор мог вообще ничего не публиковать, раз у него в релизе проблем нет. А так он делится опытом, который может пригодиться другим. Хабр же для этого и создан, имхо.
stuq1
17.08.2023 05:59Если мне не изменяет память, то в случае с новыми версиями Visual Studio даже не придется ставить этот флаг, потому что там по дефолту SSE используется и для сборки под 32 бита
Melirius
17.08.2023 05:59+1В 64-битном режиме без специального флага 80-битный легаси FPU просто не используется, емнип.
vesper-bot
17.08.2023 05:59Напрочь отсутствует sizeof(extended) и все вычисления на FPU ведутся именно в double. То есть 80-битных чисел во время выполнения 64-битной программы нет нигде. А вот во время выполнения 32-битной — есть, в том, что на ассемблере видится как регистры сопроцессора, со всякими там fstp, fld и остальными опкодами для управления. Для совместимости с 80287, по-видимому.
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.
Tiriet
17.08.2023 05:59не согласен с Вами. КМК это как раз иллюстрация особенностей работы с fp-числами и иллюстрация встроенной в них by design погрешности представления, которая может резко возрастать при некоторых операциях с малыми числами. Эта ошибка пьет кровь на вычислительной геометрии литрами- пересечение отрезков, параллельность прямых, сечения полигонов- она везде вылезает, когда надо сравнивать положения точек в пространстве- особенно точек пересечения и "ближе-дальше", и везде ее приходится аккуратно изолировать и обходить хитрыми способами.
Melirius
17.08.2023 05:59+2Что за фигня с комментариями? Отвечаю в ветку, мне показывает, что ответило в корень, а потом ещё дублирует сообщение.
Tiriet
17.08.2023 05:59+2похожая беда. после обновления страницы комменты вроде встали на место.
voldemar_d
17.08.2023 05:59Мне уже написали в другой ветке, что это баг хабра. Камент как будто попадает не туда, а после обновления страницы оказывается там, где надо.
Tiriet
17.08.2023 05:59+2:-) тоже, наверное, какая-нибудь особенность работы XMM-регистров на новых процессорах так проявляется.
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, соответственно. Есть глобальные аналоги в виде опций компилятора, однако, должна быть возможность задавать их локально для конкретных функций или даже небольших блоков кода.
ksbes
17.08.2023 05:59+4Своеобразная проф-деформация. Когда столько много работаешь с серьёзной математикой, то знаешь как можно оптимально работать с плавающей точкой, знаешь шаблоны вычислений геометрии и т.д. И потому просто не приходит в голову думать над различными чисто игровыми оптимизациями вроде "квадратного корня Кармака" и разной целочисленной "чёрной магией" - потраченное время не то что бы себя окупит.
Но да - появляются подобные риски.А насчёт детерминистичности плавающей запятой - вот положа руку на сердце: на хрена? Вот ни разу за долгие годы работы над статистикой, физическими симуляциями и обработки изображений не сталкивался с необходимостью иметь именно детерминистичность. Понятно, что стандарт требует и понятно почему, но просто 99,900000000001% этого не оценят!
MANAB
17.08.2023 05:590.099999999999% от 1.000.001 программистов это 1000 оценивших настоящих человек.
voldemar_d
17.08.2023 05:59+3в общем случае не является верным
Если два значения double вычислены в точности одинаковым образом (имеется ввиду, в машинном коде процессора), и они побитно равны, с чего бы им различаться?
ksbes
17.08.2023 05:59Ну хотя бы потому что они могут по-разному потом хранится . Что и произошло у автора. Не говоря уже о том, что даблов бывет много, даже в рамках одного процессора.
У меня тоже похожее было - но там шла перекодировка видео: где 12 бит, где 16 на плисине ... а джун решил отслеживать кадры по таймстемпу во флоатовских секундах ..
unreal_undead2
17.08.2023 05:59+8Если вычислены одними и теми же ассемблерными инструкциями с одинаковыми входными аргументами и тем же значением регистра состояния - будут равны. Но один и тот же сишный код вполне может скомпилироваться по разному в разных местах.
voldemar_d
17.08.2023 05:59Я и имел ввиду вычисление одинаковым ассемблерным кодом в одинаковых ситуациях. Статья как раз про то, что сишный код может скопмилироваться по-разному.
voldemar_d
17.08.2023 05:59+1Для минусующих: это было не утверждение, а вопрос. Заданный как раз потому, что хочется разобраться. За задавание вопросов принято тоже минусы ставить?
DrSmile
17.08.2023 05:59+1Даже в этом случае бывают приколы (например, вычисленное на одном процессоре отличается от вычисленного на другом), но я говорю про случай, когда одинаковый исходный код превращается в разный машинный (случай автора именно такой).
Tiriet
17.08.2023 05:59+4вот тут у него кстати, прикол! 1.0/2.0 гарантировано равно 1.0/2.0, а вот 1.0/5.0 уже не гарантировано равно 1.0/5.0! потому как первое- точно представимо конечной дробью в двоичной системе, а второе- периодической! и потому будет округлено в процессоре и черт его знает, что там в младших битах окажется.
slonopotamus
17.08.2023 05:59Окажется одно и то же.
Tiriet
17.08.2023 05:591/2 в single, float & extended можно сравнивать друг с другом и в любых преобразованиях- и будет равенство. 1/5- во всех трех форматах разная и при сравнении можно получать разные результаты.
slonopotamus
17.08.2023 05:59+1Я не понимаю что такое "1.0/5.0 в float". В float будет "1.0f/5.0f". И понятно что это уже другое.
vanxant
17.08.2023 05:59Не, товарисч намекает на то, что 1/2 в двоичной записи (мантисса) это 0.100000(0)b. А 1/5 это 0.0011(0011)b - числа в скобках бесконечно повторяются и становится важно, где именно дробь будет обрезана
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)
ksbes
17.08.2023 05:59+1Вот тут вы не правы - это не зависит от точности представления. В хорошей вычислительной машине при подаче идентичных данных на вход должны получать идентичный выход.
Вот только компьютер - это далеко не одна вычислительная машина!
Tiriet
17.08.2023 05:59так в том-то и дело, что то, что я воспринимаю как "одинаковые данные"- машина не воспринимает как одинаковые данные- она их вынуждена на лету перекодировать из машинного представления в человеко-читаемое и обратно. и сейчас речь не про "идентичный выход"- это любой детерминированный алгоритм должен давать, а не только "хорошая машина". Сейчас речь про то, что этот идентичный выход может не соответствовать Вашим ожиданиям! он будет идентичный, как у автора- каждая программа при подаче на вход одинаковых данных выдавала стабильно повторяющийся результат. Только результата одной программы стабильно соответствовал ожиданиям автора, а результат второй- стабильно нет.
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, устраняешь проблему в корне.)
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.
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.
В общем, проблема есть, да. Но сейчас её легко контролировать.
unreal_undead2
17.08.2023 05:59+2Очевидно, мы должны иметь возможность добавлять в очередь с приоритетами две вершины с одним отклонением.
И почему тогда не используется std::multiset?
voldemar_d
17.08.2023 05:59-1Наверное, для экономии памяти, если нет необходимости хранить более одного значения на элемент?
unreal_undead2
17.08.2023 05:59+1Вроде как танцы с бубном в функции сравнения как раз для того, чтобы заставить set хранить несколько одинаковых значений.
voldemar_d
17.08.2023 05:59Если так, то да, непонятно, зачем это. Но и при запихивании в std::multiset случится аналогичная проблема на 32-битном коде, как я понимаю?
unreal_undead2
17.08.2023 05:59Проблема из-за того, что в коде два сравнения da и db (на меньше и точное равенство), которые могут вести себя неконсистентно. С одним сравнением проблем не будет.
Правда, задача всё таки хитрее - насколько понял после второго прочтения, полностью совпадающие вершины надо всё-таки объединить, но оставить разные вершины с одинаковым deviation. Так что просто так использовать multiset тоже нельзя...
Ryppka
17.08.2023 05:59Обычное дело в низкоуровневом коде, напрямую с железом и не такие чудеса случаются(
Leetc0deMonkey
17.08.2023 05:59А нельзя сравнивать
GVertex
напрямую? Не знаю что там внутрях, но подозреваю что 3 координаты и вdeviation
производится проекция в 2 плюс ещё что-то.
Ryppka
17.08.2023 05:59Если более серьезно.
Сравнение значений с плавающей точкой на равенство -- это побитовое сравнение, практически memcmp, т.е. реально низкоуровневая операция, происходящая либо на абстрактной машине, либо уровнем ниже на конкретном железе. Оптимизация тут может быть неуместной.
Если компилятор генерит код, сравнивающий объекты с разным двоичным представлением, то с моей точки зрения это все-таки баг компилятора. Я не знакток x87, в нем сравнения только регистр-регистр или есть и регистр-память?
Использование volatile для точечного подавления нежелательной оптимизации выглядит как элегантный трюк.
ultrinfaern
17.08.2023 05:59+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 константы, сразу вычисленной до того же знака - запросто.alcotel
17.08.2023 05:59-1С авторством бага согласен. Компилятору же сказали: "скомпилируй для х86 и сравни это, как даблы", или хоть даже как fp32, и было бы всё детерминировано. "А он что? Пошёл, и кеды купил" (c) Захотелось ему аппаратно сравнить как fp80. Получается, можно ведь и на такую штуку нарваться:
// Написал в одном месте
если a > b, то делаем РАЗ
// потом ещё какой-то код
// а потом
если a <= b, то делаем ДВА
И вроде автор такого кода имел в виду, что всегда выполнится либо РАЗ, либо ДВА. И абсолютно чётко это сказал, и не пользовался "не принятыми в приличном обществе паттернами работы с fp". Но в зависимости от оптимизации и окружающего кода автор нарвётся на рандомное выполнение РАЗ и ДВА, или ни РАЗ и ни ДВА.
titbit
17.08.2023 05:59+4на самом деле это не баг компилятора: это баг процессора. Да, всё верно.
Нет, здесь и близко нет никакого бага в процесоре. Почитайте errata на процессоры для понимания какие бывают баги именно в процессорах.
Если точнее, то это поведение FPU (Floating-Point Unit), вызывающее неожиданное поведение при оптимизации кода.
FPU ничего не знает ни про компилятор, ни про его оптимизации. Просто надо использовать FPU правильно, или тогда уж не использовать вообще если нет уверенности.
А вишенкой на торте стало простое и оптимальное исправление (использование
volatile
).Мда, у нас за исправления багов с помощью расстановки volatile - больно бьют канделябром на ревью. Не надо так "исправлять" баги.
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-версия до сих пор работает, причем это не какой-то промышленный модуль, который крутится в фоне, а реальные люди в ней каждый день интерактивно обрабатывают данные....
cher-nov
17.08.2023 05:59+1Попробуйте записывать в лог не только числа, которые выдаёт генератор, но и контекст x87 FPU. Для его получения используйте команду
FNSAVE
и затемFRSTOR
из сохранённого, потому чтоFNSAVE
переинициализирует сопроцессор. Причём делайте это как до вызова функцииRANDOM()
, так и после. Это как минимум даст больше информации о происходящем.adeshere
17.08.2023 05:59...команду
FNSAVE
и затемFRSTOR
К сожалению, я не умею делать asm-вставки в фортрановский код. Наверно, это можно как-то реализовать под отладчиком, но при запуске из-под отладчика баг исчезает вне зависимости от каких-либо других обстоятельств
cher-nov
17.08.2023 05:59+1Скверно. Полистал интернеты - пишут, мол, в Intel Fortran возможности делать ассемблерные вставки вообще нет. Однако я покумекал также над справкой и нашёл там такое:
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.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.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
17.08.2023 05:59+1После вызова какой именно функции программа вываливается по Access Violation?
Именно в этом модуле у меня вообще нет никаких FP-операций. Ни одной! Есть арифметика в Integer*8 и форматная запись целого числа в строку. И все. А Access Violation происходит на операторе call, причем вызывается там не функция, а подпрограмма с единственным целым параметром.
Погодите, но ведь Вы же пишете сами чуть выше:
Точка вылета по Access Violation оказалась совсем не там, где у меня появлялся Nan, а как раз в том модуле, который обращается к системному таймеру. (Для меня с самого начала было большой загадкой, почему Nan-ы исчезают, если к таймеру не обращаться - но теперь факт связи доказан).
Если стандартная библиотека Intel Fortran высчитывает текущую дату/время из значения системного таймера, то вычисления с плавающей точкой могут иметь место именно в них - хотя бы для обычного деления. Но это лишь моё предположение, а не утверждение.
Откуда вытекает вопрос: а может ли эта функция (с некорректным прототипом) оказаться библиотечной, а не моей? И если да, то что посоветуете?
Всякое бывает, но я бы на это не рассчитывал. Кривые прототипы, как правило, проявляются очень быстро.
Я пока вообще не понимаю, можно ли как-то к библиотечной (встроенной в язык) функции прототип написать...
А зачем?
Лазить в справку по ссылкам наподобие приведенных у меня в последнее время не получается, так что буду ориентироваться на те цитаты, которые Вы привели
Для последних времён хороший прокси-сервер или VPN уже попросту необходим.
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_percentinteger8 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],edic
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]Что-то не могу разобраться, как вставить сюда код. Тащу его из редактора... но при вставке длинные строки режутся на куски. А после пометки фрагмента, как "код" некоторые короткие склеиваются. Вставил фрагментами. Сомневаюсь, что при таком форматировании можно что-то понять, но пусть останется.
adeshere
17.08.2023 05:59Если стандартная библиотека Intel Fortran высчитывает текущую дату/время из значения системного таймера, то вычисления с плавающей точкой могут иметь место именно в них - хотя бы для обычного деления. Но это лишь моё предположение, а не утверждение.
Очень похоже, что все именно так.
Я вчера полез в код, и понял, что все немного сложнее.
Изначально я получал системное время вызовом встроенной функции DATE_AND_TIME. Она возвращает набор целых значений, и в моем коде в этом месте никаких FP не было.
Но после того, как обнаружилась связь таймера и Nan-ов, я стал искать в библиотеке фортрана другие методы обращения к времени - их там целя куча. Перепробовал разные варианты, но ничего не меняется: Nan по-прежнему изредка появляется.
На данный момент у меня в рабочем варианте прицеплена функция RTC(), которая возвращает число секунд от базовой даты в виде real*8. Так что про FP-вычисления я соврал: они в моем интерфейсе к таймеру все-таки есть. Но проблема явно не в них. Баг впервые обнаружился, когда их еще не было. Судя по всему, где-то в глубине такие вычисления происходят вне зависимости от того, как именно я вызываю таймер.
И еще вопрос про FPU-стек: подтверждает ли эту версию тот факт, что для появления Nan требуется обратиться к таймеру несколько раз? Падает не с первого раза. Но тут счет идет не на миллионы, а на единицы вызовов. (Изначально я писал про миллионы операций с Real*8, но к таймеру-то я обращаюсь гораздо реже
adeshere
17.08.2023 05:59>> Я пока вообще не понимаю, можно ли как-то к библиотечной (встроенной в язык) функции прототип написать...
А зачем?
Если дело вдруг все-таки в библиотечной функции а не в моей. Что там какой-то редкий полу-глюк, проявляющийся только в сочетании с моими (не совсем безопасными) ключами компиляции способам и ее вызова. Хотя это очень маловероятно, конечно. Да и к тому же все функции работы с таймером, которые я могу вызывать, уже описаны в файле ifport.f90 (я там тоже копал и ничего подозрительного не нашел).
Но свои интерфейсы я тоже уже не один раз проверял, и пока багов в не обнаружил. Я ведь интерфейсы пишу строго копипастом. А проверить интерфейс при любом редактировании функции - это вообще железное правило. У меня ведь проект небольшой, все лежит на одном компьютере. Поэтому абсолютно любой рефакторинг начинается и кончается с глобального поиска по всем файлам проекта. Разумеется, гарантировать полное отсутствие ошибок нельзя никогда, но вероятность их наличия именно в интерфейсах довольно низкая, как мне кажется.
В общем, факт наличия бага налицо, теперь благодаря Вам даже почти понятна его природа. Непонятно только, что делать. Я уже сейчас пытаюсь менять одинарную точность на двойную и наоборот там, где это не имеет никакого значения с точки зрения алгоритма. Просто по принципу - "а ну как если вдруг?!"
> ...прокси-сервер или VPN уже попросту необходим.
Понимаю, но с наскока разобраться не получилось, пришлось
пока отложить :-(
В смутной надежде, что "новая реальность" не навсегда...
cher-nov
17.08.2023 05:59+1Я здесь тогда сразу на все три комментария отвечу, чтобы их последовательность не сбивать.
Код на фортране (падает call screen_putl0_time())
Дизассемблер (но тут я уже ничего понять не могу; даже ret не нашел)
Ага, ну вот смотрите. Видите в дизассемблированном коде команды
fldz
,fnstsw
иfstp
? Вот это именно то, что вставляет компилятор с командой/Qfp-stack-check
.Что здесь происходит:
Стек сопроцессора (8 значений) забивается нулями.
После этого в x87 FPU status word проверяется флаг "stack fault" (7ой бит,
40h
).Если он взведён, то происходит обращение по нулевому адресу, приводящее к Access Violation.
В противном же случае стек сопроцессора очищается.
У Вас это происходит перед вызовами следующих функций:
_for_write_int_fmt
SCREEN_PUTL0
SCREEN_PUTL0_TIME
По Вашим словам, падает на третьей функции. Значит, ищите ошибку где-то в ней. Будет хорошо, если Вы приведёте здесь её прототип, исходный код и дизассемблированный вид.
Если дело вдруг все-таки в библиотечной функции а не в моей. Что там какой-то редкий полу-глюк, проявляющийся только в сочетании с моими (не совсем безопасными) ключами компиляции способам и ее вызова. Хотя это очень маловероятно, конечно. Да и к тому же все функции работы с таймером, которые я могу вызывать, уже описаны в файле ifport.f90 (я там тоже копал и ничего подозрительного не нашел).
А какая у Вас версия Intel Fortran? И пробовали ли Вы более новые - вдруг в Вашей действительно есть какой-нибудь баг кодогенератора, вроде этого?
upd: А, увидел ответ внизу. Ну, тут либо шашечки, либо ехать. На Вашем месте я бы попробовал достать новую версию, скажем так, из неофициальных источников, и на ней просто попробовать один раз собрать и проверить. Иначе так можно и до скончания веков отлаживать, если не исключать подобные варианты, находящиеся вне зоны нашей ответственности.
Непонятно только, что делать. Я уже сейчас пытаюсь менять одинарную точность на двойную и наоборот там, где это не имеет никакого значения с точки зрения алгоритма. Просто по принципу - "а ну как если вдруг?!"
Это дурной принцип. Если мы отлаживаем некий код, то давайте его всё же зафиксируем, чтобы не сбивать ни себя, ни других. А то так можно и ещё где-нибудь самому себе свинью подложить.
В смутной надежде, что "новая реальность" не навсегда...
Ну, под луной ничто не вечно, но и мы в том числе. Пока толстый сохнет, худой сдохнет. Держи порох сухим, готовь сани летом.
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.
Еще раз огромное Вам спасибо! Пойду писать панегерик в свой вопрос в Q&A для тех, кто мог с похожим багом столкнуться.
adeshere
17.08.2023 05:59Для полноты вопроса приложу еще сюда ссылку на первоначальное описание проблемы, а также на мой отчет по ее исправлению. Вдруг кому пригодится...
P.S.
Пока толстый сохнет, худой сдохнет. Держи порох сухим, готовь сани летом.
Спасибо за понимание! Могу только добавить, что обжегшись на молоке, пуганную ворону бог бережет ;-)
unreal_undead2
17.08.2023 05:59Чисто практически - зачем сейчас компилировать счётную фортрановскую программу для x87?
netch80
17.08.2023 05:59+1Вопрос к знатокам: подскажите, а может ли выгрузка 80-битных real в
64-битный double и наоборот при каких-то особых условиях приводить к
появлению Nan?Вполне возможно, например, в следующем варианте:
В промежуточных результатах возникает число больше представимого в double (у double 11 бит на порядок числа, у long double - 15).
Два числа от этого становятся Inf.
Inf - Inf == NaN.
Правда, при этом на один NaN, по вероятности, должно быть несколько Inf, и это вы бы заметили. Ну это чисто пример навскидку, который явно не соответствует остальному в вашем комментарии. Настройками FPU это не лечится, они влияют на мантиссу, но не на порядок числа.
(real8) R8 = (integer8) I8
А во что это закомпилировалось на ассемблере?
В одном месте даже DOS-версия до сих пор работает
А расширитель типа DOS4G для 32-битного кода не годится?
Я вот думаю, переход на SSE с вашим компилятором возможен? Вряд ли где-то есть процессоры старее ~2002 года.
adeshere
17.08.2023 05:59r8=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]
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 года.
В некоторых местах есть. Во многих институтах техника обновляется не путем "замены", а добавлением нового компа. При этом старое не выкидывается, а продолжает использоваться еще какое-то время.
Panzerschrek
17.08.2023 05:59-2Статья раскрывает на самом деле всю суть апологетов языка Си, включая авторов компиляторов под него. Баг известен 23 года, но так и не починен. При этом починка тривиальна и всем известна - перезагружать значения из 80-битных регистров после каждой операции (
-ffloat-store
).
Но поскольку это Си, все жутко помешаны на эфемерной эффективности и посему баг не чинят, т. к. починка таки замедлит код. Производительность ценят больше корректности.
Интересно было бы узнать, как обстоит дело с вышеописанным багом в компиляторах более новых языков, вроде Rust.ksbes
17.08.2023 05:59Там LLVM всё решает. Думаю, что и там баг - есть. Как минимум при определённых оптимизациях.
netch80
17.08.2023 05:59+1Производительность ценят больше корректности.
А вы хотели бы, чтобы работа с FPU была заведомо неэффективной?
Panzerschrek
17.08.2023 05:59+2Я бы хотел корректности по умолчанию. Кому нужна скорость - заиспользует специальный флаг компилятора, чтобы было быстрее, но некорректно.
Впрочем, конкретно эта проблема слабо актуальна. Использовать FPU сейчас почти нету смысла, т. к. есть SSE.vanxant
17.08.2023 05:59Статья как раз на эту тему: в 64 битных процессорах SSE есть всегда (как минимум первые два)
Dimkama
17.08.2023 05:59В misra есть правило запрещающее использование == и != при сравнении даблов и флоатов.
Там ещё очень много полезных правил, почитайте. Гуглится по "iar misra"
vanxant
17.08.2023 05:59-1В итоге это и не баг процессора, и даже не баг компилятора, а таки баг нашего вычислительного геометра, который слишком самоуверенно сравнивал double на равенство. За что и огрёб.
Нужно было задать себе вопрос, а какой эпсилон нужен (и правильный ответ на него исправил бы баг). Ну или тупо взять и попробовать минимальный эпсилон 1>>55 (это точность double) и посмотреть, исчезнет ли баг (он бы исчез).
ksbes
17.08.2023 05:59+1Не эпсилон нужен, а нормальная логика. Ему ж не нужны на самом деле отклонения! Ему нужна оценка качества "обрезания", по которой можно сортировать.
А для этого, например, можно было бы просто целочисленно подсчитать количество закрашенных пикселей в прямоугольнике (или просто сумму всех пикселей целочисленно делённую на площадь). И это, возможно даже, работало бы быстрее, чем расчёт нормалей с даблами.vanxant
17.08.2023 05:59-1Ну, подобную оценку с полпинка не напишешь, тут думать надо, и она будет "хрупкой". Думаю, для игры это не самая важная проблема, чтобы тратить на неё время.
А вот правило "сравнивать флоаты только через эпсилон" можно просто зазубрить. И оно бы спасло в данном случае.
DrSmile
17.08.2023 05:59А вот правило "сравнивать флоаты только через эпсилон" можно просто зазубрить.
Это вредный совет и антипаттерн, как, впрочем, и любое другое использование взятых с потолка магических чисел. Алгоритм, требующий проверки вещественных чисел на равенство, является численно неустойчивым. Вместо добавления костылей, которые одну проблему убирают и две добавляют, надо поменять алгоритм на устойчивый (либо перейти на целые числа).
MANAB
17.08.2023 05:59Помню несколько раз в разных языках случалось такое, что очевидная вещь,которая должна работать - не работает. Вставляешь объявление левой переменной и вуаля. Так вот это что было...
Yami-no-Ryuu
17.08.2023 05:59А как себя поведёт?
return da < db ? true : da > db ? false : a < b;
наивный фикс
ptr128
17.08.2023 05:59А баг компилятора — это серьёзно: за двенадцать лет программирования на C++ я обнаружил (и написал отчёт) всего... об одном.
Надо быть на передовой )
По кодогенерации для AVR я в свое время почти по десятку багов прошёлся. Впрочем, при внимательном рассмотрении, я был не первым и эти баги успели отрепортить до меня.
А вот для sdcc пару багов сам репортил. Но там все же C, а не C++
Vitya_Nikolayev
17.08.2023 05:59В моменте "Отключив оптимизации компилятора" подумал "А почему бы просто не подписать volatile? :hmm_deystvitelno.jpg:" и дочитав узнал, что автор пришёл к такому же решению (Не, я не говорю что я бы понял корень проблемы быстрее, отнюдь, сам наверное увидев разницу выполнения просто добавил сравнение с эпсилоном и пошёл бы дальше, не докапываясь до истины)
kipar
я правильно понимаю что если бы он сделал как ему сразу написали, т.е. сравнивал double не на равенство а с допуском, то баг бы ушёл?
ultrinfaern
Да.
Я бы еще понял сравнене double, если бы это были предвычисленные значения в массиве, которые сравниваются друг с другом. Но нет, тут на лету сравниваются значения вычислений.
Leetc0deMonkey
А какая разница?
Tiriet
собственно- вся статья как раз о том, какая разница между предвычисленными и налету- в том, что именно сравнивает процессор в своих регистрах- fp64 или fp80 и в какой момент происходит переход от одних чисел к другим.
avost
Вряд ли кто-то обязан знать все особенности работы оптимизатора в сочетании с определёнными моделями fpu в неактуальном режиме работы процессора. Тем более, как автор пишет, что это поведение устранили в новых процессорах на аппаратном уровне (интересно, как?).
Так что хоть сравнение по классике и устранило бы проблему, но это было бы именно обходом бага процессора/оптимизатора, а не устранением бага программы. Просто случайное совпадение.
Там не исключено (не проверял, просто предположение), что если в качестве эпсилон брать ноль, то тоже бы сработало (и, например, вылезло бы снова на более высоких уровнях оптимизатора).
vesper-bot
Как я понимаю, просто заставили процессор работать на 64-битных вещественных числах формата double, не более того.
cher-nov
Сейчас для вычислений с IEEE-754 числами на платформах x86 компиляторы стараются генерировать код с применением SSE / SSE2, а не команд x87 FPU. Однако поскольку 32-битный код компиляторы по умолчанию (когда не указана целевая платформа) выдают совместимым аж с i386 / i686, то заметным это становится лишь в 64-битном режиме, где присутствие SSE2 уже можно гарантировать ввиду его наличия во всех известных x86-64 процессорах.
Именно поэтому у автора работала 64-битная сборка, но не работала 32-битная. А если бы он подал компилятору ключ
-mfpmath=387
, то сломалась бы и она - ничто не мешает использовать инструкции x87 и в 64-битном режиме, и порой это даже происходит само собой. Но для этого операционная система должна сохранять контекст сопроцессора между переключениями задач.К слову, куда более простым и правильным решением проблемы было бы использование не
volatile
, а обычногоlong double
, который поддерживается к тому же ещё начиная с C89.funny_falcon
MSVC вроде приравнивает long double к double
cher-nov
Да, но именно поэтому в нём такой подляны и не должно быть, потому что здесь она была бы совершенно однозначным багом компилятора.
unicrus
Можно было сделать и так:
return da < db ? true : (da > db? false : a < b);
nextdesu
Он же написал почему он не мог сравнивать с допуском
Использовать допуск? Простите, вы или не прочитали код, или совсем его не поняли. Что произойдёт, если вместо сравнения
da
иdb
я выполню doif(std::fabs(da-db)<epsilon)
? Если две вершины имеют близкие отклонения, то вместо сортировки их по отклонению мы отсортируем их по индексам. ОТЛИЧНО. В чём смысл, кроме как в снижении оптимальности алгоритма?Tiriet
в описанном случае с
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 - с учетом экспонент конечно чуть сложнее, но суть такая).
avost
Без условия возможности увеличения размеров можно брать эпсилон - минимально представимое по абсолютной величине в fpu число - единичка в младшем бите мантиссы и минус максимальный порядок. Тогда и масштабирование, кажется, будет детерминированым (ну, ок - в половине случаев :) ). По сложности - это плюс одна загрузка регистра и плюс одно сранение/вычитание.
А, кажется, нет - мантисса же нормированная должна быть? Тогда недетерминированность сильно возрастает...
Но авторское решение в любом случае изящнее.
slonopotamus
Если что, это называется 1 ulp.
Tiriet
ну да, написал. а потом всю статью пытался объяснить суть происходящего в его коде, и в конце концов разобрался- его код da==db фактически тоже сравнивает значения с относительной точностью, равной точности округления чисел (2.2e-16), но между первым и вторым сравнением в оптимизированном коде есть ооочень небольшое изменение значений, вызванное логикой работы процессора, и это изменение как раз перекрывает использованную им точность сравнения, поэтому результат da<db не согласован с результатом da==db. Я сам напрыгался по граблям в похожих задачах (пересечение численных сеток) и ооочень хорошо понимаю автора. С практической точки зрения проще, быстрее и спокойнее, если ты сам всегда и полностью контролируешь погрешность сравнения флоатов и держишь ее на два порядка большей, чем у процессора в худшем случае- но это приводит к усложнению вроде бы понятных алгоритмов и куче идиотских проверок, перепроверок и перепроверок перепроверок. А тут- ну да, код простой и формально логичный, но привел к багу 323, где уже и так толпа людей.