Информация об изменениях

Сообщение Re[35]: Carbon от 23.04.2024 16:05

Изменено 23.04.2024 16:09 vdimas

Re[35]: Carbon
Здравствуйте, Sinclair, Вы писали:

V>>Тут стоит задасться вопросом — а зачем компилятор вообще пытается решить такие "уравнения"?

V>>Это из-за шаблонов, из-за сильного раздутия бинарников по мере того, как шаблонный код становился всё более популярным когда-то.
S>То есть вы решили, что недостаточно бреда в ветке написали.

Для тебя это ликбез.


S>Нет, компилятору совершенно всё равно, есть ли в коде шаблоны или нет. Он прекрасно оптимизирует и вот такую функцию:

S>
S>bool is_max(long a) { return (a+1) < a; }
S>


Define "оптимизирует"? ))

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

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

Такой подход позволяет обрезать из шаблонного кода ненужные ветки где только возможно дотянуться анализом кода.
Но оно же работает для любого инлайна.



S>Вообще, вся эта оптимизация применяется уже после инстанцирования всех шаблонов. В LLVM IR никаких шаблонов нету.


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

Другие трюки оптимизации, навроде раскрутки циклов или оптимизации лейаута памяти (где локальные переменные могут занимать одно и то же физическое место в памяти на разных этапах своего жизненного цикла и т.д.) — это всё происходит обычно после классической бета-редукции, где обсуждаемое "решение уравнения" должно было состояться на этой стадии.


S>Поэтому правильный ответ на вопрос "зачем" — для максимизации производительности.


Это делает простая бета-редукция и тотальный инлайн.
Если в условии при if при распространении констант стоит значение, известное в compile-time, то бранчинг, очевидно, не нужен.


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


Рассуждения уровня детсада.
То ты рассуждаешь о решении уравнений, то совсем уж направление рукой показываешь.

Ты хоть понял, почему твоё "уравнение" компилятор и не думал решать?


V>>Такой подход позволяет обрезать из шаблонного кода ненужные ветки где только возможно дотянуться анализом кода.

S>Такой подход позволяет обрезать ненужные ветки из любого кода. Что, в свою очередь, разблокирует целую цепочку дополнительных оптимизаций.

Шаблонного/инлайного, а не любого.
Просто шаблоный код ввиду своей приоды инлайный, т.е. даже когда инстанс шаблонов описан явно в виде декларации, то в месте реализации тот код все-равно инлайный.

Частично еще инлайн происходит в режиме глобальной оптимизации, когда в момент линковки с третьесторонних либ тоже происходит "инлайн" бинарного кода. Но орудование семантикой в этот момент совсем плохое, поэтому глобальная оптимизация программ даёт не слишком много, только где небольшая вложенность относительно простых ф-ий, но и это уже неплохо порой.


S>Во-первых, уходят бранчинг-инструкции. А ведь предсказание переходов экономит только такты условного перехода. Сами сравнения в конвеере остаются.


Это результат стандартной бета-редкции, но она возможна только до неких установленных пределов своего распространения, т.к. тут размен быстродействия на объём бинарника и скорость компиляции.


S>Во-вторых, если код выполняется в коротком цикле, то пара лишних инструкций способна ещё ухудшить производительность из-за переполнения буфера декодера опкодов.

S>В-третьих, после устранения лишних бранчей меняется дерево доминирования, что позволяет оптимизировать код, идущий ниже по графу исполнения.

Бла-бла-бла. ))
Ты лучше пройдись по полному списку флагов оптимизации, если решил перечислить их все — там многие десятки флагов.
Опции Ox/O1/O2 включают сразу наборы отдельных флагов.

Мы тут рассуждали чуть о другом — почему компилятор ведеёт себя определённым образом при обнаружении UB?
И ответ там на поверхности, вообще-то.


V>>Но оно же работает для любого инлайна.

S>Оно работает даже без инлайна. Просто без инлайна функция превращается в return false.

И почему же в дотнете не превратилась?
Т.е., когда подаёшь костанту — оно даёт сразу ответ, бо JIT и AOT инлайнят маленькие методы, т.е. производят небольшую бета-редукцию.
Но почему в теле ф-ии остались честные вычисления от параметров? ))


V>>В примитивных случаях работает — уходит бранчинг, половина кода метода выбрасывается за ненадобностью.

