Pull to refresh

Comments 11

«В основном, реальная проблема в том, что С не является безопасным языком и (несмотря на его успешность и популярность) многие люди не понимают, как язык на самом деле работает.» — и уж совсем единицы осознают вышесказанное во всей полноте.

Спасибо за отличный цикл статей.
Глядя на все эти примеры с выполнением 'rm -rf /', несработкой if (a>(a+1)) и прочими UB, почему-то хочется сказать, что авторы clang/llvm соблюдают букву, но не дух стандарта. Согласно букве, можно каждый раз делать 'rm -rf /'. Однако каждое из UB введено в стандарт не просто так. Например запрет сдвига больше чем на 31 обоснован тем, что некоторые процессоры умеют сдвигать не более чем на 31 бит и игнорируют старшие биты числа сдвигов, другие — сдвигают вплоть до 63 бит, обнуляя результат в случаях сдвига на 32..63 бита и т.д. if (a>(a+1)) может не сработать, если процессор использует ones' complement (https://en.wikipedia.org/wiki/Ones'_complement) для представления чисел со знаком (например, PDP-10) или если процессор аппаратно сгенерирует exception при переполнении при сложении (так умеют делать процессоры MIPS) и т.д. Однако, шланг нам говорит — ага, UB! Теперь я делаю что хочу и плевать на программиста!
Всё это делает не clang, это делает opt, и вы можете отключить любой из проходов оптимизации. Или написать свой. По умолчанию всё работает так, как работает, и здесь бессмысленно возмущаться, потому что стандарты и алгоритмы оптимизации от этого не изменятся.
Однако каждое из UB введено в стандарт не просто так.
Совершенно верно. UB введены для в язык не просто так, а для того, чтобы программы были переносимыми. И потому на программиста (а вовсе не на компилятор) налагается требование: UB в программе — не вызывать.

А clang — что clang? Он просто полагается на то, что умный, добрый, хороший программист эти правила игры соблюдает — ни больше, ни меньше.

О каком «духе» и о какой «букве» может идти речь, если стандарт особо и специально подчёркивает, что появление UB в программе — позволяет делать что угодно: However, if any such execution contains an undefined operation, this International Standard places no requirement on the implementation executing that program with that input (not even with regard to operations preceding the first undefined operation).

Как вы думаете — стали бы они писать, что выполнение даже операций предшествующих первому появлению UB не гарантируется — если бы «дух» был не тем, каким его понимает clang?

Наоборот — стандарт специально подчёркивает что UB — это всё, конец, приплыли, может быть что угодно и когда угодно!

Другое дело, что многие пользователи C/C++ ожидают чего-то другого… ну так тут как с той микроволновкой: написано «кошек не сушить» — написано. Чего вы теперь хотите, чёрт побери?
UFO just landed and posted this here
Ну если говорить именно о «железках», то
1. MIPS, Alpha, RISC-V: поля condition codes нет вообще, соответственно, флаг о переполнении не ставится, остаются только младшие 32 или 64 бита (но см. ниже про MIPS). Вообще, стандартные condition codes считаются сейчас наследием CISC, в то время, как новые разработки (с конца 80-х) почти повально RISC, если не что-то более хитрое (IA-64 и NVPTX, например, тоже не имеют condition codes, у них регистры-предикаты — однобитовые булевские значения, ставятся явным сравнением, с ними можно совершать стандартные операции и применять в условных переходах).
LLVM IR тоже спроектирован под RISC-модель (точнее, он ближе даже к подходу с предикатами). Есть встроенные функции вроде sadd_with_overflow, которые явно проверяют переполнение, но обычный код на C/C++ их не зовёт (есть GCC builtins для этого, Clang их поддерживает). Но они не везде хорошо сделаны (для x86 — да, а вот для SystemZ, например, их выхлоп, мягко говоря, ужасен).
2. MIPS, тем не менее, имеет различие, например, между add и addu; второе не проверяет никакого переполнения (даже беззнакового — но оно очень легко проверяется через sltu), а первое — вызывает исключение при знаковом переполнении. Некоторые компиляторы генерируют такое при операциях с signed int, так что при переполнении программа может вылететь без возможности восстановления. А вот возможность проверить факт знакового переполнения без генерации исключения на нём громоздко (или битовые манипуляции, или условная развилка).

Напрямую на обсуждаемое тут с переполнениями это не влияет, но «не как на x86» в полный рост.

Вот чего уже давно не видится нигде — так это иного представления отрицательных целых, чем в дополнительном коде (английский жаргон — twoʼs complement). Вообще не уверен, что после 60-х годов сохранились живые компы с другим подходом. LLVM требует представление в дополнительном коде от платформы реализации; JVM, Go явно его указывают в своих спецификациях. C, C++ по старинке допускают альтернативы, но уже давно непонятно, зачем.
UFO just landed and posted this here
Почему в принципе есть возможность успешно скомпилировать программу обращающуюся к неинициализированной переменной на чтение хоть с какими-то настройками компилятора? Ну и тот же вопрос про весь класс подобных «простых» UB.
Я понимаю, что это практически вопрос почему C++ не C#, но какая польза в том, чтобы C++ не был в этом аспекте Си-шарпом? Какой смысл начинать делать оптимизации, если в первую очередь здесь не известно какой должен быть результат?
Какой смысл начинать делать оптимизации, если в первую очередь здесь не известно какой должен быть результат?
Почему неизвестно? Известно: чтобы любая программа, которая не вызывает UB продолжана работать правильно.

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

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

Потому что в общем случае это невозможно диагностировать.
Пример:
int i;
if (some_runtime_dependent_condition()) {
    cin >> i;
}
if (another_runtime_dependent_condition()) {
  cout << i;
}

Мы административно гарантируем, что
another_runtime_dependent_condition()
равна true только когда
some_runtime_dependent_condition()
тоже равна true (это гарантируется, скажем, архитектурой).

Так что все эти «простые» UB на деле не диагностируемые. А в тех частных случаях, когда UB очевидно, компиляторы действительно пытаются сообщить юзеру. По крайней мере msvc с дефолтными настройками отказывается компилировать, если видит очевидное чтение неинициализированной переменной или выход из функции без return.
C# в данном случае справляется: он говорит, что инициализация переменной возможно не выполнится (т.к. она под if-ом с не константным условием и в else её тоже не производят) и требует проинициализировать переменную при всех возможных путях выполнения. Это заставит меня сделать одно из двух: либо сразу написать int i = 0, либо занести второй if под первый. В данном случае я скорее сделаю второе. Это никак не повлияет на производительность, но теперь этот код будет отражать логические отношения между кондишенами. И даже в том случае, если они изменятся код останется валидным и без UB.

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

А рассуждения, что вот поэтому С++ может так хорошо оптимизировать инструкции, как-то сомнительно звучат. Было бы больше структурных проверок на этапе анализа — можно было бы больше предположений доказать на этапе компиляции, можно было бы выкинуть больше проверок…
Sign up to leave a comment.

Articles