Макс Казанцев @xortator
Разработчик компиляторов
Information
- Rating
- Does not participate
- Location
- Новосибирск, Новосибирская обл., Россия
- Registered
- Activity
Specialization
System Software Engineer, Software Performance Engineer
Разработчик компиляторов
Пожалуйста, перестаньте писать мне, что "курощение" -- это опечатка. Это не опечатка, а отсылка к Карлсону. Стыдно классику не знать, товарищи! :)
У джавы в числодробилках (т.е. когда в бенче не создаются объекты, не выделяется активно память и не собирается gc и т.п.) нет никаких других дополнительных проблем. Перфоманс у неё будет плюс-минус как у раста (если речь о Зинге используется одна версия LLVM)
Примеры на расте строятся банально (достаточно заюзать что угодно с UB). Ну например:
С++: https://godbolt.org/z/z6dxvrE13
Rust: https://godbolt.org/z/nK19aWMYE
Проверки на 0 честно делаются и никуда не выкашиваются, поэтому в цикле сложный CFG, против одного блока в С++. Я это не бенчмаркал, потому что мне лень, но перфоманс тут и так понятен.
Я бы сказал, что сложно построить пример, где возможны в теории (но не происходят на практике) какие-то исключительные ситуации (которые в С++ ведут к UB, а в rust -- к panic), и при этом перфоманс будет одинаковый. Если сможете это сделать, можете покидать примеры, очень интересно.
Я всё жду, когда авторов идеи UB начнут сравнивать с нацистами или даже конкретно с Гитлером. :)
Давайте я сразу пошлю в лес все инсинуации на тему "чего можно было бы сделать". clang и LLVM -- открытые продукты. Вы можете пойти и написать соответствующие ворнинги там, где хотите. Если вы сможете обосновать ревьюерам их необходимость, ваши правки примут. Честно. Никакой магии нет.
Я рассказываю о том, как оно работает сейчас, и объясняю, почему. Это не значит, что это лучший или единственно возможный способ делать то или это, но жизнь такова и более никакова. Вы можете 125 раз быть несогласны со вторым началом термодинамики или с правилами навигации кораблей в полярных водах, но разглагольствовать об этом здесь -- бесполезно.
https://en.cppreference.com/w/cpp/utility/program/raise
Подойдёт?
Java это сделала задолго до Раста. Это не "додумались убрать UB, которое исторически откуда-то притекло", а "вполне осознанно зарезали оптимизации путём спецификации любого поведения в угоду безопасности программирования". Перфоманс соответствующий.
Да, хорошее замечание, но я опустил эти подробности, имея в виду, что читатель все-таки знает, что такое printf. На самом деле достаточно вывести willreturn для этой функции (тогда не будет зависания или передачи управления неизвестно куда) и noalias для ptr. Полное определение тут можно не иметь, хватит атрибутов.
В смысле усилий программиста дешевле UB sanitizer. Он эти чеки сам вставляет. Другое дело, что так можно отловить сильно не всё. Вручную, впрочем, тоже. Банальный пример: если у вас есть только int*, вы никак не можете проверить, вылезаете вы за границы выделенной памяти или нет. Для этого где-то дополнительно придётся хранить длину, и это будет работать только если указатель всегда только на начало массива, а не куда-то в середину.
Понял. Пишите на лиспе.
Знал ли Наполеон Бонапарт, какой у него IQ, учитывая, что IQ придумали в 1916 году? :)
Не сенатор, а какой-то большой начальник в IBM. Что никак не отменяет 15+ лет опыта.
Вы можете генерить такой LLVM IR, в котором вообще не будет UB. Например, так, как вы описываете (плюс не навешивать никуда флагов типа nsw). Даже там, где язык это позволяет, он не обязывает делать именно UB. Но оптимизатору с этим будет жить тяжелее, поэтому на практике всегда, когда язык позволяет UB, стараются сгенерить такой IR, который ведёт себя так же.
За очень-очень-очень дорого, но можно. Можете померить скорость работы джавы с отключенным Tier 2 компилятором и сравнить с аналогичными программами на С++. Будет примерно то же самое (ожидаю в среднем разницу раз в 5-10, в терминальных случаях в сотни и тысячи).
Строго говоря, в джаве UB нет вообще, и даже в JLS такое слово не встречается. :) Дело в том, что "потенциально опасные" инструкции в джаве всё равно могут быть. Например, если вы компилируете джаву в LLVM (как делает компилятор Falcon), у вас всё равно в какой-то момент могут появиться всякие nsw флаги. Другое дело, что (если только в компиляторе нет багов) в этом случае реальное, непосредственное UB никогда не должно выполниться. Однако poison вполне себе генерится и протекает.
Посмотрите на ассемблер внимательно. Там две разные функции, просто записанные подряд. Тело одной начинается с лейбла
main:
, а тело другой -- сnever_called():
. Просто так получилось, что весь код после лейблаmain:
выкосился, и поэтому при передаче туда управления немедленно начинает исполняться код другой функции, который лежит дальше.Это разруливает фронтэнд. LLVM IR имеет чётко прописанное поведение каждой своей инструкции (когда там poison и когда там UB), а задача компилятора С или С++ -- перевести конструкции этих языков в LLVM IR таким образом, чтобы они работали так, как требуют стандарты этих языков.
Можете почитать статью Джона Рейгра Undefined Behavior != Unsafe Programming. Это не поможет писать без UB (это невозможно), но может, вы станете к этому относиться проще. :)
https://blog.regehr.org/archives/1467
Не сомневаюсь, вы дизайните языки и пишете стандарты лучше, чем спек-комитет. Но данная оптимизация очень даже не бессмысленная. Представьте такой код:
Представьте, что компилятор доказал, что cond начиная с итерации 10 -- инвариант. И переписал цикл в духе
Теперь у вас во втором цикле инвариантное условие, и его можно переписать как
Допустим, cond_10 в реальности всегда true, но доказать это невозможно. Так бывает. Теперь у вас второй while -- бесконечный цикл с горой кода, который ничего не делает. Если вы туда зайдёте, ваша программа зависнет и не сделает ничего и никогда. Вычищение такого кода -- экономия компайл тайма.
Вы, надеюсь, понимаете, чем сложение в длинных числах отличается от сложения в типах, поддерживаемых аппаратно, и во столько раз это медленнее? Если нет, то марш читать про то, что такое регистры.
То есть, вы предлагаете перед каждым делением вставлять (на уровне компиляторного фронта) чек типа
? Ну тогда будет Java, пишите на ней. В С++ это убьёт перфоманс.
А что будет, если вы предположили, а они не выполняются? :)
Помимо того, что дорого, так ещё и убьёт кучу оптимизаций. Ну например:
Вы тут не сможете заменить на print(10), придётся честно проверять.
Уточните -- кому надо? :)
Если вы не понимаете, зачем это делается, это не значит, что в этом нет логики и здравого смысла.
Верно, вы ничего не понимаете в компиляторах. Вас это удивляет? :)
Ну без nsw, который тут только фронт и может поставить, не обойтись. Языки, где переполнение работает на кольце, обязаны здесь вызвать do_2 при a=SINT_MAX, а C++ не обязан.
Я как раз сейчас в процессе написания статьи про UB. Надеюсь выпустить на этой неделе.
Если коротко, то изначально генерится максимально топорный IR, который работает "как написано", без каких-либо попыток эксплуатировать UB. А дальше запускается оптимизирующий пайплайн, который может эксплуатировать те или иные факты.
В данном конкретном примере происходит следующее: поскольку знаковое переполнение в С++ это UB, то на уровне LLVM у всех арифметических инструкций стоит флаг nsw. Пример можно посмотреть здесь: https://godbolt.org/z/j3n77no7r
Не вдаваясь в подробности, наличие флага nsw говорит компилятору: "работай с этой инструкцией так, как будто там никогда не происходит знаковое переполнение". Зная это, какая-нибудь оптимизация (скорее всего, InstCombine) может решить, что "add nsw x, 1 > x", поскольку случай переполнения его в этом случае не волнует, а без переполнения это всегда так.
В rust, видимо, семантика сложения такова, что оно всегда делается по модулю 2^32. Поэтому фронтенд флаг nsw на инструкции add не поставит, и описанная выше трансформация применяться не будет.
Вот да. Почему-то люди понимают, что выучиться на онлайн-курсах на пластического хирурга -- невозможно, но эти же люди уверены, что могут за тот же срок стать программистом.
Если верить Википедии, Билл Гейтс ещё в школе увлекался программированием, в 13 лет написал компьютерную игру и всё такое. Так что к 28 годам у него было 15+ лет опыта.