Как стать автором
Обновить

Комментарии 127

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

Да.

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

НЛО прилетело и опубликовало эту надпись здесь

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

в описанном случае с 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 - с учетом экспонент конечно чуть сложнее, но суть такая).

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

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

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

не верно. ulp- unit in the last place- это более сложная штука, чем "минимальное число".

минимально представимое по абсолютной величине в fpu число- 4,9406564584124654e−324 - это минимальное денормализованное число. (единичка в последнем бите мантиссы и минимальная экспонента). есть еще минимальное нормализованное число- 2,2250738585072014e−308, а есть точность представления чисел, которая как раз ulp- и которая определяется как разница между 1,0 и следующим положительным вещественным числом, и для fp64 ~2.2e-16. при этом разница между 16.0 и следующим вещественным числом не будет равна 2.2e-16, а будет 3,5е-15. и разница между 2,2250738585072014e−308 и следующим за ним положительным вещественным числом не будет равна 2,2250738585072014e−308, а будет равна примерно 2.2e-324- что, между прочим, меньше, чем минимальное представимое в fpu число. числа с плавающей точкой- это не так просто, как кажется. :-).

Можно взять std::numeric_limits<double>::epsilon().

std::numeric_limits<double>::epsilon(). Returns the machine epsilon, that is, the difference between 1.0 and the next value representable by the floating-point type T.

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

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

Зашёл в коменты, чтобы как раз это и найти)

Если бы он сравнивал с допуском, то он сломал бы std::set прямо с гарантией. Неточное сравнение не удовлетворяет аксиоматике отношения порядка.

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

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

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

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

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

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

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

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

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

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

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

НЛО прилетело и опубликовало эту надпись здесь

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Как показывает более чем 20-летний опыт работы с клиентами, у некоторых древние компы даже с Windows 98 встречались года 3 назад. Вот работает какая-нибудь региональная телекомпания на госбюджете. Думаете, легко обосновать руководителю советской закалки, что надо бы купить новый комп вместо старого, который вполне нормально работает и свою задачу выполняет?

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

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

Вообще, конечно, дожились. Я так понимаю, казуальная игрушка (впрочем, я не вникал, могу ошибаться), и 64-битная. Больше 3 гигов оперативки требует, что-ли?

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

НЛО прилетело и опубликовало эту надпись здесь

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

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

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

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

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

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

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

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

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

Т.е. для кроссплатформенного кода нужен тип double_t который имеет предсказуемые результаты, а тут что-то типа использование нативного int и работы с его переполнением в надежде что поведение будет ожидаемым всегда при вычислении классическом и на всяких аккумуляторах из avx.
А то например uint8_t переполнился, но сравнение даёт ошибку, т.к. carry биты всё ещё лежат в 32 битном регистре.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

32-битные

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

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

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

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

Скверно. Полистал интернеты - пишут, мол, в 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.

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

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

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

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

Но!

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

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

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

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

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

@cher-nov

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

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

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

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

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

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

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

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

А зачем?

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

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

После вызова какой именно функции программа вываливается по 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]

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

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

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

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

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

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

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

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

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

А зачем?

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

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

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

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

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

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

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

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

Код на фортране (падает 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: А, увидел ответ внизу. Ну, тут либо шашечки, либо ехать. На Вашем месте я бы попробовал достать новую версию, скажем так, из неофициальных источников, и на ней просто попробовать один раз собрать и проверить. Иначе так можно и до скончания веков отлаживать, если не исключать подобные варианты, находящиеся вне зоны нашей ответственности.

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

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

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

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

Функция 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 для тех, кто мог с похожим багом столкнуться.

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

P.S.

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

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

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

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

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

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

Погодите, рано радоваться. Мы до сих пор не знаем, из-за чего именно появляется косяк. Описанное Вами решение проблемы мало чем по сути отличается от того, что Вы уже делали до меня, а именно: исключения вызова функции getdat() из цикла. Давайте разбираться.

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

Тут нужно основываться на тех гарантиях, которые даёт как сам язык, так и его конкретный компилятор.

Например, в Ваших записках я видел недоумение по поводу замечания "This subroutine is thread-safe" в описании функции getdat() - мол, зачем такое писать, разве она была раньше не потокобезопасной? А это и есть гарантия. По умолчанию программист ни для какой внешней функции не может заведомо знать, является ли она потокобезопасной (thread-safe) и/или перепосещаемой (reentrant) - такое ему может пообещать только её некое сопроводительное описание.

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

Попробуйте всё же достать последнюю версию Intel Fortran (откуда угодно) и попробовать собрать на ней. Это просьба. Если косяк повторится, то пришлите, пожалуйста, исходный код и дизассемблированный вид всех функций, которые вызываются из SCREEN_PUTL0_TIME() и далее. Ссылка на мой Telegram у меня в профиле.

Попробуем соорудить minimal reproducible example, и если опасение подтвердится, то зашлём отчёт о баге компилятора в Intel.

Погодите, рано радоваться. Мы до сих пор не знаем, из-за чего именно появляется косяк. Описанное Вами решение проблемы мало чем по сути отличается от того, что Вы уже делали до меня, а именно: исключения вызова функции getdat() из цикла. Давайте разбираться.

Да,

но как?

Конечно, хорошо бы найти конкретный блок asm-команд с ошибкой. Но как? У меня в других местах есть аналогичные вызовы функций типа real*4, где возвращаемое значение не используется. И там аналогичные баги вроде не возникают. "Минимальная программа", которую я пытался сделать в самом начале (вызов таймера из моей корявой функции + вызов ГСЧ из того же модуля) работала корректно.

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

понять не могу

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

А как тогда сравнивать, если даже не могу найти в дизассемблере то место, где это присвоение происходит?!

Тут нужно основываться на тех гарантиях, которые даёт как сам язык, так и его конкретный компилятор (...) . Если компилятор или язык разрешают не использовать возвращаемое значение, даже пусть оно и присвоено,

Насколько я знаю -

однозначно разрешают

Я нигде ни разу не видел ничего похожего на требование, что любая переменная должна обязательно использоваться. Если объявленная переменная встретилась в программе хотя бы в одном месте, никакие предупреждения компилятор не выдает. Достаточно в коде написать "i=42", и все: считается, что переменная i задействована.

С функцией еще проще. Если я написал:

my_real = my_real_function(...)

то никаких сомнений в формальной корректности этого кода нет. В фортране нельзя написать:

call my_real_function(...)

это действительно будет ошибкой. Но первый способ вызова - это стандарт.

то мы напоролись на совершенно конкретный баг компилятора (а если точнее, то оптимизатора).

Да, очень похоже на то. Однако, если это и баг, то

не совсем тривиальный

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

глобальным поиском,

после чего остается вручную пробежаться по паре (ну или паре сотен) аналогичных мест ;-)

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