S> Эскимосы впервые увидели снег.

Угу, особенно в Египте, где этого снега не бывает.
Изначально этот трюк не работал, когда появились генерики, бо это было первой мыслью — проэмулировать числовые параметры генериков.
Обломс...
Заработало с 4.x какой-то версии дотнета (раз в несколько лет проверял), и то — криво и косо вплоть до аж 8-го дотнета, где этот трюк, наконец-то, стал полноценным.


V>>Для сравнения, дотнет ничего не ищет, тупо исполняет:

V>>
V>>Program:IsMax(int):bool:
V>>       lea      eax, [rdi+01H]
V>>       cmp      eax, edi
V>>       setl     al
V>>       movzx    rax, al
V>>       ret  
V>>

S>И это — прямое следствие того, что в дотнете знаковое переполнение не является UB.

Это прямое следствие того, что дотнет не имеет права оптимизировать код на уровне байт-кода.
В режиме "Optimize" он слегка оптимизирует только логику вокруг локальных переменных.
JIT априори не имеет права тратить кучу времени на анализ кода, а AOT вот только вышло из пеленок в 8-м дотнете, выводы делать рано.

Плюсы без агрессивных оптимизаций тоже дают верный код, как и дотнет.


S>Можете для эксперимента посмотреть, как будет выглядеть С++ код для unsigned long.

S>Внезапно, он тоже "ничего не ищет, а тупо исполняет". Как вы думаете, почему?

Вопрос здесь не почему исполняет в случае знаковых, а почему именно так?
Я ведь ниже дал хороший пример, который должен был объяснить логику разруливания UB.


V>>Забавно, что сложение выполняется в 64 бит, да еще через трюк адресной арифметики, но запоминаются младшие 32 бит результата.

S> Выполняется ли сложение для старших 32бит — вопрос эзотерический. Узнать это невозможно, т.к. доступа к потрохам процессора у нас нет, а на наблюдаемые регистры это никак не влияет. Ни старшие биты RAX, ни регистр флагов ничего нам не скажут о том, что там происходит в кристалле.

Ес-но, что вопрос не принципиальный, что там случается унутре. Просто через адресную арифметику можно оперировать размером адреса, поэтому там rdi+1 а не edi+1.
И плюс это UNIX ABI для x64, где первые несколько параметров ф-ий идут в регистрах:

- Arguments 1-6 are passed via registers RDI, RSI, RDX, RCX, R8, R9 respectively;
— Arguments 7 and above are pushed on to the stack.

(Это для целых, указателей и тривиальных типов)


V>>Для этого требуется сначала объявить переполнение знаковых не потенциальной ошибкой, а нормой.

S>Да, это является необходимым условием. Но одного его недостаточно. Посмотрите, какой код генерируется для is_max<unsigned long>. Там нет никакой "потенциальной ошибки", и тем не менее компилятор не может избавиться от сложений и вычитаний.

Ну, значит не запрограммировали оптимизацию того уникального случая, когда прибавляют единицу и сравнивают.
Если прибавлять двойку — уже два сравнения, больше — больше.

Беззнаковые "по кругу" переполняют произвольными приращениями, это популярная техника для табличных генераторов (случайных чисел, шума, гармоник и т.д.)


V>>Плюс, некоторые UB довольно-таки, забавны.


V>>Например, вызов метода объекта, когда не обращаются данным объекта, т.е. ни к виртуальному методу, ни к полю.

V>>Например, в C# вызов sc.Method() полностью заинлайнился, обращения к this нет, но в коде перед вызовом метода всё-равно стоит проверка, что адрес sc валидный.
S>Это потому, что C# порождает callvirt для всех методов, независимо от того, является ли метод виртуальным или нет.

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


S>А по стандарту, CLR обязан проверять this на null перед вызовом метода, даже если в данном контексте биндинг выполняется статически. Это было осознанным решением.


Эдак тебя заносит. ))
Причина ранней диагностики ошибки по твоей ссылке понятна, когда еще не были развиты наработки по escape analisys.
А если this не уходит дальше, как в примере?
Почему компилятор/JIt/AOT неспособны сделать escape analysis для простейших случаев?

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

