Pull to refresh

Comments 42

У меня просьба: проверте, как компилятор обработает это ((-1)^2)^(1/2)=?.

Результат вас удивит. В C-подобных языках ^ – это xor, выполняемый над целыми числами, так что это будет (-3)^0, т.е. -3.

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

А откуда ошибка-то? Квадратный корень из единицы не извлечь?

А с чего вы взяли, что там квадратный корень?🙂 И я даже не про ^.

В Си (1/2) = 0, потому что вы целое делите на целое, то есть это целочисленное деление.

Вот если бы вы записали (1.0/2) или (1/2.0), тогда было бы 0.5 😉

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

А, да, действительно, был невнимателен и решил, что товарищ выше имел в виду sqr(sqrt(-1)), а не наоборот.

Математически запись \sqrt{x^2} эквивалентна abs(x) и часто используется именно с этой целью. Мат.пакеты подобное сокращают только при явном указании x>0.

Вы забыли добавить эту статью для наброса на вентилятор. Запасаюсь попкорном :)

Не абстрактное правило из учебника

Просто этому правилу научили и компьютер, не более того. Вот если бы разбиралась физические процессы аналогичны делению и там тоже оказалось, что на 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

Воттераз, как раз на границу попал!

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

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

Rust такой фигнёй не страдает

Математическая ошибка в рассуждении компилятора

В алгебре существует фундаментальное правило:

Сокращение x в числителе и знаменателе допустимо тогда и только тогда, когда явно оговорено, что x ≠ 0.

Что? Если в знаменателе стоит x то он не может быть нулем, следовательно мы всегда можем сокращать на x. Иначе, все выражение не имеет смысла.

вам никогда не встречались неопределенности вида "0/0"?

но вообще не понятно почему деление на ноль нужно демонстрировать через x/x, а не x/0

Встречались, только при чем тут они?

Не, ну так-то деление на ноль на x86 красивее всего демонстрировать, когда ни числитель, ни знаменатель не 0: инструкция div процессора делит 32-битное число на 16-битное, и если результат не влез в 16 бит – это считается делением на 0 :-)

sin(x)/x

Может, пока ОДЗ явно не определена так, чтобы исключать 0 в знаменателе. Что иными словами так и читается «когда явно оговорено, что знаменатель не обращается в ноль»

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

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

И не умер молодым

Нужно показать детям, что деление на ноль — это ошибка.

Это не ошибка, а "бессмысленная" операция, для которой в формальной системе не определена операция обратная осмысленная.

А в некоторых местах вне элементарной алгебры делить можно. Разрешаю и вашим ученикам.

Дети видят — компьютер не может делить на ноль, программа аварийно завершается.

С тем же успехом можно объяснять детям, что ветер дует, потому что деревья качаются.

Математика устроена не так, как считает правильным железо/компилятор. Это железо/компилятор устроены так, как выгодно математике, догадываетесь?

Причем, преимущественно с инженерной точки зрения.

Если вы хотели показать, что нельзя делить на 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.

Ну, он мог и ноль вывести, на что ноль ни Дели, получится всегда ноль

Только в арифметике нельзя делить на ноль. В алгебре иногда можно.

По моему компилятор правильно сделал.

0/0 = 1

Компилятор имеет полное право заменить выражение x/x = 1.

А деление на 0 это неопределенное поведение. Не нужно бросать всё в 1 ящик.

Выражение a/0 не оптимизируется. По этому ваш пример неверен.

Вторая часть статьи будет? Когда внезапно окажется, что во float/double делить на ноль вполне себе можно и получится плюс или минус бесконечность, в зависимости от знака делимого. А потом от от этой бесконечности можно взять например арктангенс - и получить 1.5707... без всяких пределов.

Sign up to leave a comment.

Articles