
Комментарии 124
У меня просьба: проверте, как компилятор обработает это ((-1)^2)^(1/2)=?.
Результат вас удивит. В C-подобных языках ^ – это xor, выполняемый над целыми числами, так что это будет (-3)^0, т.е. -3.
А что касается других языков – всюду свои правила, зачастую оператора возведения в степень нет, вместо него функция – и можно рассчитывать, что она завершится с ошибкой.
А откуда ошибка-то? Квадратный корень из единицы не извлечь?
А с чего вы взяли, что там квадратный корень?🙂 И я даже не про ^.
В Си (1/2) = 0, потому что вы целое делите на целое, то есть это целочисленное деление.
Вот если бы вы записали (1.0/2) или (1/2.0), тогда было бы 0.5 😉
Если вы про си, тогда мы говорим про ксор. Однако семантика сообщения говорит явно про возведение в степень, так что целочисленное деление не в тему.
Буквально неделю назад ломали голову, как деление сделать не целочисленным. Потом просто посмотрели в старую тетрадь с конспектами за 1 курс универа (да, я ещё храню их), чтобы вспомнить это очевидное решение/
А, да, действительно, был невнимателен и решил, что товарищ выше имел в виду sqr(sqrt(-1)), а не наоборот.
Математически запись эквивалентна
и часто используется именно с этой целью. Мат.пакеты подобное сокращают только при явном указании
.
Вы забыли добавить эту статью для наброса на вентилятор. Запасаюсь попкорном :)
Не абстрактное правило из учебника
Просто этому правилу научили и компьютер, не более того. Вот если бы разбиралась физические процессы аналогичны делению и там тоже оказалось, что на 0 делить не получается, вот тогда бы да.
Так в том то и дело, что не научили.
именно что научили. C - язык, который считает программиста не идиотом. Программист с точки зрения компилятора априори знает и не допускает деления на ноль, разыменования пустых указателей, целочисленного знакового переполнения, неправильного приведения типов и тд и тп. Этого всего в программе быть не может просто потому что не может, и компилятор использует это знание во славу производительности и для регулярного производства новых CVE
Не научили это значит процессор зависает и перезагружается. Если он бросает исключение, то значит он корректно обрабатывает данную ситуацию и в принципе знает про неё.
На то оно и UB: у меня падает.
Вообще, интуитивно хочется, чтобы ответ был и правда 1. Как будто lim(x/x)=0 при х->0. Совсем другой разговор если речь идёт о выражении х/у.
Компилятор?
Самое нужное и не указал. GCC версии 6.3.0 , среда Кодблокс под виндой.
Так и хочется сказать - когда мы были молодыми а деревья зелеными) Достаочно древняя версия. Я проверял на gcc 12.2.1 wsl/alpine и gcc 13.3.0 wsl/ubuntu
https://godbolt.org/z/17qbhjozP Да, в gcc 7 началось сворачивание
https://godbolt.org/z/Y463sa7qf clang на -O0 оставляет деление, а на -O1 и выше – константа
Воттераз, как раз на границу попал!
Проверял на той же машине, с которой и пишу комментарий. "Офисной" как её называю. Тут среда разработки вообще случайно оказалась. Последний раз открывал её полгода назад, сваять проект на вечерок для АВР.
Ну что ж, зато можем сравнивать поведение актуальных версий с моей.
Rust такой фигнёй не страдает
Т.е. он не сможет оптимизировать выражение x/x?
Ваша ссылка это подтверждает. Раст при оптимизации добавит сравнение с нулем и вызов паники. https://godbolt.org/z/1ebqPG3Er
А, да, всё правильно. Я чёт затупил и не с 0 запускался, а с 3.
Математическая ошибка в рассуждении компилятора
В алгебре существует фундаментальное правило:
Сокращение x в числителе и знаменателе допустимо тогда и только тогда, когда явно оговорено, что x ≠ 0.
Что? Если в знаменателе стоит x то он не может быть нулем, следовательно мы всегда можем сокращать на x. Иначе, все выражение не имеет смысла.
вам никогда не встречались неопределенности вида "0/0"?
но вообще не понятно почему деление на ноль нужно демонстрировать через x/x, а не x/0
Встречались, только при чем тут они?
Не, ну так-то деление на ноль на x86 красивее всего демонстрировать, когда ни числитель, ни знаменатель не 0: инструкция div процессора делит 32-битное число на 16-битное, и если результат не влез в 16 бит – это считается делением на 0 :-)
зачем пишете, если не знаете? неопределенности [0/0] встречаются в пределах, в которых рассматривается выколотая окрестность точки стремления, т.е. если в самой точке это [0/0], то в окрестности это почти всегда не так, а значит и деления на 0 не происходит.
но вообще не понятно почему деление на ноль нужно демонстрировать через x/x, а не x/0
А иначе не будет повода написать статью, чтобы прорекламировать свою школу программирования.
Но меня ещё другое смутило - фактически делить на 0 можно, в высшей математике есть пределы и там есть неопределенность 0/0, где в некоторых случаях (а именно lim x/x), будет действительно 1...
никто на 0 там не делит, успокойтесь уже, это миф. рассматривается выколотая окрестность точки, т.е. до самого деления на 0 не доходит
Я после вашего комента 2 раза пересмотрел статью и вообще не обнаружил в ней никаких ссылок? В чем реклама заключается, если даже название школы, город и страна не упомянуты?
sin(x)/x
Может, пока ОДЗ явно не определена так, чтобы исключать 0 в знаменателе. Что иными словами так и читается «когда явно оговорено, что знаменатель не обращается в ноль»
Как раз наоборот, сокращать можно только тогда, когда есть ограничение на область определения - уверенность в том что х никогда не равен нулю, разве в этой программе выражено такое утверждение? Ниже правильно было сказано - компилятор не имеет право додумывать, его задача перевести в машинный код инструкции программиста. В данном случае есть сомнения в том что компилятор правильно справился с этой задачей.
Уверенность в том, что X не равен нулю нам даёт тот факт, что X находится в знаменателе. Иначе все выражение не имеет смысла.
Ну как так??? С какого пререпугу факт нахождения х в знаменателе может дать уверенность в том что он не может быть равен нулю, вы путаете причну и следствие.
Потому что ноль в знаменателе - UB, а программа на C/C++ не может пользоваться UB, компилятор вправе расчитывать на то, что программист предусмотрел, что ноля там не будет никогда. Поэтому сам факт наличия x в знаменателе чётко указывает на то, что нулю он не равен никогда.
Потому что в ином случае все выражение не имеет никакого смысла, оно невалидно, ерунда, чушь, как ещё объяснить - я не знаю.
Del
Да уж. К сожалению, та малая часть программирования, которая использует в решении своих задач достаточно низкоуровневые языки, все больше и больше помимо непосредственно программирования занимается борьбой с компиляторами. Стремление максимально оптимизировать код оборачивается тем, что компилятор все чаще и чаще приходится бить по рукам.
Впрочем, это порождает "проблему вагонетки" в мире кода - что лучше, выдать неверный результат на определенной итерации или упасть и прекратить дальнейшую работу совсем. Плохо только, что вот такими скрытыми оптимизациями решает ту дилемму не разработчик (который должен был бы проверить на ноль, если не сразу то после N-ого падения). От него-то как раз проблему спрятали и сильно усложнили отладку. Хорошо когда результат вот так запросто воспроизводится. А если вычисляется? Да из множества переменных? Впрочем, в таком варианте этой проблемы вроде бы и возникнуть не должно.
Где-то мир свернул не туда. Иногда очень хочется вернуться к славному ассемблеру, где только ты был автором.
Проверил для "младшего брата"
#include <stdio.h>
int x;
void main(void)
{
scanf("%d",&x);
printf("%d\n", x/x);
}
константа в коде. Четко как в статье. gcc (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0
0000000000001169 <main>:
1169: f3 0f 1e fa endbr64
116d: 55 push %rbp
116e: 48 89 e5 mov %rsp,%rbp
1171: 48 8d 05 9c 2e 00 00 lea 0x2e9c(%rip),%rax # 4014 <x>
1178: 48 89 c6 mov %rax,%rsi
117b: 48 8d 05 82 0e 00 00 lea 0xe82(%rip),%rax # 2004 <_IO_stdin_used+0x4>
1182: 48 89 c7 mov %rax,%rdi
1185: b8 00 00 00 00 mov $0x0,%eax
118a: e8 e1 fe ff ff call 1070 <__isoc99_scanf@plt>
118f: be 01 00 00 00 mov $0x1,%esi
1194: 48 8d 05 6c 0e 00 00 lea 0xe6c(%rip),%rax # 2007 <_IO_stdin_used+0x7>
119b: 48 89 c7 mov %rax,%rdi
119e: b8 00 00 00 00 mov $0x0,%eax
11a3: e8 b8 fe ff ff call 1060 <printf@plt>
11a8: 90 nop
11a9: 5d pop %rbp
11aa: c3 ret
Справедливости ради - в реальном коде такое встретить сложно, но... Если угораздит, то седых волос при отладке добавит.
И да, для учебного примера куда лучше сгодился бы вариант
#include <stdio.h>
int x;
void main(void)
{
scanf("%d",&x);
printf("%d\n", x/(x-1));
}
или его аналог на плюсах. Тут и проблемы бы не было, и потенциальные грабли подсвечиваются ближе к реальной жизни.
скрытыми оптимизациями
А бывают иные? Любая оптимизация по определению - изменение исходного кода.
Это проблема не в компиляторе, а в стандарте языка.
Да что вы прицепились к стандарту языка, которому уже много лет. Он пережил разный endian, разную разрядность и много чего еще. Все его UB просто следствие необходимости работы в очень разном окружении, которое не ограничивается Linux на amd64/arm64. Проблема не в стандарте. Проблема в избыточной оптимизации, ломающей логику работы. А подобные проблему будут у абсолютно любого языка, когда он достигнет зрелости. Только вот... Большинство умирает молодыми и просто не доживут до этой прекрасной поры. Ну устаю повторять про python при переходе со 2-ой на 3-ю версию. Он уже поломался. В отличии от дряхлого старичка, который держится.
Впрочем - ну его нафиг. Я не буду ввязываться в очередной раз в эти войны. Вообще учить детей сегодня С или плюсам - ну так себе идея. Сюда надо ЗАХОТЕТЬ погрузиться. Это очень специфические языки для очень специфических задач. Это не то, с чем есть шанс случайно встретиться. Удел инженеров. С старом смысле этого слова.
Rust такой фигнёй не страдает
И не умер молодым
Дайте ссылку на компилятор Rust для PIC и AVR. А еще для AIX, beOs, OS/2 и MSDOS (а еще Windows 3.1, QnX и прочего зооларка и бестиария возможных осей и архитектур).
Нет, он не умер молодым. Но в лучшем случае детский сад с отличием закончил. А доживет ли хотя бы зрелости - это мы еще посмотрим.
Некоторые есть
https://dev-doc.rust-lang.org/rustc/platform-support/nto-qnx.html
https://dev-doc.rust-lang.org/rustc/platform-support/avr-none.html?highlight=Avr#avr-none
целом Rust через no_std + custom target spec можно натянуть на почти что угодно, если есть LLVM backend для архитектуры
Rust такой фигнёй не страдает
Эта и подобные оптимизации - это фича LLVM, на которой и раст базируется.
Так что я бы сначала проверял такие случаи, 100% уверенности нет.
Все его UB просто следствие необходимости работы в очень разном окружении, которое не ограничивается Linux на amd64/arm64
Разве за это не отвечают unspecified behavior и platform specific? За все эти 10 битовые байты, int-ы в 1 байт и прочее.
Проблема в избыточной оптимизации, ломающей логику работы
Так почему ломающей то? Компилятор сохраняет видимое поведение программы всегда. Корректной с точки зрения стандарта программы, уточню. А если вы вышли за стандарт - значит пишите уже на каком-то другом языке, компилятор тут не поможет.
Согласен, не самая правильная формулировка. Но...
Последовательность операций, важная для правильной инициализации оборудования. Формально компилятор имеет право, по факту при определенных ключах поведение железа ломается. Избыточное выделение функции, в тех местах где я борюсь за производительность и пишу линейный код. Оно не ломает видимое поведение, но роняет производительность. И это только то, что на поверхности. К сожалению, это норма жизни. И отлавливается (а за одно исправляется) очень сложно.
Нужно показать детям, что деление на ноль — это ошибка.
Это не ошибка, а "бессмысленная" операция, для которой в формальной системе не определена операция обратная осмысленная.
А в некоторых местах вне элементарной алгебры делить можно. Разрешаю и вашим ученикам.
Дети видят — компьютер не может делить на ноль, программа аварийно завершается.
С тем же успехом можно объяснять детям, что ветер дует, потому что деревья качаются.
Математика устроена не так, как считает правильным железо/компилятор. Это железо/компилятор устроены так, как выгодно математике, догадываетесь?
Причем, преимущественно с инженерной точки зрения.
Если вы хотели показать, что нельзя делить на 0, то, ну... надо было делить на 0. А не на переменную.
В алгебре существует фундаментальное правило
Сокращениеxв числителе и знаменателе допустимо тогда и только тогда, когда явно оговорено, чтоx ≠ 0.
Чего? Покажите, пожалуйста, в какой из алгебр вы видели такое правило.
И вот здесь компилятор воспроизвёл классическую математическую ошибку
Компилятор все сделал верно. И даже гуманно - справедливо было бы упасть с сообщением "сокращай".
А он за вас сократил, так еще и бонус приятный сделал - сократил накопление ошибки в нагруженном смыслом расчете. Потому что он сделан для выражений чуть сложнее x/x.
Чего? Покажите, пожалуйста, в какой из алгебр вы видели такое правило.
Если честно, удивлён вашим недоумением. Решите уравнение sqrt(x)/sqrt(x) = 1
Мы бы хотели зачеркнуть одинаковые выражения, но нужно убедиться, что преобразование не изменит множество решений. Равносильным переходом будет переход к системе {sqrt(x) != 0; 1=1}. Второе уравнение даёт x€R. Первое неравенство решается, как x€(0; +inf), и это ответ. Я уверен, что автор говорил именно в таком ключе.
Если честно, удивлён вашим недоумением.
Оно связано с тем, что автор "сформулировал" собственно сам бан деления на ноль и сокращения тут ни при чем.
Вопрос был скорее риторическим :)
Решите уравнение
sqrt(x)/sqrt(x) = 1
Ну, компилятор такое и не сократит. Я понимаю, что вы пытаетесь сказать, но это никак не влияет на то, что компилятор, расстроивший автора - прав.
CPU не решает уравнения и не оперирует множествами. Он выполняет операции над числами.
Мы бы хотели зачеркнуть одинаковые выражения
Вы имеете на это полное право. Даже не так - вы обязаны это сделать, чтобы не вычислять корень два раза. Из некой сложной f(x), где такая дробь возникла, легитимно выкидываете, заменяя на единицу.
Перед этим, разумеется, вычисляете корень единожды и осуществляете проверку root != 0, кидаете исключение сами. А можете проверку и не делать, если где-то выше есть гарантии, что x != 0. Да, это тоже самое, что написали и вы, де-факто распространив на свое решение бан деления на ноль.
Контекст всегда важен, но компилятор исходит и всегда должен исходить из того, что написанное написано со смыслом. А вот генерить деление, в котором один и тот же регистр фигурирует 2 раза - не очень осмысленно.
Я понимаю, что вы пытаетесь сказать, но это никак не влияет на то, что компилятор, расстроивший автора - прав
Да я и не отрицал этого.
Да я и не отрицал этого.
А это, на самом деле, очень спорный вопрос. Дело компилятора транслировать код в язык аппаратуры. И если автор желает делить на ноль - на здоровье. Дели. И уже аппаратура будет решать что делать. А решить она может по разному. Например 8051 выставит флаги и (сюрприз!) неопределенный результат (который обычно 0xFF, но это не точно). АVR и PIC даже флагами не пошевелят, правда выставят 0xFF. ARM кинет исключение. Собственно это и есть причина, по которой в С это UB.
И не дело компилятора додумывать что я, как автор, хочу в данном месте. Может мне надо таким вот образом 0xFF в регистры на AVR поместить. Или флажки выставить на PIC. Или принудительно в исключение свалиться. Я автор - мне решать. А компилятор... Может помочь - замечательно. Но мешать не надо.
Может мне надо таким вот образом 0xFF в регистры на AVR поместить
Тогда и выражайте свои намерения явно, x = 255?
В случае автора это также означает, что надо было вместо статьи написать x/0, но видимо помешал опыт программирования в 30 лет.
Если бы вместо int бахнул float - удивился бы с учениками ещё сильнее.
И не дело компилятора додумывать что я, как автор, хочу в данном месте
Компилятор ничего не додумывает, да и на намерения художника ему все равно.
Он просто выбирает (в лице людей его написавших) оптимальный путь в рамках оборудования и формальной системы, которой является язык.
Это ваша обязанность - знать и понимать выхлоп компилятора.
Либо берете в руки asm. Но и там могут поджидать свои опасности из-за, например, out-of-order.
Тогда и выражайте свои намерения явно, x = 255?
Вы ограничиваете мою свободу, как автора? В другом мире это называется словом цензура и сильно не приветствуется. Я не говорю что так правильно. Я говорю что так можно - аппаратура это не запрещает. А любой компилируемый код, в конце концов работает именно на аппаратуре. Даже если кому-то кажется по другому.
Компилятор ничего не додумывает, да и на намерения художника ему все равно.
Первое хорошо, но не соответствует действительности. А второе очень плохо - намерения художника для него обязаны быть четким приказом, а не поводом для принятия собственного решения.
выбирает (в лице людей его написавших) оптимальный путь в рамках оборудования и формальной системы, которой является язык.
Я не против, чтоб деление или умножение на 2^n заменяли сдвигом - все в рамках. Но если я пишу x/x то это означает именно то, что мне НЕОБХОДИМО поделить x на x. И я, как автор, понимаю как это будет работать на конкретной аппаратуре. И ожидаю вполне конкретный выхлоп от компилятора и реакцию оборудования. Я не нарушаю ни правила для оборудования, ни правила формальной системы. С чего вдруг не делается то, что нужно мне как автору?
То же про volatile - к сожалению очень часто приходится пользоваться этим указанием там, где оно не должно стоять в принципе. Это приводит к крайне плохому стилю написания кода, когда оное указание ставится "на всякий случай". Что в корне неправильно.
Либо берете в руки asm.
Да, и именно избыточная оптимизация и приводит к тому, что ассемблерных вставок еще хватает. Там, где реально надо то, что я хочу как автор (и гарантии того, что следующая версия компилятора точно ничего не сломает), ничего другого и не остается.
Впрочем, SUN еще в далеком 2008-ом с гордостью рассказывала о том, что мы теперь оптимизируем и ассемблерные вставки. Мы лучше знаем - у нас есть контекст окружения. А недовольны - используйте volatile asm {}; Так что лучше отдельным файлом.
Вы ограничиваете мою свободу, как автора?
Ни в коем случае. Это делает компилятор.
Я говорю что так можно - аппаратура это не запрещает.
Ну так для ваших PIC и AVR он может и не сократит. Проверьте, если GCC под них собирать умеет.
Но если я пишу x/x то это означает именно то, что мне НЕОБХОДИМО поделить x на x.
Приведите мне, пожалуйста, пример, при котором это имеет смысл в целочисленном делении. По мимо вашего желания запихнуть в регистр 0xFF, на него я уже ответил.
Я не нарушаю ни правила для оборудования, ни правила формальной системы.
Вы упускаете из внимания тот факт, что программу вы пишете не для оборудования. Вы ее пишите для другой программы, компилятора. Хоть на асме хоть на жаваскрипте.
Не хотите ориентироваться в слоях абстракций и выбранных решений, выбранных в той программе, для которой пишите свой код - пишите машинные инструкции.
Вы упускаете из внимания тот факт, что программу вы пишете не для оборудования.
Интересно. А я думал, что мне, embedder'щику, деньги платят ровно за это. И язык С создавался как "переносимый ассемблер"... Я бы позволил такие вольности плюсам. Они, обычно, сильно дальше от аппаратуры. Но классическому, безплюсовому С... Это уже перебор.
Проверьте, если GCC под них собирать умеет.
Прямо сейчас в доступе нет. Да и специфика тамошней аппаратуры накладывает некоторые ограничения на использование scanf/printf. Но при случае проверю. Не забуду - отпишусь с результатами.
Приведите мне, пожалуйста, пример, при котором это имеет смысл в целочисленном делении. По мимо вашего желания запихнуть в регистр 0xFF, на него я уже ответил.
Т.е. все таки цензура. Вариант "потому, что могу" не катит. Малоли что ты можешь - высший разум считает что не нужно.
И язык С создавался как "переносимый ассемблер"... Я бы позволил такие вольности плюсам. Они, обычно, сильно дальше от аппаратуры. Но классическому, безплюсовому С... Это уже перебор.
Си - язык высокого уровня, далёкий от современного "железа". Ваши ожидания от него совершенно необоснованы и не совпадают с реальностью.
А я думал, что мне, embedder'щику, деньги платят ровно за это.
Вы можете продолжать думать, что код на C++ написан для оборудования, я вряд ли в силах вас переубедить.
Но это не изменит реальности, в которой код на C++ написан для компилятора С++. А код на асме написан для ассемблера.
Т.е. все таки цензура.
Нет, это оптимальный осмысленный выбор. Такой же, как тот факт, что ваши микроконтроллеры по вашим же словам утверждают0/0 = 255. От чего у автора наверное сердечный приступ случился бы.
Вариант "потому, что могу" не катит.
К позиции автора, которую он пытается в статье отстоять, и вы вроде как с ним соглашаетесь, можно еще более тонкий вопрос задать.
Почему автора не смущает, что C++ как и куча других языков позволяет написать:
int a, b, c;
...
c = a / b;а глупый CPU возьмет да и положит результат целочисленного деления в целочисленный регистр?
Это с точки зрения математики чушь какая-то, как это мы получили целое число, поделив целое на целое?
Предлагаю соблюдать правила математики со всей строгостью - для начала всем участникам снижаем оценки.
Потом переделываем процессоры, чтобы они такие глупости не делали, а клали в вещественный, нечего остатку пропадать.
А потом исправляем от этого недуга математически некорректные языки ассемблера и все высокоуровневые.
Красота?
Потом переделываем процессоры, чтобы они такие глупости не делали, а клали в вещественный, нечего остатку пропадать.
Ты не поверишь (с), но в большинстве известных мне архитектур остаток не пропадает. А путь для работы оптимизатора в конструкциях типа
int a,b,c,d;
...
c = a/b;
d = a%b;вполне очевиден. В отличии от операций с вещественными числами.
Вы можете продолжать думать, что код на C++ написан для оборудования, я вряд ли в силах вас переубедить.
Я, если уж меня спрашивают, считаю что для работы с аппаратурой создан язык С. Плюсы - это работа с математическим описанием объектов реального мира. Первое меня кормит. Второе... В лучшем случае хобби. И я бы очень не хотел ставить знаки даже приближенного равенства между этими двумя задачами. Они просто разные.
Ты не поверишь (с), но в большинстве известных мне архитектур остаток не пропадает.
То, что он улетает в какой-нибудь edx, не меняет сути того, что я пытался вам донести :)
Я, если уж меня спрашивают, считаю что для работы с аппаратурой создан язык С. Плюсы - это работа с математическим описанием объектов реального мира.
Подобный нонсенс с математической точки зрения вам и уютный С позволяет.
Вы либо от позиции автора откажитесь, либо от железячной. А лучше с ветряными мельницами вообще не бороться.
Почему не меняет? Как раз меняет. Достаточно посмотреть на ассемблерных код, получаемый из указанного примера.
И я серьезно не понимаю чем позиция автора "не железячная". Впрочем, я свое видение того, как это стоило бы показать детям приводил. (x/x) действительно очень плохой пример. Во всех смыслах этого слова. И даже безотносительно языка программирования.
А лучше с ветряными мельницами вообще не бороться.
Человек который почувствовал ветер перемен должен строить не щит от ветра а ветряную мельницу (с)
Я не борюсь с ветряными мельницами. Я просто вижу естественное развитие языка. Как бы ни казалось адептам других богов, язык С продолжает развиваться. А тот факт, что его развитие не всегда идет в ногу с моими требованиями... Ну и ладно. Мне, в принципе, понятны механизмы которыми я могу добиваться нужного результата. Тут и ассемблерные вставки, и функции непосредственно на ассемблере, и набившая оскомину volatile, и разные ключи компиляции для разных файлов проекта, и #pragma и много чего еще. Что-то переносимо между реализациями, что-то нет. Абсолютно ничего страшного. А весь мой спич исключительно по теме, заданной автором.
Я прекрасно понимаю ПОЧЕМУ оптимизатор так сделал. Ровно так же понимаю почему при оптимизации под минимальный размер он агрессивно выискивает что бы вынести в функцию. Почему при оптимизации на скорость меняется порядок присвоений. Опять же - мне, для моих частных задач, часть решений кажутся спорными. Но мои задачи очень частные. Даже с позиции "железяного" использования языка. И да, опыт позволяет находить и исправлять их достаточно быстро.
Что до конкретного примера автора - то для учебных целей было бы куда полезнее конструкции типа a=x/(x-1), а еще лучше a=x/(x-y). У них не было бы таких проблем, и к реальности они были бы сильно ближе. Но, при всем при этом, мне бы очень хотелось, чтобы с -O0 (или ее аналогами) компилятор четко выполнял пожелания автора и ни на шаг не отступал то того, что написано в программе. Как по мне, вполне справедливая хотелка. Но только она и ничего больше.
чтобы с -O0 (или ее аналогами) компилятор четко выполнял пожелания автора и ни на шаг не отступал то того, что написано в программе.
Если никакую опцию -О не указывать вообще, gcc так и делает
Смотрите мой комментарий выше. Увы, но и без ключей, и с -O0 результат константа.
Программа, использующая UB, не является программой на языке Си. По сути вы хотите, чтобы компилятор Си компилировал программы, не написанные на языке Си.
Программа, использующая UB, не является программой на языке Си.
Сильное заявление. Но не правильное.
Если моя программа использует UB - она не перестает быть написанной на языке С. Она перестает быть переносимой. Нужна ли мне переносимость - зависит от множества факторов.
Но, именно для меня, она обычно важна в части переносимости между компиляторами (Keil, IAR, GCC), но не важна в части переносимости между платформами. Обычно я не хочу использовать код под Arm на AVR или наоборот. Зато есть желание писать код в коммерческих системах (Keil, IAR), но при этом иметь возможность собирать его рабочую версию Open Source инструментами (gcc). Потому UB (а вернее, как верно подсказали выше - platform specific realisation) - мне не страшно. Конечно, что при условии что все производители компиляторов его понимают одинаково.
Я не против, чтоб деление или умножение на 2^n заменяли сдвигом - все в рамках. Но если я пишу x/x то это означает именно то, что мне НЕОБХОДИМО поделить x на x.
Вы понимаете, что ваши два предложения противоречат друг другу? Наверняка найдётся какая-нибудь платформа, в которой сдвиг и деление приводят к каким-нибудь разным флагам в богом забытом регистре. И обязательно найдётся кто-нибудь, кто бы хотел именно то, что делает деление и "ожидает вполне конкретный выхлоп от компилятора и реакцию оборудования".
С чего вдруг не делается то, что нужно мне как автору?
С того, что в программе на Си нельзя выразить вашу хотелку. Пишите ассемблерную вставку.
С того, что в программе на Си нельзя выразить вашу хотелку. Пишите ассемблерную вставку.
Я утащу это в список цитат. Про остальное - сдвиг отрицательных чисел тоже не просто так UB. И те, кто работает с реальным железом об этом знают. К сожалению, авторы компиляторов... Впрочем, не будем о грустном.
P.S.
Впрочем, доля правды есть. На С переносимо очень сложно поймать overflow/underflow. И не только это. И тут да - не все хотелки можно.
Так ведь этот пример ваш тезис про достаточность неравенства нулю и опровергает, выходит что мы исключаем √x=0 <=> x=0, а для остальных случаев считаем √x/√x=1, на деле же для отрицательных x будет та же неопределенность
мы исключаем √x=0 <=> x=0
Не делали мы этого. Мы получили требование, что sqrt(x) != 0. Ну и выяснили, при каких икс это выполнено. В процессе выяснения (в момент, когда корень погиб) подтянулось требование о неотрицательности икса. Оно было с первой строчки, но было неявным, в смысле, не писалось отдельно. Ну а зачем, пока болтается значок корня в системе?
Когда-нибудь вы узнаете про комплексные числа. И про кватернионы. И про что угодно ещё.
Зачем принимать школьную егэшную математику за абсолют? Все гораздо сложнее и интереснее. "На ноль делить нельзя" и "корень можно извлечь только из неотрицательного числа" это правила не математики, а аксиоматика, в рамках которой проверяются школьные экзамены.
Ну, он мог и ноль вывести, на что ноль ни Дели, получится всегда ноль
Только в арифметике нельзя делить на ноль. В алгебре иногда можно.
По моему компилятор правильно сделал.
0/0 = 1
Компилятор имеет полное право заменить выражение x/x = 1.
А деление на 0 это неопределенное поведение. Не нужно бросать всё в 1 ящик.
Выражение a/0 не оптимизируется. По этому ваш пример неверен.
Вторая часть статьи будет? Когда внезапно окажется, что во float/double делить на ноль вполне себе можно и получится плюс или минус бесконечность, в зависимости от знака делимого. А потом от от этой бесконечности можно взять например арктангенс - и получить 1.5707... без всяких пределов.
А теперь вспоминаем высшую математику, х/х в точке 0 и раскрытие неопределенностей по правилу Лопиталя. Предел отношения функций заменяется на предел отношений их производных. Производная от х = 1. Соответственно, получаем 1/1.
Что компилятор сделал не так?
Вы видите в программе указания совершить предельный переход? Или для вас операция деление и предельный переход одно и то же?
Значение в точке А - это предельный переход при х стремящемся к А.
Нет, у функции х/x значение в точе 0 не определено, а предельный переход это инструмент математического анализа помогающий в решении ряда задач, но это ни как не значение функции в этой точке. Есть масса функций которые асимптотически приближаются к некорому значению, но никогда его не достигают, но при применении предельного перехода - да. Вербально, прделеьный переход звучит не "при х равном значению" а "при х стремящемся к значению", так что утверждать что x/x при x=0 тождественно равен lim(x/x) при x->0 и равен 1 не корректно.
Правило Лопиталя определено для функций, а не для чисел (обычных), и опирается на их разложение в степенной ряд. Для дуальных чисел уже можно (один раз). Но и причин, по которой имеет больше смысла в точке ноль лично я тоже не вижу.
Ну и для наглядности можно определить функцию, которая в целочисленных точках совпадает с
. Но
.
У меня только один вопрос:
Что за интересный такой способ извращения - преподавать плюсы пятиклашкам?
Вы видите в этой программе указание совершить предельный переход? Или для вас операция деления и совершение предельного перехода одно и то же?
В комнате лежало яблоко. Пришёл один Коля и съел целое яблоко. При делении яблока на одного Колю так и получилось одно яблоко после деления на одного Колю.
Яблоко/1 = яблоко.
В комнате лежало яблоко. Но за ним никто не пришёл. Никакого Коли или Пети. Не говоря о нуле, приближениях, хитрых равенствах из института, можно сказать, что деление попросту не состоялось как операция, результат поистине не определён. Как решите — сообщите компилятору о своей философии.
В рамках школьной математики можно просто запретить, в институтской математике ввести 1/0 = бесконечность (если Коли нет, то неподелённое яблоко бесконечное в том смысле, что вообще никому не достанется, останется для Николаев неисчислимо огромным).
Итого с философской точки зрения прав стандарт. То есть результат зависит от цели разработки компилятора, а по факту именно не определён.
Другое дело, что практичнее кидать исключение, дать разработчику управлять ситуацией тоньше.
С точки зрения программирования некорректный результат почти всегда лучше падения программы, и я бы на вашем месте донёс эту точку зрения до детей.
Я работаю с медикал. Вы точно уверены, что хотите получить рандомный результат после, скажем анализа крови, который пошел "не так"?
Всегда лучше падать, кроме случаев когда падать нельзя (в шаттле к примеру), но если нельзя всё равно лучше падать, флаг поднимет резервирование.
А делать вид, что всё ок, когда всё не ок - это годится только для тех, кто зачем-то пишет совершенно ненужные никому программы.
Именно про шатл я и думал. Но да, "почти всегда" тут не корректно, скорее иногда.
И да, в вашем домейне лучше крашнуться. Чтобы не повторилась история по типу Therac-25.
Важно понимать, какой подход нужен в конкретном домейне, а для этого о них нужно знать.
В шатле было специально предусмотрено, что может произойти такое несчастье, что не крешнется, а будет выдавать неверные решения вообще-то. Им было бы гораздо удобнее иметь вариант "или ок, или ничего", а пришлось ставить три компьютера и сравнивать результаты.
В каких-таких распространенных случаях нужно получить неверный результат даже не зная об этом ?
пришлось ставить три компьютера и сравнивать результаты
Это защита от аппаратного сбоя, а не алгоритмического. Если писать некорректные программы, они сразу на всех компьютерах выдадут неверный результат.
Совершенно нет гарантии. Есть куча случаев, когда вы получите неодинаковые результаты на совершенно одинаковом софте и железе. Из-за ошибок в том числе и в софте. Т.е. даже в шатле крайне сомнительно, что все три копии были совершенно идеальными по причинам физического характера, к примеру питание на них подавалось очевидно не одновременно.
В любом случае они были вынуждены построить на случай сбоя довольно сложную систему. Если бы сбой гарантированно приводил к отказу было бы гораздо проще, можно было бы не пять серверов с собой возить, а.... ээээ четыре :).
Неодинаковые результаты возможны. Но от неверного результата \ краша в случае программной ошибки мажоритарное резервирование не спасет, т.к. большинство результатов все равно будет неправильными.
Ну и да, мы вот в театральной отрасли работаем, там “show must go on”. Если скажем сохранение на диск отвалилось то надо продолжать работать хотя бы с той копией спектакля что есть в памяти, а если наоборот связь с механикой сошла с ума то надо дать возможность хотя бы сохранить спектакль на диск. Соответственно программа старается продолжать работать всегда (есть ньюансы насчет безопасности движущихся механизмов, но там обычно ничего программе не доверяют, всё на электрическом уровне блокируется).
Очень странный у вас в примере код. Вам надо либо и числитель тоже от пользователя получать, либо сделать его константой. И будет вам счастье:
$ ./a.out
0
Floating point exception (core dumped)
PS. Так-то понятно, что статья не об этом. И очень даже интересный момент в ней показан.
ЭВМ не авторитет в делах «проверки» ограничений той или иной алгебры. Тут-то и основной подвох.
Продолжая натягивать сову, только на этот раз на глобус Марса, можно утверждать с тем же успехом, что ЭВМ иллюстрирует сокращение x/x под знаком предела, что есть ОК и даже верно, если x → 0. Причем x/x в этих условиях будет резко отличаться от 1/x.
Код реализует извращенную проверку переменной x на ноль. Если значение равно "ноль", то вызывается обработчик исключений. Но в коде обработчик не реализован.
А вот что происходит в Rust

Да, в Rust нет UB при делении на ноль, всегда паника, и даже если значения одинаковы и не известны, компилятор не будет пытаться оптимизировать. black box именно это и делает, прячет значение от компилятора, чтобы он не мог на основе его оптимизировать код, это я сделал чтобы не писать ввод из stdin, он не доступен в rust playground
А если включить полную оптимизацию?
То же самое

https://rust.godbolt.org/z/15KK6P1T8
Флаги сами попереключайте
Если очень надо, есть div_unchecked.
даже если значения одинаковы и не известны, компилятор не будет пытаться оптимизировать
Неправда, отлично оптимизирует:

Разница в том, что компилятор делает это молча
Разве компилятор не создает логи, в которых показывает что он выбросил из кода?
Для тех кто пишет о странности примера, а именно x/x
В реальном коде, такое с высокой вероятностью может быть результатом опечатки. Программист опечатался, компилятор втихаря это убрал, программа стала работать еще загадочнее.
Нарушили правила математики, домножив числитель и знаменатель на х. Допустимо лишь домножать на константы. Затем влезли на поле undefined behavior из области, а где в инструкции стиралки, что нельзя стирать животных. Испытали на детях. Решили не останавливаться, испытали на сообществе.
P.S. Если подходить из принципа модульности, подобные вещи компилятору не нужно вменять в ответственность, иначе обработка таких спорных еджкейсов будет требовать больше кода, чем собственно компиляция. И то, если есть подобная неоднозначность, всем всё равно не угодят. Успокоились математики, прибегут условные физики. Такой булшит это область статического анализа кода, PR или PR с помощью AI и т.п. Там как раз можно и нужно заставлять переделывать такой код.
ПМСМ в примере не ошибка компиляторов, а программиста.
ПРоверка на ноль - его задача именно для расширенного макроассемблера, именуемого Си ))) ... А подход басиков и прочих для сей неприемлем в корне, в определении - это ошибка преподавателя.
Не зная базовой идеологии инструмента развести статью чтоб продемонстрировать навыки лабания кода и не понимая сути? Ужссс
Именно так. Чем-то напоминает подход вида:
try { a=b/c} except { print "division by zero" }
Вместо
if (c!=0) a=b/c else print "division by zero"
Мощно задвинул, внушает.
Хотя, по буквам, то на то )))
А если будет a = b/c +d/e - e/f/g (и конечно все переменные не однобуквенные а как обычно), то по буквам заметно хуже станет. Но по производительности конечно кидать исключение вместо того чтоб проверить ручками только для скриптоязыков годится. С другой стороны, С++ никакого исключения в этом случае и не будет кидать, так что там только проверять и остается.
Обучать программированию примером x / x в оптимизирующем компиляторе? Простите, но это-же типичный случай "не умеешь сам - учи других, не умеешь учить, учи как надо учить!"
Более правильным же примером для обучения детей (и папы) будет:#include <iostream>int main() { double x, y; std::cin >> x; std::cin >> y; std::cout << x / y << std::endl; }
Заодно и выяснится, чем inf отличается от nan и -inf
Пример все равно не идеальный, даже вредный, но для изучения компьютерной арифметики в 3-м классе школы сгодится.
Компилятор сократил x/x — и оказался «прав». Урок математики, который преподал gcc