И, чтобы уж закрыть тему, в C# происходит проверка на null дважды: первый раз "по правилам хорошего тона" в начале ф-ий проверяются аргументы или в коде проверяются возвращаемые значения джругих методов. Второй раз проверка происходит на уровне системы как в моём примере.

Новомодные nullable-нотации не помогают сэкономить на "системных" проверках аж никак, они помогают сэкономить только на юзверских проверках.

Т.е. nullable-подход в дотнете не является строгим. Ты можешь вернуть null или подать null в кач-ве аргумента, даже если те описаны как non-nullable.


V>>В плюсах это UB, но, например, в MSVC прекрасно работает

V>>И кто тут прав?
V>>C#, который делает лишнюю проверку в рантайм перед вызовом практически каждого метода (nullable-аннотация не помогает), хотя зачастую нет обращения к this?
V>>Или MSVC, который исполняет ненужный бранчинг?
V>>Или Clang, который создаёт ненужный объект?
V>>Или GCC, который не создаёт ненужный объект, но даже косвенно про проблему узнать не получится, бо нет утечки памяти? ))


S>В дотнете UB нет, там вызов метода на null-объекте является defined behavior, что облегчает портирование программ.


И одновременно с этим обрезает оптимизации, т.е. не позволяет выкидывать ненужный код. ))


S>А из плюсовых компиляторов правы все, на то оно и UB.


Вот именно.
Во всей этой истории я зацепился из-за твоего любимого взятого тона в адрес плюсов, мол, херня какая-то происходит.

Нет, не херня. В каждом конкретном случае или никакой оптимизации при UB не происходит вовсе (как в MSVC), либо происходит такая оптимизация, которая считает, что ошибочная ситуация произойти не должна.

Clang традиционно более щепетилен — поэтому он всегда создаёт ненужный объект в моём сниппете.
Но Clang — это не про производительность, это некий референс плюсов как таковых.

GCC — это существо из рил ворлд, поэтому исследует объекты на предмет побочных эффектов и убегания зависимостей, и если их нет — может "уничтожать" объекты, т.е. не конструировать их вовсе, а брать только описанную функциональность. Единственным "побочным эффектом" тут будет лишнее выделение памяти аккурат перед AV, что побочным эффектом не очень-то и считается.

Лично меня тут удивило чуть другое — когда-то MSVC в деле оптимизаций драл GCC в хвост и гриву, а сейчас малость сдал позиции...
Но посмотрим, посмотрим...


S>Посмотрите, что происходит в LLVM-based компиляторах, если чутка поменять код:

S>https://godbolt.org/z/Y1W431348

В GCC аналогично.


S>Неожиданно, внутри метода Method() нет никакого бранчинга. И даже строка "!!!" в выхлоп не попадает — компилятор решил, что this никогда-никогда не может быть null, и выкинул ветку.

S>Кланг хотя бы воспитанно предупреждает об этом:
S>

S>warning: 'this' pointer cannot be null in well-defined C++ code; comparison may be assumed to always evaluate to false [-Wtautological-undefined-compare]

S>А gcc делает это молча.

Этот ворнинг слишком легко нивелировать:
const char * GetString(void * arg) {
    return !arg ? "!!!" : ":)";
}

struct SomeObj {
    void Method() {
        std::cout << GetString(this) << std::endl;
    }
};


Т.е. в реальных проектах, где никто в здравом уме никогда не сравнивает this с nullptr, а передаёт его куда-то дальше, где уже и возникает ошибка, Clang никак не помогает.
Потому что требуется чуть более глубокий анализ кода (по сниппету видно, что действительно хотя бы чуть), но CLang и так компилит небыстро.

Собсно, поэтому существует рынок анализаторов кода.
Есть статические:
https://www.incredibuild.com/blog/top-9-c-static-code-analysis-tools
https://learn.microsoft.com/en-us/cpp/code-quality/quick-start-code-analysis-for-c-cpp?view=msvc-170
(плюс в релизе ворнинги продвигаются до ошибок)

Есть динамические — на обращение к памяти, на верную её реинтерпретацию, на гонки и т.д.

Для дотнета рынок динамических анализаторов бедноват и уровень их невысок:
https://devblogs.microsoft.com/dotnet/infer-v1-2-interprocedural-memory-safety-analysis-for-c/
https://github.com/microsoft/infersharp

Т.е. вся связка остальных UB в дотнете (гонки, неверная реинтерпретация памяти, обращение к удалённой памяти и т.д.) в дотнете проявляется в полный рост в отсутствии адекватных инструментов анализа.
(Да, в safe-режиме неверная реинтерепретация памяти как минимум затруднена, как минимум раньше, до появления Unsafe.As<TFrom,TTo>(ref TFrom source), которе прекрасно компилится и в safe-режиме)

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

А так-то, в своей конторе приходилось искать гонки у коллег и в дотнете, и там всё тоже прекрасно и подвисает и падает с грохотом серванта, полного хрустальной посуды. ))
И что бесит, сцуко, что пресловутая "планка входа" — это чёртов миф.
Да, в дотнете легко начать программировать, примерно как в Паскале когда-то...
Но как только начинаешь касаться многопоточности, синхронизации, детерминированного управления ресурсами и прочим — то планка входа становится обычной, а местами даже выше должна быть, т.к. в плюсах хорошо видно, что происходит, а в дотнете необходимо обладать эрудицией, как оно происходит подкапотом в CLR и базовых библиотеках.

Я уже озвучивал не раз, что в дотнете "легко" только то, что можно использовать уже готовым.
А если что-то почти с 0-ля рисовать — это застрелиться, до чего неудобный язык, невыразительный и опасный в использовании язык (дефолтная инициализация value-types, теневые копии readonly-полей, вместо детектирования ошибок и т.д. до бесконечности).

В последних версиях язык стал чуть удобней, конечно, но всё-равно еще расти и расти. ))

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

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

Даже взять современный стандарт C++20 — компы 20-тилетней давности банально не справились бы с компиляцией за адекватный срок, работать работу программиста С++ было бы невозможно.

Но отрасль решила проблемы через использование билд-серверов, названных малоподходящим баззвордом CI.
Там компиляется не только целевой проект для различных платформ с запусками тестов, а так же в параллель работают сразу 3 различных типов анализаторов (по крайней мере в моей конторе).


S>Поэтому если у вас есть привычка писать на msvc подобные штуки, то при переходе на другой компилятор стоит ждать сюрпризов:


ЧТД
Здесь и кроется основное моё расхождение в рассуждениях с тобой.

Ты допускаешь ill-formed код в своих рассуждениях, просто ждешь от компилятора некоей предсказуемой реакции на такой код в рантайм.
Т.е. утекание ошибки в продакшен — "а чо такого?" ))

А в отрасли давно сложился консенсус, что код должен быть well-formed.
Статических анализаторов кода для дотнета хватает, кстате, и тоже неспроста.

И если билд-процесс тщательно неастроен, если любой коммит проверяется соотв. тулзами на билд-серверах, то оно оседает уже где-то на уровне мозжечка, отчего твои рассуждления выглядят местами забавными, без обид.
Это спор слепого со зрячим.
Мы были слепы еще примерно в 90-х. Ближе к концу пошли анализаторы и общее понимание важности well-formed кода.
Чего только стоило отучить всю отрасль в конце 90-х и начала 2000-х делать более одного побочного эффекта в одной точке следования...

Херни-то можно на любом языке налепить, понятно.
Без агрессивных оптимизаций плюсы будут на любую херню реагировать в точности как C#, т.е. спор становится бесполезным.

В общем, чем агрессивнее применяемая оптимизация, тем меньше нежданчиков должно сидеть в коде, ес-но.
Именно поэтому я вангую, что по мере улучшения оптимизации в дотнете, будет корректироваться и стандарт.

Помнишь себя в 2004-2005-х годах как ты вещал с трибун: "Представьте, что сейчас вы напишете код, а потом он будет исполняться намного быстре на будущих версиях платформы!!!"
Дудки!
Сначала убери из кода потенциальные ошибки. ))
Например, в дотнете (и особенно в Джаве) есть ошибки, когда в метод подают не копию объекта, а сам объект, т.е. провоцируют убегание побочных эффектов, а компилятор помочь не может, т.к. он понятия не имеет о семантике.

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

На текущей стадии дотнет резко опережает джаву в возможностях языка, но заметно отстаёт в качестве генерируемого JIT или AOT кода.
Дотнет пока что спасается, считай, только за счёт value-type и и более гладкого сопряжения с нейтивом, а так бы давно уже умер.

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