больше десяти таких мест

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

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

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

Я все-таки думаю, что не запрещают. Если собирать программу без оптимизации (или на каком-то минимальном уровне), то все работает без малейших проблем. И даже с оптимизацией - работает корректно, но

не на всех процессорах

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

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

Попробуйте всё же достать последнюю версию Intel Fortran (откуда угодно) и попробовать собрать на ней. Это просьба.

Я сам уже давно

собираюсь

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

Если косяк повторится, то пришлите, пожалуйста, исходный код и дизассемблированный вид всех функций, которые вызываются из SCREEN_PUTL0_TIME() и далее. Ссылка на мой Telegram у меня в профиле.

Попробуем соорудить minimal reproducible example, и если опасение подтвердится, то зашлём отчёт о баге компилятора в Intel.

Задача ясна

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

в сроках

и вот тут я даже предварительно ничего спрогнозировать не могу :-((

Хабр в этот раз почему-то не прислал оповещение мне на почту, заметил ответ только сейчас.

Конечно, хорошо бы найти конкретный блок asm-команд с ошибкой. Но как? У меня в других местах есть аналогичные вызовы функций типа real*4, где возвращаемое значение не используется. И там аналогичные баги вроде не возникают. "Минимальная программа", которую я пытался сделать в самом начале (вызов таймера из моей корявой функции + вызов ГСЧ из того же модуля) работала корректно.

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

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

Да, именно это и нужно сделать в первую очередь.

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

А как тогда сравнивать, если даже не могу найти в дизассемблере то место, где это присвоение происходит?!

Выложите оба варианта дизассемблированного вида функции screen_putl0_time() на Pastebin и пришлите мне или сюда ссылки. Попробую разобраться.

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

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

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

«Состояние FPU» — это довольно‑таки общее понятие, подразумевающее сразу несколько вещей в совокупности: operating environment (control word, status word, tag word, instruction pointer, data pointer, and last opcode) и register stack. Первое, естественно, по ходу выполнения x87-инструкций может частично меняться. Но нас это уже не интересует, потому что, как мы выяснили, проблема возникает из‑за остаточных значений в register stack.

Сейчас мысль проскользнула: а что, если дело в неправильно указанном соглашении о вызове? Потому что при том же cdecl число с плавающей точкой в качестве результата функции возвращается именно через стек FPU. При этом отдельно оговаривается, что больше в нём значений быть не должно, а перед вызовом стек вообще обязан быть пустым.

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

Вопрос к знатокам: подскажите, а может ли выгрузка 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 года.

  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]

Ну по сути тут fild+fstp, остальное к данной операции не относится. А они реализуются прямолинейно и тут не на чем сгенерировать NaN или даже Inf.

А расширитель типа 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 года.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

наивный фикс

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

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

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

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

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

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

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

Полностью согласен. Всё жду (самому не досуг) - кто напишет 3D движок на фихедпоинтах. 10 км/2^63 = 10^-15 метров. Этого хватит описать сцену с точностью до протона. Зато никаких дёрганий по краям карты (привет всяким кракенам из KSP и похмельным рукам из Subnautica)

Пожалуйста, поверьте мне, когда я говорю, что знаю, на что иду, когда сравниваю double.

С какой радости тебе верить? На каком основании? Тот бред который я поскипал не доказывает наличие знаний. Либу он использовал. Баг который тебе дал пинка под зад доказывает их отсутствие. Это как раз и есть знания - как писать так чтобы не было багов. В реальности. А не больном воображении на абстрактной си машине. И этих знаний на проверку в реальности не оказалось. Как и не оказалось знаний как подобрать эпсилон.

Хуже с++ может быть только с++ который знает.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории