Здравствуйте, vdimas, Вы писали:
V>Тут стоит задасться вопросом — а зачем компилятор вообще пытается решить такие "уравнения"?
V>Это из-за шаблонов, из-за сильного раздутия бинарников по мере того, как шаблонный код становился всё более популярным когда-то.
То есть вы решили, что недостаточно бреда в ветке написали.
Нет, компилятору совершенно всё равно, есть ли в коде шаблоны или нет. Он прекрасно оптимизирует и вот такую функцию:
bool is_max(long a) { return (a+1) < a; }
Вообще, вся эта оптимизация применяется уже после инстанцирования всех шаблонов. В LLVM IR никаких шаблонов нету.
Поэтому правильный ответ на вопрос "зачем" — для максимизации производительности. Если компилятор не будет делать таких оптимизаций, то по бенчмаркам его код будет проигрывать конкурентам.
V>Такой подход позволяет обрезать из шаблонного кода ненужные ветки где только возможно дотянуться анализом кода.
Такой подход позволяет обрезать ненужные ветки из
любого кода. Что, в свою очередь, разблокирует целую цепочку дополнительных оптимизаций.
Во-первых, уходят бранчинг-инструкции. А ведь предсказание переходов экономит только такты условного перехода. Сами сравнения в конвеере остаются.
Во-вторых, если код выполняется в коротком цикле, то пара лишних инструкций способна ещё ухудшить производительность из-за переполнения буфера декодера опкодов.
В-третьих, после устранения лишних бранчей меняется дерево доминирования, что позволяет оптимизировать код, идущий ниже по графу исполнения.
V>Но оно же работает для любого инлайна.
Оно работает даже без инлайна. Просто без инлайна функция превращается в return false.
V>В примитивных случаях работает — уходит бранчинг, половина кода метода выбрасывается за ненадобностью.
Эскимосы впервые увидели снег.
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>
И это — прямое следствие того, что в дотнете знаковое переполнение не является UB.
Можете для эксперимента посмотреть, как будет выглядеть С++ код для unsigned long.
Внезапно, он тоже "ничего не ищет, а тупо исполняет". Как вы думаете, почему?
V>Забавно, что сложение выполняется в 64 бит, да еще через трюк адресной арифметики, но запоминаются младшие 32 бит результата.
Выполняется ли сложение для старших 32бит — вопрос эзотерический. Узнать это невозможно, т.к. доступа к потрохам процессора у нас нет, а на наблюдаемые регистры это никак не влияет. Ни старшие биты RAX, ни регистр флагов ничего нам не скажут о том, что там происходит в кристалле.
V>Для этого требуется сначала объявить переполнение знаковых не потенциальной ошибкой, а нормой.
Да, это является
необходимым условием. Но одного его недостаточно. Посмотрите, какой код генерируется для is_max<unsigned long>. Там нет никакой "потенциальной ошибки", и тем не менее компилятор не может избавиться от сложений и вычитаний.
V>Плюс, некоторые UB довольно-таки, забавны.
V>Например, вызов метода объекта, когда не обращаются данным объекта, т.е. ни к виртуальному методу, ни к полю.
V>Например, в C# вызов sc.Method() полностью заинлайнился, обращения к this нет, но в коде перед вызовом метода всё-равно стоит проверка, что адрес sc валидный.
Это потому, что C# порождает callvirt для всех методов, независимо от того, является ли метод виртуальным или нет. А
по стандарту, CLR обязан проверять this на null перед вызовом метода, даже если в данном контексте биндинг выполняется статически. Это было
осознанным решением.
V>В плюсах это UB, но, например, в MSVC прекрасно работает
V>И кто тут прав?
V>C#, который делает лишнюю проверку в рантайм перед вызовом практически каждого метода (nullable-аннотация не помогает), хотя зачастую нет обращения к this?
V>Или MSVC, который исполняет ненужный бранчинг?
V>Или Clang, который создаёт ненужный объект?
V>Или GCC, который не создаёт ненужный объект, но даже косвенно про проблему узнать не получится, бо нет утечки памяти? ))
В дотнете UB нет, там вызов метода на null-объекте является defined behavior, что облегчает портирование программ.
А из плюсовых компиляторов правы все, на то оно и UB.
Посмотрите, что происходит в LLVM-based компиляторах, если чутка поменять код:
https://godbolt.org/z/Y1W431348
Неожиданно,
внутри метода Method() нет никакого бранчинга. И даже строка "!!!" в выхлоп не попадает — компилятор решил, что this никогда-никогда не может быть null, и выкинул ветку.
Кланг хотя бы воспитанно предупреждает об этом:
warning: 'this' pointer cannot be null in well-defined C++ code; comparison may be assumed to always evaluate to false [-Wtautological-undefined-compare]
А gcc делает это молча. Поэтому если у вас есть привычка писать на msvc подобные штуки, то при переходе на другой компилятор стоит ждать сюрпризов:
template<typename T, T default_value>
struct Holder {
private: T _value;
public:
Holder(T value) { _value = value; }
T Value() {
return (this != NULL)
? _value
: default_value;
}
};