Ты один из немногих динозавров остался, который никак не согласится снять розовые очки. ))
Re[35]: Carbon
Здравствуйте, Sinclair, Вы писали:

V>>Тут стоит задасться вопросом — а зачем компилятор вообще пытается решить такие "уравнения"?

V>>Это из-за шаблонов, из-за сильного раздутия бинарников по мере того, как шаблонный код становился всё более популярным когда-то.
S>То есть вы решили, что недостаточно бреда в ветке написали.

Для тебя это ликбез.


S>Нет, компилятору совершенно всё равно, есть ли в коде шаблоны или нет. Он прекрасно оптимизирует и вот такую функцию:

S>
S>bool is_max(long a) { return (a+1) < a; }
S>


Define "оптимизирует"? ))

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

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

Такой подход позволяет обрезать из шаблонного кода ненужные ветки где только возможно дотянуться анализом кода.
Но оно же работает для любого инлайна.



S>Вообще, вся эта оптимизация применяется уже после инстанцирования всех шаблонов. В LLVM IR никаких шаблонов нету.


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

Другие трюки оптимизации, навроде раскрутки циклов или оптимизации лейаута памяти (где локальные переменные могут занимать одно и то же физическое место в памяти на разных этапах своего жизненного цикла и т.д.) — это всё происходит обычно после классической бета-редукции, где обсуждаемое "решение уравнения" должно было состояться на этой стадии.


S>Поэтому правильный ответ на вопрос "зачем" — для максимизации производительности.


Это делает простая бета-редукция и тотальный инлайн.
Если в условии при if при распространении констант стоит значение, известное в compile-time, то бранчинг, очевидно, не нужен.


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


Рассуждения уровня детсада.
То ты рассуждаешь о решении уравнений, то совсем уж направление рукой показываешь.

Ты хоть понял, почему твоё "уравнение" компилятор и не думал решать?


V>>Такой подход позволяет обрезать из шаблонного кода ненужные ветки где только возможно дотянуться анализом кода.

S>Такой подход позволяет обрезать ненужные ветки из любого кода. Что, в свою очередь, разблокирует целую цепочку дополнительных оптимизаций.

Шаблонного/инлайного, а не любого.
Просто шаблоный код ввиду своей приоды инлайный, т.е. даже когда инстанс шаблонов описан явно в виде декларации, то в месте реализации тот код все-равно инлайный.

Частично еще инлайн происходит в режиме глобальной оптимизации, когда в момент линковки с третьесторонних либ тоже происходит "инлайн" бинарного кода. Но орудование семантикой в этот момент совсем плохое, поэтому глобальная оптимизация программ даёт не слишком много, только где небольшая вложенность относительно простых ф-ий, но и это уже неплохо порой.


S>Во-первых, уходят бранчинг-инструкции. А ведь предсказание переходов экономит только такты условного перехода. Сами сравнения в конвеере остаются.


Это результат стандартной бета-редкции, но она возможна только до неких установленных пределов своего распространения, т.к. тут размен быстродействия на объём бинарника и скорость компиляции.


S>Во-вторых, если код выполняется в коротком цикле, то пара лишних инструкций способна ещё ухудшить производительность из-за переполнения буфера декодера опкодов.

S>В-третьих, после устранения лишних бранчей меняется дерево доминирования, что позволяет оптимизировать код, идущий ниже по графу исполнения.

Бла-бла-бла. ))
Ты лучше пройдись по полному списку флагов оптимизации, если решил перечислить их все — там многие десятки флагов.
Опции Ox/O1/O2 включают сразу наборы отдельных флагов.

Мы тут рассуждали чуть о другом — почему компилятор ведеёт себя определённым образом при обнаружении UB?
И ответ там на поверхности, вообще-то.


V>>Но оно же работает для любого инлайна.

S>Оно работает даже без инлайна. Просто без инлайна функция превращается в return false.

И почему же в дотнете не превратилась?
Т.е., когда подаёшь костанту — оно даёт сразу ответ, бо JIT и AOT инлайнят маленькие методы, т.е. производят небольшую бета-редукцию.
Но почему в теле ф-ии остались честные вычисления от параметров? ))


V>>В примитивных случаях работает — уходит бранчинг, половина кода метода выбрасывается за ненадобностью.

S> Эскимосы впервые увидели снег.

