Comments 72
Большое спасибо за статью. Очень напоминает проблемы strict aliasing, который многие выключают, чтобы не разбираться с этим вот всем. Потеря производительности в обмен на стабильность.
Есть множество статических анализаторов, в том числе и рекламируемых на хабре, которые находят многие случаи UB, плюс динамические вроде ubsan.
Ещё есть опции, запрещающие некоторые оптимизации. Например, -fno-strict-aliasing. Но единого выключателя для UB оптимизаций нет.
Но единого выключателя для UB оптимизаций нет.Его нет, потому что даже выключение всех оптимизаций вообще ничего не может гарантировать. Пример — чуть ниже.
Я хотел показать, что не в оптимизации дело. А в том, что если у вас в программе UB — то, вообще говоря, нельзя говорить о том, что программа должна делать в принципе.
В каких-то частных случаях, для каких-то конкретных UB — да, можно. В общем случае — нельзя.
В моём примере — всё то же самое, только ещё и компиляция с -O0.
То есть, оптимизатору негде “разгуляться”. Он выключен.
Ну нельзя создать компилятор, который будет “правильно” компилировать все программы с UB. Просто нельзя.
Просто потому что для программы с UB нельзя сказать что такое “правильно”.
Вернее, из того, что ни одно из них не срабатывает во время работы (UB могут зависеть от входных данных).
Есть ubsan — он некоторые UB (не все) отлавливает во время исполнения.
Но, вообще говоря, «отлавливать UB» — это задача программиста, не компилятора. Это программист должен обеспечить, чтобы UB отсутствовали.
P.S. Вы же не спрашиваете «как узнать сколько раз компилятор использовал тот факт, что „+“ — это сложение, а „-“ — это вычитание»? А это, наверное, можно посчитать… но зачем? Если программист складывае с помощью оператора „-“ — то это ошибка. Её нужно исправить. И всё. Даже если сегодня ветка, где кто-то использует „-“ для сложения, не вызывается и всё работает — завтра это может измениться и программа работать перестанет. С UB — то же самое.
исходит из простой посылки: UB в программе нет. Ни одного.
Вот тут вы мне и скажите с какого перепуга он решает что нет ни одного UB. Потому что программист сказал: «мамой клянусь нет UB».
Это программист должен обеспечить, чтобы UB отсутствовали.
И как вы себе это представляете. Предположим у вас есть сторонняя библиотека которую писали десятки человек. Что вам надо чтобы убедиться что там нет ниодного UB? Потратить кучу времени и лично перелопатить весь код? Или взять расписку с каждого что там нет UB. И после этого молится что всё заработает как без оптимизации?
Может лучше подобную проверку должен делать компилятор, а не человек.
ps: Я вообще сторонник того что бы можно было явно отключать разные функции языка. Вон не хотите что бы использовался double в коде. Запретили если где-то используется — ошибка. Или сними ограничение или исправляй.
Есть пару примеров простейшего UB. Например выход за границы массива:
cin >> x;
cout << vec[f(x)];
Каким образом компилятор должен узнать есть тут выход за границу или нет? А никак. Поэтому он просто возвращает разыменовывает указатель vec.data() + f(x)
а куда он там указывает это уже забота программиста.
если f(x) чистая, то закэшировать значение идея хорошая. А вот выводить чему она равна из каких то граничных случаев, я слабо представляю как это на практике поможет. Все равно это какие то граничные кейсы на которых сэкономим меньше процента.
Да и строить логику на заведомо ложном предположении это так себе идея. «Данная программа идеальна и не содержит ошибок и работает правильно во всех случаях» — это утверждение не возможно проверить или доказать. Но именно это лежит в основе подобных оптимизаций. Зато можно всё свалить на программистов, мол следите чтоб UB не было нигде (им же больше нечем заняться так ведь).
А в результате получаем что у семи нянек детё без глаза.
И мы вместо локализации UB мы имеем распространение и размножение ошибок.Как я уже сказал: можете сделать лучше — делайте. В конце-концов всё то же самое ведь можно сделать и без всякого UB. В той же самой дорогостоящей компьютерной ошибке в истории никакого оптимизатора и UB не было — а всё вами описанное (ошибка в одном месте распространилась на весь код и всё поломала) — было.
Это, извините, просто свойство программных систем. UB тут ничем не отличается от путаницы между ньютонами и фунт-силами. И ошибки округления могут накапливаться — с очень плачевными последствиями.
«Данная программа идеальна и не содержит ошибок и работает правильно во всех случаях» — это утверждение не возможно проверить или доказать.Во-первых иногда это таки можно доказать. Во-вторых — а как ещё вы можете проводить вообще хоть какие-нибудь преобразования с программой? Да, блин, если вы не знаете что имеет в виду программист когда пишет
+
— то ли сложение, то ли вычитание, а может вообще «логическое или»… как вы при таком подходе вообще что-то собрались порождать на выходе?Зато можно всё свалить на программистов, мол следите чтоб UB не было нигде (им же больше нечем заняться так ведь).Ещё раз: какие вы предлагаете альтернативы.
Ну вот просто:
Дано: Программист намесил какой-то лажи с кучей ошибок.
Надо: Сделать так, чтобы программа работала без ошибок.
Ну вот как вы себе представляете решение этой проблемы? Конструктивно? Нет, возможно когда-нибудь “сильный AI” и научится это делать. Только вот следующим шагом будет просто увольнение всех программистов, потому что пустая программа — частный случай программы с ошибками, а если компилятор из такой программы порождает правльный выхлоп… то нафига нужен программист?
Вещь, которую вы, почему-то отказываетесь понять, на самом деле проста: некоторые UB обработать иначе, чем они обрабатывают невозможно — и потому, для простоты все UB обрабатываются тем же способом.
Хотите чтобы часть UB перестали быть UB, а превратились в unspecified behavior или implementation-defined behavior? Вперёд: пишите proposal, стандарт меняется, за ним меняется компилятор.
Вы же, на самом деле, желая получить в некоторых местах вместо undefined behavior либо unspecified behavior, либо implementation-defined behavior вместо соотвествующих proposal'ов мечете громы и молнии на форумах, плачетесь в списках рассылки и так далее. В общем делаете всё, что угодно — только не то, что нужно делать.
(ошибка в одном месте распространилась на весь код и всё поломала). Это, извините, просто свойство программных систем.
Это свойство любых систем. Но и с этим можно справляться. Давно уже строят надёжные системы из мене надёжных подсистем. Для этого делают дублирующие системы которые должны работать на отличающихся принципах, независимо, по разным алгоритмам или хотя бы написанных разными группами.
Вещь, которую вы, почему-то отказываетесь понять, на самом деле проста: некоторые UB обработать иначе, чем они обрабатывают невозможно — и потому, для простоты все UB обрабатываются тем же способом.
Вообще ничего не понял.
Я просто хочу что бы компилятор не молчал когда видит UB и особенно когда на его основе меняет код.
Я просто хочу что бы компилятор не молчал когда видит UBВы хотите на каждую строку получить 20-30 строк диагностики? Ну кроме строк совсем без кода?
Вот возьмём такую ужасную функцию с тремя целочисленными массивами:
void foo(int a[], int b[], int c[]) {
a[0] = b[0] + c[0];
}
Тут у вас примерно десяток потенциальных UB. b
и c
могут оказаться неинициализированными (это UB), результат сложения может не влезть в int
, указатели могут указывать в неаллоцированную или освобождённую память (это UB), одна и та же память может быть разmmap
ела по разным адресам (это UB), по адресам b[0]
и c[0]
может данных не оказаться (это другое UB, не то, что с неинициализированными a
и b
) и так далее (там ешё много чего можно вспомнить… в частности проблемы алиасинга, обсуждаемые в статье тоже могут на такую функцию повлиять).Вы дейсвительно хотите пользоваться компилятором, который на каждую подобную строчку будет писать портянку на три экрана? И на почти любую другую?
Компилятор видит то, что в вашей программе есть. Это относительно немного (столько, сколько вы написали). А вот то чего в ней нет (но могло бы быть, в зависимости от того, что в других частях кода происходит)… ооо… там много-много-много чего может быть.
и особенно когда на его основе меняет код.А он не меняет код “на основе”. Он просто предполагает, что у UB нет — и всё.
Нигде в компиляторе нет какого-либо учёта UB. Это абсурд. Как программист не думает над тем, что будет если
a
и b
будут разными указателями, указывающими на одно место в памяти (потому что он такого ужаса не писал… хотя используемые ими библиотеки вполне могут такое сотворить… кто ж им запретит?), так и компилятор об этом “не думает” — потому что программист ему это пообещал.При этом большинство UB программист старательно изводит (например если два разных потока работают с этими массивами без синхронизации — вы же не будете требовать, чтобы программа работала?), но вот некоторые — почему-то не считает своим долгом править.
И как разрабочики компилятора должны “отделять зёрна от плевел”, извините? Какие UB считать “доброкачественными” (которые программист “изведёт” без истерик, а если нарвётся — будет винить себя… скажем обращение к освобождённой памяти) от “плохих” (которые программист считает возможным оставить в программе, а когда компилятор их “поиспользует” — у него случится истерика… скажем челочисленные переполнения и обращения к одному участку памяти то как к
int
, то как к unsigned int
)?Программисты ж разные: кто-то считаете, что если уж записали
int
, то и читать надо int
(как, собственно, и говорит стандарт), а кто-то, считает, что int
и unsigned int
— это ж почти одно и то же, почему вдруг нельзя записать unsigned int
, а прочитать int
… я ж знаю, что в моей программе переполнения не будет).Потому компиляторы (разработчики компиляторов, на самом деле) исходят из простого подхода: всё что стандарт называет UB (обращение к неинициализированной переменной, обращение к невыделенной или удалённой памяти, обращение к одному неатомарному объекту из двух потоков без специальных конструкций и так далее и тому подобное) — обрабатывается одинаково: считается что программист их извёл и всё. Иногда (там где программист явно написал какую-то лажу) выдаются предупреждения, но на все UB их выдавать нельзя, вы таким компилятором пользоваться не сможете.
Для самых популярных UB (которые стандарт называет UB, но которые програамисты не склонны считать UB) есть ubsan, но он, конечно, всё покрыть не может (в частности много раз упомянутый выше трюк с
mmap
он не отловит).Давно уже строят надёжные системы из мене надёжных подсистем. Для этого делают дублирующие системы которые должны работать на отличающихся принципах, независимо, по разным алгоритмам или хотя бы написанных разными группами.И как это относится к компилятору? Предлагаете компилировать программы 3-4 раза и выбирать реализацию функции случайным образом? Так они все на основе одного стандарта написаны, так что список UB, в частности, у них совпадает.
Тут программисту нужно писать приограмму на нескольких языках, извините. То есть всё вернулось туда, откуда начали…
Не вижу никакого смысла писать соответствующие proposal.Ну не хотите, так не хотите, дело ваше, тогда будем считать, что имеющийся список UB вас устраивает. Другие, кстати, подобные proposal'ы писали — при должной агруменации они вполне могут быть приняты. Вот например или вот.
Конечно вопля “я тут написал программу с хитрым трюком, а сволочь-компилятор её испортил” будет недостаточно: вам придётся описать влияние вашего изменения на экосистему в целом.
Может это вы называете “слишком забористой травой”?
Но это вот — реальный способ что-то изменить. А плакать на форумах по поводу козней компиляторщиков — нереальный. Как бы вам ни хотелось обратного.
Тогда придется жить без оптимизаций. Или без определенного вида оптимизаций. Но локализировать не получится полностью, записал на стек в адрес возврата что угодно и программа выполнила что угодно. Так уязвимости и работают. У тут даже с -O0 можно компилировать.
Но локализировать не получится полностью, записал на стек в адрес возврата что угодно и программа выполнила что угодно.Все современные компиляторы это отлавливают.
Написание безопасного кода — отдельное дело. Слабо связанное с истериками отдельных разработчиков, которые истошно вопят, что им разработчики компиляторов обязаны “взять и обеспечить”… а если сделать это не позволяют законы математики и физики… то и пофиг: пусть меняют математику. Сложно, что ли? Пусть Евросоюз или Дума новый закон примет…
У тут даже с -O0 можно компилировать.Я, собственно, это давным-давно продемонстировал.
Есть флаги которые выдают дамп после каждого прохода.
Например на хабре было для llvm описание. Т.е. втихаря никто ничего не делает, просто после десятка проходов всё равно у вас не получится так просто найти концы. Надо сидеть и разбиратся.
Апд. продолжил мысль выше
Кстати нафига вообще может понадобится «aggressive-loop-optimization» я как то не могу придумать.Позволяет избавиться от циклов. Например если цикл у вас имеет размер один и вы обращаетесь к его элементам, то ясно, что вы обратитесь только к нулевому элементу и цикл выполнится один раз.
Не понятно всё равно. Мы не можем узнать размер цикла по условию внутри for в компайл-тайм (если бы могли то думать не надо). Но каким то образом по телу цикла делаем вывод о его размере, и выводим что он размера один. Такой код имхо уже подозрительный.
Или я не правильно понял как оно работает? Т.е. есть вариант когда эта оптимизация "ошибочно" сработает (т.е. сработает для программы с UB) и при этом не выкинет warning?
Апд. наверное это имеет смысл для циклов с break… хм...
Но каким то образом по телу цикла делаем вывод о его размере, и выводим что он размера один.Не по “телу цикла”. По размеру массива! Если у нас переменная — это массив (а не указатель), и у неё размер массива равен единице, то мы точно знаем, что обращение будет только к нулевому элементу — и ни к какому другому.
Т.е. есть вариант когда эта оптимизация «ошибочно» сработает (т.е. сработает для программы с UB) и при этом не выкинет warning?Ах, это. Я не знаю, может ли эта оптимизация сработать с UB и без warning'а, если честно. Да это и неинтересно.
Я описывал когда она срабатывает в программе без UB и работает “как задумано”.
Потому что программист сказал: «мамой клянусь нет UB».Нет.
Вот тут вы мне и скажите с какого перепуга он решает что нет ни одного UB.Потому что если в программе случается UB, то любое поведение программы, вот совершенно любое, считается допустимым:
if any such execution contains an undefined operation, this document places no requirement on the implementation executing that program with that input (not even with regard to operations preceding the first undefined operation)
Замечание в скобках — это тоже часть стандарта, не моё изобретение.
Может лучше подобную проверку должен делать компилятор, а не человек.Как только вы создадите акой компилятор — можно будет о чём-то рассуждать.
Пока что желающих пользоваться такими компиляторами — наблюдается в количестве, желающих такое чудо создавать — нуль.
ps: Я вообще сторонник того что бы можно было явно отключать разные функции языка.А я вообще сторонник того, чтобы можно было написать «сделайте мне клёво» и компилятор у меня в мыслях бы прочитал чего я хочу. Ну сделайте, а? Чего вам стоит?
Правда же заключается в том, что создавать программы без UB — возможно. Сложно, но возможно. А создать компилятор, который бы «правильно компилировал» любые программы без UB — невозможно. Просто потому что у разных людей разные мнения на тему «что такое правильная компиляция программы с UB».
Даже компиляция с
-O0
ничего не гарантирует! Потому что, например, если взять какой-нибудь старенький компилятор, то будет что-нибудь типа такого работать:int foo() {
int i;
i = 42;
}
it bar() {
int i;
return i;
}
int main() {
foo();
printf("%d\n", bar());
}
А в clang это не работает.То, чего вы, на самом деле, хотите — это телепатический модуль, который будет у вас в голове читать список использованных вами трюков (и оставлять их в покое), но при этом будет оптимизировать всё остальное.
Было бы круто, да, но разработчики компьютеров подкачали: модуль телепатии туда пока не встраивают.
Потому разработчикам компиляторов, увы, приходится довольствоваться стандартом.
return i;
в конце функции foo, всё работает в т.ч. и в шланге.Верно, потому что невозврат значения из не-void функции — это UB.
Но, если в foo добавить return, функция bar возвращает 42, которого там близко не было. Вот это уже действительно UB «на усмотрение компилятора».
Невойд функция без return вообще не должна компилироваться, это явно некорректная программа
Такие простые случаи действительно могут детектиться и вызывать ошибку компиляции. Но в общем случае задетектить выход из функции без return-а невозможно.
Но так не сделали. И в C (но не в C++!) вызывать функцию без return ещё и вполне законно (а вот использовать возвращаемое ей значение — таки нет).
P.S. Современные компиляторы, кстати, позволяют это запретить: gcc, clang. Но это не «поведение по умолчанию», потому как стандарт это таки разрешает.
Странно, что такое вообще пропускается компиляторами.Ничего странного. Дело в том, что
void
— это относительно “недавнее” изобреение (ну как недавнее… C89… C к моменту его появления уже больше 10 лет существовал и использовался).До того —
void
не существовал, а функции, ничего не возращавшие описывались как int
… но в них можно было не делать return
.И да, ради совместимости этот стиль поддерживается в C и сегодня. Даже в C17.
Зачем тогда вообще завозили [[noreturn]]?
Просто стандартами занимаются бюрократы из крупных корпораций, которые любой чих оценивают с позиции «а сколько это нам будет стоить».
Ни один современный компилятор не скомпилит ни один пример из оригинального K&R, где всё набрано КАПСОМНикогда такого не было. C был с момента создания Case-sensitive, чай не Pascal. Вот тут есть примеры 1972го года — нет там никакого КАПСА. Это ещё сильно до соответствующей книжки (она в 1978м вышла).
Может быть на каких-нибудь советских ЭВМ где были большие русские и латинские, а маленьких букв не было? Не знаю. У AT&T никогда никакого КАПСА не было…
типы аргументов функции описаны отдельно, между () и {}, и по умолчанию все переменные считаются int.Ну да. Вот же:
Clang 11 — прекрасно
Gcc 10 — работает
Icc 19 — без проблем
MSVC 19 — замечательно.
Так эта… о каком компиляторе речь-то?
Зачем тогда вообще завозили [[noreturn]]?Он тут причём???
Просто стандартами занимаются бюрократы из крупных корпораций, которые любой чих оценивают с позиции «а сколько это нам будет стоить».Если бы. Нет, увы. Критерии там совсем другие. Иначе бы не занимало столько времени ни продвижение корутин, ни многие другие вещи. Но это уже другая история.
P.S. GCC, кстати, очень долгое время только на K&R вариант рассчтан был. Только версия 3.4, уже в XXI веке, позволила себе отказаться поддерживать старые компиляторы и стала требовать ANSI C. C89, конечно.
FIZZBUZZ()
{
FOR (I = 0; I < 100; ++I) {
IF (I%3 == 0) PRINTF("FIZZ");
IF (I%5 == 0) PRINTF("BUZZ");
}
RETURN I;
}
Не знал, что компиляторы до сих пор поддерживают древний синтаксис с вынесенным описанием аргументов.
Ну, речь в любом случае о С++, нахрена это г… но мамонтов тащить туда?
Гуглите кодировку RADIX-50, она например была стандартной в PDP-11.В PDP-11 она не могла быть стандартной, так как это зависело от ахитектуры операционки. Может вы про RSX-11 или RT-11? Так они какое отношение к C имеют? С был в Unix разработан, а Unix унаследовал ASCII от Multics. Так что там маленькие буквы были всегда.
Ну, речь в любом случае о С++, нахрена это г… но мамонтов тащить туда?Речь в данном случае о программах с UB. Просто на C программы проще и показать как можно “самому себе насрать в карман” тоже проще.
Гуглите кодировку RADIX-50, она например была стандартной в PDP-11.
RADIX-50 использовалась для имён файлов. В код программы её, естественно, никто не тащил, да и не мог: кодировка, в которой нет никаких скобок, не подходит ни для одного традиционного языка программирования (даже для Forth). Поэтому её упоминание в данном контексте совершенно неуместно.
Но вот большие буквы в коде на C — факт… я ещё школьником видел это на ДВК и мы даже что-то пытались на этом писать :) хотя цирком было, что в KOI-7H2 {} превращались в ШЩ, ~ становилась Э, ну и так далее… программы конечно читались и писались, но с диким акцентом...
Речь о том, что возможность компиляции функций без return это явный баг в стандарте языка.
Зачем вообще городить систему типов, если функция может вернуть любой мусор, оставшийся в аккумуляторе?
Речь о том, что возможность компиляции функций без return это явный баг в стандарте языка.Ну почему же баг-то? Фича. В C++, кстати,
return
обязателен, но там тоже есть одно маленькое исключение: If control flows off the end of the compound-statement of main
, the effect is equivalent to a return
with operand 0
.В C оно не нужно, так как значение же
mail
никто внутри программы не использует — а значит возращать значение не обязательно…Зачем вообще городить систему типов, если функция может вернуть любой мусор, оставшийся в аккумуляторе?Затем, что C (и C++) — небезопасные языки, предназначенные для написания безопасных программ.
Потому UB — это ограничения, налагаемые не на компилятор, но на программиста.
Это программист обязан обеспечить, чтобы “любой мусор, оставшийся в аккумеляторе” не влиял на поведение программы. Это программист должен озаботиться тем, чтобы к объекам не быдо доступа после вызова
operator new
. Это программист должен гарантировать, что вы не пытаетесь передать куда-нибудь “пустую обёртку” из под объекта, содержимое которого “уехало” куда-то.Хотите чего-то другого? К вашим услугам масса разных других языков. Java, Rust, да хоть Visual Basic… А C и C++ — они вот такие вот. Тут вы в ответе за всё (ну не совсем за всё уж… но очень много вещей, за которыми, как кажется, мог бы следить и компилятор… в C и C++ следит именно программист).
Но С++ претендует на большее. Вы не можете сказать, что делает f(42), не зная контекста. Это может быть что угодно, от инициализации переменной до вызова f.operator().
Сегодня уже никому не нужны «просто программы». Всем нужны корректные программы. Блин, сегодня рекламщики пишут свои анальные зонды на тупоскрипте со строгим контролем типов. А в С++, на котором пишут прошивки для, например, блока управления тормозами автомобиля, функция всё её может вернуть неинициализированную переменную. Охренеть, блин. И почему это Rust и Go такими темпами взлетают?
И почему это Rust и Go такими темпами взлетают?Вы имеете в виду почему при таком количестве хайпа Go до сих пор в десятку не попал, а Rust едва-едва в тридцатке? Притом, что обоим уже больше 10 лет?
Блин, сегодня рекламщики пишут свои анальные зонды на тупоскрипте со строгим контролем типов.Что не мешает им порождать кучи глючащего и тормозного дерьма.
Да, за счёт обилия UB писать на C и C++ не так просто, как том же тупоскрипте. Но вызов UB в программе — это плюс-минус такая же логическая ошибка, как и использование знака „+“ вместо „-“. А уж эту-то ошибку никакая «система типизации» не ловит (за исключением выроденных, спецально написанных примеров).
У меня нет претензий к Си, это такой «компилируемый в уме макро-ассемблер».Ну какой это это макроассемблер? Пример kovsergа там работает точно так же, как в C++.
И вот не надо тут про проблему останова, любая функция имеет конечный размер. Также компилятор знает особые случаи, когда функция в принципе не возвращается (именно благодаря [[noreturn]]).
В общем случае это не так просто. Возьмем следующий код:
int foo(int arg)
{
if (arg) {
return 0;
}
bar(); /* Определена в другом модуле, всегда вызывает exit() */
}
[[noreturn]] тут не подходит, но и UB здесь нет, ибо flowing off the end of a function иначе чем через return никогда не происходит. Писать лишний return для красоты, лишь бы компилятор был доволен и сгенерировал код, который никогда не будет вызван? Ну такое — даже в наш век Electron'а и прочего мусора существуют области, где борьба до сих пор идет в буквальном смысле за каждый байт и за каждый такт. Будет предупреждение от компилятора, но его можно выключить или проигнорировать, и получить вполне корректный ad hoc код.
Зачем вообще городить систему типов, если функция может вернуть любой мусор, оставшийся в аккумуляторе?
Она и так может его вернуть, пожалуйста:
int foo()
{
int i;
return i;
}
Это как бы немного ортогонально системе типов. Заставлять всегда инициализировать все переменные фундаментальных типов при их объявлении, даже когда существует вероятность, что они потом не будут использованы? Отрицательно скажется на производительности и поломает обратную совместимость. В C и C++ принцип «ты не платишь за то, что не используешь» вообще является чуть ли не краеугольным камнем.
У вас в ABI не пролезает специальное значение для undefined? У процессора нет флагов, которые можно было бы использовать для индикации ошибок? Ну так используйте ещё один регистр или выбросьте этот процессор на свалку. Почему ABI в MS-DOS и CP/M семьдесят лохматого года ширше и лучше, чем в линупсе 2021-го? Машины, в которых nullptr имел ненулевое значение, уже полвека не используются, но мы продолжаем тащить груз совместимости — а вдруг кто-нибудь захочет запустить на них браузер. (ну да, я знаю, что в С++17 чтоле nullptr таки приравняли ((void*)0) )
Равно как и undefined для неинициализированных переменных.
Только оно в них тоже не из воздуха появляется.
выбросьте этот процессор на свалку
Отлично, отлично. Ладно, пошел разговор ни о чем, «за все хорошее и против всего плохого, за мир во всем мире» :) Бывает, что я и в таких участвую, но сегодня я лучше книжку почитаю, извините :)
Только оно в них тоже не из воздуха появляется.
Вы таки удивитесь, насколько это дёшево реализуется. Грубо говоря, это один из NaN. Т.е. двоичный вид 0x7fffffffffffxyz0 на 64 битах или 0x7fffxyz0 на 32
Вы таки удивитесь, насколько это дёшево реализуется.Web-сайты почему тормозят? Если всё это так просто и быстро?
В Javascript (Javascript, Карл!) завезли let для объявления переменных блочного уровня видимости.И как? Помогло? Программы в браузерах перестали глючить и тормозить, да? Вот тут у людей в соседней статье другое мнение.
Почему ABI в MS-DOS и CP/M семьдесят лохматого года ширше и лучше, чем в линупсе 2021-го?Что значит «ширше и лучше»? Файл размеров 8GiB они открыть могут хотя бы?
ABI MS-DOS или CP/M компактнее, да… но «ширше и лучше»… я бы помолчал.
Ну т.е. понятно что в 1972 флаги были не на всех архитектурах, да и регистров могло быть раз, два, расчёт окончен, но мы до сих пор продолжаем с этим жить.
А веб тормозит из-за повсеместной моды на функциональщину и иммутабельность. Если написать на плюсах программу, которая на каждое движение мышкой делает полную копию своей памяти вместо исправления пары байтов по месту, тормозить будет ничуть не меньше.
В Си/UNIX для этого есть специальная статическая глобальная переменная errno, общая для всех вызовов, что особенно удобно в асинхронных, корутинных и прочих пакетно-неблокирующих программах.Вы уж определитесь о чём вы говорите. В прошлый раз вы про Linux говорили. То бишь ядро. В ядре нет никакой глобальной переменной
errno
. А с системной библиотеке она thread-local.Ну т.е. понятно что в 1972 флаги были не на всех архитектурах, да и регистров могло быть раз, два, расчёт окончен, но мы до сих пор продолжаем с этим жить.В современных MIPS и RISC-V флагов тоже нет, кстати.
Если написать на плюсах программу, которая на каждое движение мышкой делает полную копию своей памяти вместо исправления пары байтов по месту, тормозить будет ничуть не меньше.Ну а кто просит всё копировать-то? Haskell вполне себе добивается скорости, сравнимой с C/C++ не отказываясь от иммутабельности.
Просто нужно программы писать используя мозги, а не генератор случайных чисел.
Сейчас же работает не принцип о "преждевременной оптимизации", а принцип о "преждевременном использовании мозгов" :)
А сама статья посвящена, внезапно, тому как “разворачивать циклы” (руками, так как компиляторы в те времена этого делать не умели).
Сейчас же под этот “соус” протаскивают решения, которые в десять, сто, тысячу раз менее эффективны, чем оптимальные.
Нет, Кнут это не имел в виду, когда говорил о том, что иногда лучше оставить цикл просто циклом.
Речь о том, что возможность компиляции функций без return это явный баг в стандарте языка.
Вот пример валидной программы на C++:
/*[[noreturn]]*/ void abort_programm();
struct A
{
static A make();
private:
A() = default;
};
template<typename T>
T create_object(std::function<T()> maker)
{
if (maker)
return maker();
abort_programm();
}
int test()
{
A a = create_object<A>(&A::make);
}
Компилятор не видит что делает функция abort_programm
, она может вызвать exit
, abort
, кинуть исключение или еще что-то т.е. прервать поток выполнения и не возвратить управление вызывающей стороне и в этом случае программа валидная. А может и возвратить, тогда это UB. Мы не можем здесь написать ни какой return, потому что не знаем как создать объект. Максимум что может сделать компилятор — это предупредить и обратить внимание на потенциальную проблему, сгенерировав предупреждение.
control reaches end of non-void function [-Wreturn-type]
Если мы правильно реализовали функцию abort_programm
, то это предупреждение будет ложным. Эта причина существования [[noreturn]]. Если функция будет помечена таким атрибутом, то даже без знания определения, компилятор может сделать вывод о том что все в порядке и подавить генерацию ложного предупреждение.
Помечать валидную программу ошибкой по умолчанию — не правильно.
throw std::bad_function_call{};/* will never happen */
И это в 100500 раз лучше, чем где-то там в доках писать километры комментариев, почему так.
Это не эквивалентная замена.
Во-первых, исключения могут быть отключены.
Во-вторых, abort_programm
может нести дополнительную нагрузку и выполнять полезную работу до завершения выполнения.
В-третьих, я могу объявить функции с noexpect, например
а [[noreturn]]
пропагируется наверх?
[[noreturn]] void abort(); // real abort from system
void abort_program() {
cout << "пока\n";
abort();
}
знает ли компилятор что abort_program
тоже noreturn?
Небольшие функции современные компилторы могут встраивать даже если они не описаны явно как
inline
.Получается пометить надо самому. Но если пометить функцию которая не вызывает std::exit
то компилятор сгенерирует варнинг. Т.е оно на уровне аннотаций работает аналогично как и в расте боттом на уровне типов. Теперь мне интересно как определить саму std::exit
без варнинга.
If the } that terminates a function is reached, and the value of the function call is used by the caller, the behavior is undefined.
В C++ — там таки да, оговорку про использование возвращаемого значения убрали:
Flowing off the end of a constructor, a destructor, or a non-coroutine function with a cv void return type is equivalent to a return with no operand. Otherwise, flowing off the end of a function other than main (6.9.3.1) or a coroutine (9.5.4) results in undefined behavior.
Т.е. если компилировать вышеприведенный код C компилятором, то foo() и ее вызов написаны вполне корректно, никакого UB нет. UB только в bar().
А вот написание такой функции — вполне корректно.
P.S. Обратите внимание, что пример таки на C, не C++.
Спасибо за такой подробный обзор, наконец-то удалось что-то понять!
Сейчас пробуем добавить -fstrict-vtable-pointers, -fwhole-program-vtables при сборке Clickhouse:
https://github.com/ClickHouse/ClickHouse/pull/20151
Но что-то пошло не так и lld-11 упал при линковке (из-за LTO): https://clickhouse-builds.s3.yandex.net/20151/7b3481322edb4ed1f0f1f76128c3283d87fd4fbf/clickhouse_build_check/build_log_889491481_1612624311.txt
то есть в стандарт потихоньку протаскиваются вещи, более удобные для конкретного компилятора?
C++17. Функция стандартной библиотеки std::launder и задача девиртуализации