Как стать автором
Обновить
23
0
Дмитрий Чернов @cher-nov

в экономике, так сказать, необходим

Отправить сообщение

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

Это с военной приёмкой-то, да ещё и в шестидесятых, до "разрядки"? Довольно сомнительно.

Вы откуда это взяли?

Лично я против названия "звук" для языка, который к звуку даже отношения не имеет, но "протому что просто красиво".

Так речь здесь не про тот sound, который audio, а тот, который soundness и в "safe and sound".

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

Название только и впрямь неудачное - по такому искать в интернете неудобно. Затем бренды и нужны.

Выглядит очень костыльно.

Но это общепринятый способ для всего WinAPI. К слову, в обычном Си также есть похожая идиома с приведением выражения к void. Чтобы такое выглядело чуть менее костыльно, подобные стопки следует писать уже после return, в самом хвосте кода функции.

Простой комментарий или отпил имени параметра выглядит логичней.

В стандартном переносимом Си (по крайней мере, до C99 включительно) такого делать нельзя.
https://stackoverflow.com/questions/8 776 810/parameter‑name‑omitted‑c-vs‑c

«Согласно протоколу, при подлёте «Луны-25» к Луне бортовой компьютер станции должен был подать на «БИУС-Л» команду на включение акселерометров. Программисты эту команду подали (это было видно по циклограмме), то есть, по сути, тоже все сделали правильно, за исключением одного. Их программное обеспечение не проанализировало ответную реакцию «БИУС-Л», по сути, просто не убедилось, что блок измерения угловых скоростей запущен в работу. А дублирование команды для проверки запуска модуля в программе не было», - рассказали источники СМИ.

Звучит буквально как ошибка наподобие вызова malloc() без проверки на NULL.

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

Ничего. Потому что это вообще-то не вопрос поддержки, а вопрос общей инженерной культуры. Если бы стандартная библиотека во-первых не была монолитной, во-вторых поддерживала бы opt-in подход к её использованию, а в-третьих не была бы намертво приштопана к самому языку на уровне всех этих lang_items, то необходимости в столь жестокой deprecation policy попросту бы не было.

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

в Rust 1.76.0 разработчики планируют отказаться от Windows 7, 8 и 8.1, оставив Windows 10 в качестве минимальной версии для компилятора и целей сборки.

Это, конечно, финиш полный и абзац. Шёл 2023 год, а разработчики стильно-модно-молодёжных языков программирования так и не научились делать стандартные библиотеки с opt-in и без монолитности.

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

Конечно, хорошо бы найти конкретный блок 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. При этом отдельно оговаривается, что больше в нём значений быть не должно, а перед вызовом стек вообще обязан быть пустым.

То есть вы предлагаете эмулировать референсное поведение даже там, где это не нужно/не эффективно, я правильно понял?

Нет. Как раз я предлагаю писать стандарты таким образом, чтобы подобные ситуации в принципе не возникали. Без reference implementation же определять точки возможной неэффективности можно лишь путём тычка опытным пальцем в небо.

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

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

Что значит "естественно, сам по себе"? UB - это когда никакое конкретное поведение не гарантируется стандартом, всего-навсего.

Это означает, что в reference implementation места, требующие явной поддержки со стороны исполнительного устройства, становятся видны сразу, и по ним уже можно ориентироваться, что стандартизировать, а что оставить на волю implementation-defined / unspecified / undefined поведения. А не как сейчас, когда, скажем, в C23 realloc(ptr, 0) вдруг превратился в самый настоящий UB, а malloc(0) как был, так и остался implementation-defined, и всё это произошло по щучьему велению.

Референсный компилятор вполне может компилить в байт-код по типу "ассемблера MIX" Кнута или p-code Вирта, на момент создания С оба уже были, а синтаксис Си в отличие от тех же плюсов разбирается любым стандартным (т.е. многократно проверенным) лексером/парсером.

О, спасибо большое — Вы у меня этот комментарий с языка сняли. :)

Должно ли поведение этой виртуальной машины любой ценой эмулироваться везде, даже там, где это неэффективно/вредно?

Если речь идёт о стандарте — то да, должно. Но тут надо определиться. Либо мы хотим стандартизировать язык программирования, и тогда документ надо писать по образцу A Commentary on the UNIX Operating System (то есть в виде неформальных поясняющих записок к формальному строгому описанию). Либо же мы идём по стопам условного Pascal, где за всю историю накопилось множество едва совместимых друг с другом диалектов (и да, я знаю, что для него тоже стандарт сделали, но на него всем было наплевать ещё тогда). Тоже вполне себе вариант, со своими плюсами. К слову — это весьма вероятное будущее Rust в свете появления gcc‑rs.

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

UB в таком случае начинает возникать естественно, в ходе дела, и перестаёт описываться произвольным образом. А не как сейчас в стандартах Си и C++: хачю штобы харошые праграмы на харошых мафынах роботале, а нихарошые на нихарошых нироботале, вотЪ! (и ладонью по столу).

А может, хватит уже нам гордости за великую и необъятную, а?

вам — хватит

UB ведь это такое хорошее оправдание чтобы писать небезопасный компилятор.

Это неправда.

Разработчики GCC и Clang по сути делают то, что должен был с самого начала делать исключительно комитет по стандартизации, а именно — справочный образец (reference implementation) компилятора Си, который одновременно с этим являлся бы непосредственной формализацией семантики языка программирования. Потому что изложить её обычным человеческим языком на практике невозможно — документ такого объёма нельзя ни нормально (то есть однозначно) написать, ни нормально прочитать.

Давно показано, что из любого текста, в общем‑то, можно извлекать фактически бесконечное число разных смыслов, причём многие из них будут друг с другом вполне сопоставимы по своей логической состоятельности. Привлечение же специальной терминологии ситуацию не только не улучшает, но даже наоборот — любой сложный термин является лишь некоторым обобщением (абстракцией) над более простыми, чему неизбежно сопутствуют и невязки в его понимании. Собственно, именно эта проблема в своё время уже привела в философии к так называемому лингвистическому повороту — Витгенштейн, Коржибский и прочие ребята (как же я люблю аналитическую философию, вот они слева‑направо). К слову, здесь на Хабре вот совсем недавно были весьма занятные статьи на эту же тему: раз, два.

Поэтому винить разработчиков компиляторов не в чем — винить надо комитет. Не стреляйте в программиста — он играет, как сказали. Не говоря уже о том, что и в GCC, и в Clang издавна наворочено множество ключей и флагов компиляции буквально затем, чтобы люди могли при необходимости устранять идиотию стандартизаторов или хотя бы обходить её стороной.

А вот C++ уже едва ли что-то поможет, тут да, всё так.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

А зачем?

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

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

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

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

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

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

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

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

Информация

В рейтинге
Не участвует
Откуда
Владивосток, Приморский край, Россия
Зарегистрирован
Активность