Угу, особенно в Египте, где этого снега не бывает.
Изначально этот трюк не работал, когда появились генерики, бо это было первой мыслью — проэмулировать числовые параметры генериков.
Обломс...
Заработало с 4.x какой-то версии дотнета (раз в несколько лет проверял), и то — криво и косо вплоть до аж 8-го дотнета, где этот трюк, наконец-то, стал полноценным.
Двадцать лет коту под хвост...


V>>Для сравнения, дотнет ничего не ищет, тупо исполняет:

V>>
V>>Program:IsMax(int):bool:
V>>       lea      eax, [rdi+01H]
V>>       cmp      eax, edi
V>>       setl     al
V>>       movzx    rax, al
V>>       ret  
V>>

S>И это — прямое следствие того, что в дотнете знаковое переполнение не является UB.

Это прямое следствие того, что дотнет не имеет права оптимизировать код на уровне байт-кода.
В режиме "Optimize" он слегка оптимизирует только логику вокруг локальных переменных.
JIT априори не имеет права тратить кучу времени на анализ кода, а AOT вот только вышло из пеленок в 8-м дотнете, выводы делать рано.

Плюсы без агрессивных оптимизаций тоже дают верный код, как и дотнет.


S>Можете для эксперимента посмотреть, как будет выглядеть С++ код для unsigned long.

S>Внезапно, он тоже "ничего не ищет, а тупо исполняет". Как вы думаете, почему?

Вопрос здесь не почему исполняет в случае знаковых, а почему именно так?
Я ведь ниже дал хороший пример, который должен был объяснить логику разруливания UB.


V>>Забавно, что сложение выполняется в 64 бит, да еще через трюк адресной арифметики, но запоминаются младшие 32 бит результата.

S> Выполняется ли сложение для старших 32бит — вопрос эзотерический. Узнать это невозможно, т.к. доступа к потрохам процессора у нас нет, а на наблюдаемые регистры это никак не влияет. Ни старшие биты RAX, ни регистр флагов ничего нам не скажут о том, что там происходит в кристалле.

Ес-но, что вопрос не принципиальный, что там случается унутре. Просто через адресную арифметику можно оперировать размером адреса, поэтому там rdi+1 а не edi+1.
И плюс это UNIX ABI для x64, где первые несколько параметров ф-ий идут в регистрах:

- Arguments 1-6 are passed via registers RDI, RSI, RDX, RCX, R8, R9 respectively;
— Arguments 7 and above are pushed on to the stack.

(Это для целых, указателей и тривиальных типов)


V>>Для этого требуется сначала объявить переполнение знаковых не потенциальной ошибкой, а нормой.

S>Да, это является необходимым условием. Но одного его недостаточно. Посмотрите, какой код генерируется для is_max<unsigned long>. Там нет никакой "потенциальной ошибки", и тем не менее компилятор не может избавиться от сложений и вычитаний.

Ну, значит не запрограммировали оптимизацию того уникального случая, когда прибавляют единицу и сравнивают.
Если прибавлять двойку — уже два сравнения, больше — больше.

Беззнаковые "по кругу" переполняют произвольными приращениями, это популярная техника для табличных генераторов (случайных чисел, шума, гармоник и т.д.)


V>>Плюс, некоторые UB довольно-таки, забавны.


V>>Например, вызов метода объекта, когда не обращаются данным объекта, т.е. ни к виртуальному методу, ни к полю.

V>>Например, в C# вызов sc.Method() полностью заинлайнился, обращения к this нет, но в коде перед вызовом метода всё-равно стоит проверка, что адрес sc валидный.
S>Это потому, что C# порождает callvirt для всех методов, независимо от того, является ли метод виртуальным или нет.

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


S>А по стандарту, CLR обязан проверять this на null перед вызовом метода, даже если в данном контексте биндинг выполняется статически. Это было осознанным решением.


Эдак тебя заносит. ))
Причина ранней диагностики ошибки по твоей ссылке понятна, когда еще не были развиты наработки по escape analisys.
А если this не уходит дальше, как в примере?
Почему компилятор/JIt/AOT неспособны сделать escape analysis для простейших случаев?

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

И, чтобы уж закрыть тему, в C# происходит проверка на null дважды: первый раз "по правилам хорошего тона" в начале ф-ий проверяются аргументы или в коде проверяются возвращаемые значения джругих методов. Второй раз проверка происходит на уровне системы как в моём примере.

Новомодные nullable-нотации не помогают сэкономить на "системных" проверках аж никак, они помогают сэкономить только на юзверских проверках.

Т.е. nullable-подход в дотнете не является строгим. Ты можешь вернуть null или подать null в кач-ве аргумента, даже если те описаны как non-nullable.


V>>В плюсах это UB, но, например, в MSVC прекрасно работает

V>>И кто тут прав?
V>>C#, который делает лишнюю проверку в рантайм перед вызовом практически каждого метода (nullable-аннотация не помогает), хотя зачастую нет обращения к this?
V>>Или MSVC, который исполняет ненужный бранчинг?
V>>Или Clang, который создаёт ненужный объект?
V>>Или GCC, который не создаёт ненужный объект, но даже косвенно про проблему узнать не получится, бо нет утечки памяти? ))


S>В дотнете UB нет, там вызов метода на null-объекте является defined behavior, что облегчает портирование программ.


И одновременно с этим обрезает оптимизации, т.е. не позволяет выкидывать ненужный код. ))


S>А из плюсовых компиляторов правы все, на то оно и UB.


Вот именно.
Во всей этой истории я зацепился из-за твоего любимого взятого тона в адрес плюсов, мол, херня какая-то происходит.

Нет, не херня. В каждом конкретном случае или никакой оптимизации при UB не происходит вовсе (как в MSVC), либо происходит такая оптимизация, которая считает, что ошибочная ситуация произойти не должна.

Clang традиционно более щепетилен — поэтому он всегда создаёт ненужный объект в моём сниппете.
Но Clang — это не про производительность, это некий референс плюсов как таковых.

GCC — это существо из рил ворлд, поэтому исследует объекты на предмет побочных эффектов и убегания зависимостей, и если их нет — может "уничтожать" объекты, т.е. не конструировать их вовсе, а брать только описанную функциональность. Единственным "побочным эффектом" тут будет лишнее выделение памяти аккурат перед AV, что побочным эффектом не очень-то и считается.

Лично меня тут удивило чуть другое — когда-то MSVC в деле оптимизаций драл GCC в хвост и гриву, а сейчас малость сдал позиции...
Но посмотрим, посмотрим...


S>Посмотрите, что происходит в LLVM-based компиляторах, если чутка поменять код:

S>https://godbolt.org/z/Y1W431348

В GCC аналогично.


S>Неожиданно, внутри метода Method() нет никакого бранчинга. И даже строка "!!!" в выхлоп не попадает — компилятор решил, что this никогда-никогда не может быть null, и выкинул ветку.

S>Кланг хотя бы воспитанно предупреждает об этом:
S>

S>warning: 'this' pointer cannot be null in well-defined C++ code; comparison may be assumed to always evaluate to false [-Wtautological-undefined-compare]

S>А gcc делает это молча.

Этот ворнинг слишком легко нивелировать:
const char * GetString(void * arg) {
    return !arg ? "!!!" : ":)";
}

struct SomeObj {
    void Method() {
        std::cout << GetString(this) << std::endl;
    }
};


Т.е. в реальных проектах, где никто в здравом уме никогда не сравнивает this с nullptr, а передаёт его куда-то дальше, где уже и возникает ошибка, Clang никак не помогает.
Потому что требуется чуть более глубокий анализ кода (по сниппету видно, что действительно хотя бы чуть), но CLang и так компилит небыстро.

Собсно, поэтому существует рынок анализаторов кода.
Есть статические:
https://www.incredibuild.com/blog/top-9-c-static-code-analysis-tools
https://learn.microsoft.com/en-us/cpp/code-quality/quick-start-code-analysis-for-c-cpp?view=msvc-170
(плюс в релизе ворнинги продвигаются до ошибок)

Есть динамические — на обращение к памяти, на верную её реинтерпретацию, на гонки и т.д.

Для дотнета рынок динамических анализаторов бедноват и уровень их невысок:
https://devblogs.microsoft.com/dotnet/infer-v1-2-interprocedural-memory-safety-analysis-for-c/
https://github.com/microsoft/infersharp

Т.е. вся связка остальных UB в дотнете (гонки, неверная реинтерпретация памяти, обращение к удалённой памяти и т.д.) в дотнете проявляется в полный рост в отсутствии адекватных инструментов анализа.
(Да, в safe-режиме неверная реинтерепретация памяти как минимум затруднена, как минимум раньше, до появления Unsafe.As<TFrom,TTo>(ref TFrom source), которе прекрасно компилится и в safe-режиме)

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

А так-то, в своей конторе приходилось искать гонки у коллег и в дотнете, и там всё тоже прекрасно и подвисает и падает с грохотом серванта, полного хрустальной посуды. ))
И что бесит, сцуко, что пресловутая "планка входа" — это чёртов миф.
Да, в дотнете легко начать программировать, примерно как в Паскале когда-то...
Но как только начинаешь касаться многопоточности, синхронизации, детерминированного управления ресурсами и прочим — то планка входа становится обычной, а местами даже выше должна быть, т.к. в плюсах хорошо видно, что происходит, а в дотнете необходимо обладать эрудицией, как оно происходит подкапотом в CLR и базовых библиотеках.

Я уже озвучивал не раз, что в дотнете "легко" только то, что можно использовать уже готовым.
А если что-то почти с 0-ля рисовать — это застрелиться, до чего неудобный язык, невыразительный и опасный в использовании язык (дефолтная инициализация value-types, теневые копии readonly-полей, вместо детектирования ошибок и т.д. до бесконечности).

В последних версиях язык стал чуть удобней, конечно, но всё-равно еще расти и расти. ))

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

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

Даже взять современный стандарт C++20 — компы 20-тилетней давности банально не справились бы с компиляцией за адекватный срок, работать работу программиста С++ было бы невозможно.

Но отрасль решила проблемы через использование билд-серверов, названных малоподходящим баззвордом CI.
Там компиляется не только целевой проект для различных платформ с запусками тестов, а так же в параллель работают сразу 3 различных типов анализаторов (по крайней мере в моей конторе).


S>Поэтому если у вас есть привычка писать на msvc подобные штуки, то при переходе на другой компилятор стоит ждать сюрпризов:


ЧТД
Здесь и кроется основное моё расхождение в рассуждениях с тобой.

Ты допускаешь ill-formed код в своих рассуждениях, просто ждешь от компилятора некоей предсказуемой реакции на такой код в рантайм.
Т.е. утекание ошибки в продакшен — "а чо такого?" ))

А в отрасли давно сложился консенсус, что код должен быть well-formed.
Статических анализаторов кода для дотнета хватает, кстате, и тоже неспроста.

И если билд-процесс тщательно неастроен, если любой коммит проверяется соотв. тулзами на билд-серверах, то оно оседает уже где-то на уровне мозжечка, отчего твои рассуждления выглядят местами забавными, без обид.
Это спор слепого со зрячим.
Мы были слепы еще примерно в 90-х. Ближе к концу пошли анализаторы и общее понимание важности well-formed кода.
Чего только стоило отучить всю отрасль в конце 90-х и начала 2000-х делать более одного побочного эффекта в одной точке следования...

Херни-то можно на любом языке налепить, понятно.
Без агрессивных оптимизаций плюсы будут на любую херню реагировать в точности как C#, т.е. спор становится бесполезным.

В общем, чем агрессивнее применяемая оптимизация, тем меньше нежданчиков должно сидеть в коде, ес-но.
Именно поэтому я вангую, что по мере улучшения оптимизации в дотнете, будет корректироваться и стандарт.

Помнишь себя в 2004-2005-х годах как ты вещал с трибун: "Представьте, что сейчас вы напишете код, а потом он будет исполняться намного быстре на будущих версиях платформы!!!"
Дудки!
Сначала убери из кода потенциальные ошибки. ))
Например, в дотнете (и особенно в Джаве) есть ошибки, когда в метод подают не копию объекта, а сам объект, т.е. провоцируют убегание побочных эффектов, а компилятор помочь не может, т.к. он понятия не имеет о семантике.

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

На текущей стадии дотнет резко опережает джаву в возможностях языка, но заметно отстаёт в качестве генерируемого JIT или AOT кода.
Дотнет пока что спасается, считай, только за счёт value-type и и более гладкого сопряжения с нейтивом, а так бы давно уже умер.

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

Ты один из немногих динозавров остался, который никак не согласится снять розовые очки. ))