Re[34]: Carbon
От: Sinclair Россия https://github.com/evilguest/
Дата: 23.04.24 03:50
Оценка: :)
Здравствуйте, 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;
    }
};
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[30]: Carbon
От: Sinclair Россия https://github.com/evilguest/
Дата: 23.04.24 04:02
Оценка:
Здравствуйте, vdimas, Вы писали:

V>Здравствуйте, Sinclair, Вы писали:


S>>Следите за руками: в дотнете прибавление единицы к int 0x7FFFFFFF даёт int 0x80000000 (overflows are ignored). Здесь нет никаких бит для отбрасывания, т.к. весь результат fits in the destination type.

S>>А в C++ такое прибавление даёт undefined behavior.

V>Продолжаешь пытаться показывать фокусы? ))

Продолжаю развеивать невежество.
V>Если в этот же код подать другое число, то может быть отбрасывание.
Ок, давайте подадим в этот код другое число. Например, 0xFFFFFFFF. Это, напомню, int(-1) в дополнительном коде. Прибавление единицы даст 0x100000000, и старший бит будет отброшен. Опять нет никакого undefined behavior, как бы вы ни старались.
V>Это даже уже не фокус у тебя, это мошенничество в расчёте на однобитную тусовку, один завсегдатай уже отметился.

V>Тебе же уже говорилось, что промежуточные вычисления в более широких типах допустимы прямо по стандарту.
. В четвёртый раз спрашиваю вас — что за "более широкий тип" применяется в is_max<long>?

V>И раздражает твоё непонимание понятия UB.

А уж меня-то как раздражает ваше непонимание UB. Я же носом ткнул уже вас в пример, когда один и тот же код, в одном и том же компиляторе, для одного и того же значения выдаёт разные результаты./
Но вы продолжаете фонтанировать чушью.

V>Т.е. вот у тебя в формуле встречается участок (a+b)/2, в котором может наступить переполнение и выдача ошибочного результата.

V>Разумеется, до факта переполнения формула остаётся работоспособной.
V>А после переполнения — неопределено, т.е. может остаться работоспособной, а может и не остаться.
"работоспособность" зависит от того, что мы вкладываем в это понятие. В частности, если мы зачем-то полагаемся на то, что этот код переполнится для чисел, сумма которых не влезает в тип результата, то это тоже может оказаться неверным. Компилятор, увидев в коде avg(x, x) после инлайна имеет право убрать всю арифметику, т.к. x*2/2 == x при отсутствии переполнения, а раз переполнение — UB, значит при его наступлении можно делать всё, что угодно.

V>И что ты рядом бил себя в грудь, что в дотнете формула достоверно будет неработоспособной — это потому что дотнет пока мест всё еще детская игрушка.

Нет, не поэтому. А потому что нет UB.
Для проверки ваших рассуждений попробуйте повторить их для unsigned long. Ровно тот же самый оптимизатор, который "не игрушка", почему-то не "отбрасывает никаких бит", и не сравнивает числа "в более высокой разрядности".
А как только вы поймёте, что рассмотренная оптимизация целиком построена на UB, можно будет попробовать прикинуть, применима ли она для языков, где в арифметике UB нет ни для беззнаковых, ни для знаковых типов.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[32]: Carbon
От: Sinclair Россия https://github.com/evilguest/
Дата: 23.04.24 06:35
Оценка:
Здравствуйте, vdimas, Вы писали:

V>Здравствуйте, Sinclair, Вы писали:


S>>1. Команда инкремента "полурегистра", конечно же, есть.


V>Увы, отдельно оперировать только младшим полурегистром невозможно.

Попробуйте сделать в вашем коде inc ax. Это к вопросу о том, что происходит с is_max<short>.

V>"Сходи туда, не знаю куда". ))

V>Ты ж не дал ассемблерный листинг интересующего тебя кода, про который просишь пояснений.
Я дал все нужные листинги.
Вот на всякий случай ещё раз пример: https://godbolt.org/z/axaMesqce
bool is_max<long>(long):
        xor       eax, eax                                      
        ret                                                     
bool is_max<unsigned long>(unsigned long):
        lea       rdx, QWORD PTR [1+rdi]                        
        xor       eax, eax                                      
        cmp       rdx, rdi                                      
        setb      al                                            
        ret

Давайте, расскажите мне, что там расширил компилятор для is_max<long>, и почему он не стал ничего расширять для is_max<unsigned long>.
V>И вообще, слишком много умозрительных рассуждений безо всякого полезного выхлопа.
От вас — да. Воображаемые "расширения битности" и прочая нерелевантная муть.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[35]: Carbon
От: vdimas Россия  
Дата: 23.04.24 16:05
Оценка: :)
Здравствуйте, 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 — это существо из рил ворлд, поэтому исследует объекты на предмет побочных эффектов и убегания зависимостей, и если их нет — может "уничтожать" объекты, т.е. не конструировать их вовсе, а брать только описанную функциональность. Единственным "побочным эффектом" тут будет лишнее выделение памяти, которая не освобождается, что тоже ошибка, и вот GCC её исправил. ))

Лично меня тут удивило чуть другое — когда-то 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-полей обращаются через блокировку для записи и без блокировок для чтения. Это нормальный сценарий для многих алгоримов, но анализаторы дотнета пока мест вытягивают только простейшие ситуации.

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

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

Ты один из немногих динозавров остался, который никак не согласится снять розовые очки. ))
Отредактировано 23.04.2024 16:17 vdimas . Предыдущая версия . Еще …
Отредактировано 23.04.2024 16:16 vdimas . Предыдущая версия .
Отредактировано 23.04.2024 16:12 vdimas . Предыдущая версия .
Отредактировано 23.04.2024 16:09 vdimas . Предыдущая версия .
Отредактировано 23.04.2024 16:07 vdimas . Предыдущая версия .
Re[36]: Carbon
От: Sinclair Россия https://github.com/evilguest/
Дата: 24.04.24 01:54
Оценка: :)
Здравствуйте, vdimas, Вы писали:
V>Для тебя это ликбез.
Ложные утверждения ликбезом быть не могут.

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

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


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

Оставляет от всего кода xor eax, eax.

V>Оптимизаций много разных.

О, началось виляние.
V>"Вся оптимизация" — это какая?
Примерно вся. Распространение констант, инлайн вызовов, разворачивание и разгрузка циклов, алгебраические тождества, приведение подобных — я вспотею перечислять.

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


V>Это делает простая бета-редукция и тотальный инлайн.

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

V>То ты рассуждаешь о решении уравнений, то совсем уж направление рукой показываешь.
V>Ты хоть понял, почему твоё "уравнение" компилятор и не думал решать?
Я? Нет, не понял. Поясните своими словами.

V>Шаблонного/инлайного, а не любого.

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

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

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

V>Ты лучше пройдись по полному списку флагов оптимизации, если решил перечислить их все — там многие десятки флагов.

Отож.

V>Мы тут рассуждали чуть о другом — почему компилятор ведёт себя определённым образом при обнаружении UB?

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

V>И почему же в дотнете не превратилась?

Я же вам объяснил — потому что в дотнете нет UB.

V>Т.е., когда подаёшь костанту — оно даёт сразу ответ, бо JIT и AOT инлайнят маленькие методы, т.е. производят небольшую бета-редукцию.

V>Но почему в теле ф-ии остались честные вычисления от параметров? ))
Потому, что в дотнете эта функция возвращает true только для одного значения — int.MaxValue. А для всех остальных она, в соответствии со стандартом, возвращает false.
И точно так же себя ведут плюсы, когда тип аргумента не позволяет UB.

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

Вы продолжаете феерически тупить. Я такого давно не видел.
V>Плюсы без агрессивных оптимизаций тоже дают верный код, как и дотнет.
Плюсы дают верный код и с агрессивными оптимизациями. Просто он верный с т.з. стандарта.
V>Вопрос здесь не почему исполняет в случае знаковых, а почему именно так?
Повторю вопрос: почему С++ с агрессивными оптимизациями не превратил is_max<unsigned long> в xor eax, eax?
V>Я ведь ниже дал хороший пример, который должен был объяснить логику разруливания UB.
Нет, не дали.

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

А для long, значит, запрограммировали?
Всё ещё хуже, чем я думал.

V>Если прибавлять двойку — уже два сравнения, больше — больше.

Это вы вообще о чём? Какие два сравнения?

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

Понятно. Я сдаюсь — вы не способны понять правильный ответ, даже если в него ткнуть носом.

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

Это — досужие рассуждения. Было принято осознанное решение, в котором ошибкой является вызов метода на null-объекте, а не дереференсинг this.


V>Эдак тебя заносит. ))

V>Причина ранней диагностики ошибки по твоей ссылке понятна, когда еще не были развиты наработки по escape analisys.
V>А если this не уходит дальше, как в примере?
V>Почему дотнетные компилятор/JIt/AOT неспособны сделать escape analysis для простейших случаев?
JIT и AOT не имеют права это делать. Потому что семантика инструкции callvirt прописана в стандарте, и отклонение от неё — это бага.
А C# принял такое решение. Что непонятного-то?

V>Джава с такими вещами хорошо справляется уже лет 15, если не больше.

V>В рантайме в джаве, если код пойдёт по ошибочной ветке, будет просто выброшено исключение, а основная ветка выполнится без создания ненужного экземпляра объекта.
Джава была вынуждена заниматься ескейп-анализом потому, что в ней нету value типов. Из-за этого простейшие вещи, вроде комплексной арифметики, приводят к очень быстрому накоплению мусора.
Канонические примеры работы ескейп-анализа — это что-то вроде:
class Complex
{
  private double _re; public double getRe() { return _re; }
  private double _im; public double getIm() { return _im; }
  
  public Complex(double re, double im) 
  {
    _re = re;
    _im = im;
  }

  public Complex add(Complex other)
  {
    return new Complex(getRe() + other.getRe, getIm() + other.getIm()
  }
}
...

public static Complex sum(Complex[] data)
{
  Complex s = new Complex(0, 0);
  for(int i=0; i<data.length; i++)
    s = s.add(data[i]);
  return s;
}

Честное исполнение такого кода на массиве из миллиона элементов порождает миллион временных объектов, провоцируя вызов GC. Как несомненная победа преподносится то, что в методе sum JIT "понимает", что наружу s уезжает только в момент return; так что можно положить s на стек, и сконструировать объект в куче только для конечного результата.

Ну так в дотнете этого не делается в основном потому, что Complex в нём — value-тип, и s будет жить на стеке безо всякого escape analysis.
Кроме того, джава в массиве Data держит указатели на Complex, что приводит двух-трёхкратному росту потребления памяти и примерно двукратному замедлению кода итерирования по массиву (это если оптимистично предположить идеальный для джавы сценарий, когда сами экземпляры Complex лежат в памяти в том же порядке, как и ссылки на них в массиве — в силу удачной инициализации или недавней сборки мусора).
А дотнет держит экземпляры Complex прямо в массиве, без лишней косвенности и object headers.
Так что в этой гонке он выигрывает ещё до того, как джава выходит на забег.
Чтобы осмысленно рассуждать о недостатках escape analysis в дотнете, нужно показывать более убедительные примеры, чем "у меня тут джава смогла подавить один объект, а дотнет — не смог".
На производительность перемещение единичных объектов на стек не особо влияет. Влияют массовые операции, когда подавляется конструирование O(N) объектов.

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

Покажите пример, где дотнет проверяет на null дважды.

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

Всё верно, так и должно быть.
V>Т.е. nullable-подход в дотнете не является строгим. Ты можешь вернуть null или подать null в кач-ве аргумента, даже если те описаны как non-nullable.
Именно. Потому что нет гарантий, что код по обе стороны вызова скомпилирован с одной и той же конфигурацией нуллабельности.

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

Какой именно?

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

V>Нет, не херня. В каждом конкретном случае или никакой оптимизации при UB не происходит вовсе (как в MSVC), либо происходит такая оптимизация, которая считает, что ошибочная ситуация произойти не должна.
Ну так это и есть херня, т.к. поведение кода радикально меняется при оптимизации и без неё. В итоге можно успешно отлаживаться, а в релизе получить булшит.
Вот вы только что с гордостью привели пример, как здорово работает код с UB в MSVC. Ну так это и провоцирует писать код в стиле приведённого мной примера — "а чо, я же знаю, что на самом деле будет происходить!".

V>Clang традиционно более щепетилен — поэтому он всегда создаёт ненужный объект в моём сниппете.

V>Но Clang — это не про производительность, это некий референс плюсов как таковых.
Теперь уже и про производительность. Потому что в LLVM вваливают очень много ресурсов, в том числе — и в оптимизации.

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

Тут нет выделения памяти перед AV.

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

V>Но посмотрим, посмотрим...

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

Отож.

V>Т.е. в реальных проектах, где никто в здравом уме никогда не сравнивает this с nullptr, а передаёт его куда-то дальше, где уже и возникает ошибка, Clang никак не помогает.

С чего вы взяли, что "где-то уже" будет происходить ошибка? Покажите мне ошибку в коде Holder.

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

V>Ты допускаешь ill-formed код в своих рассуждениях, просто ждешь от компилятора некоей предсказуемой реакции на такой код в рантайм.

V>Т.е. утекание ошибки в продакшен — "а чо такого?" ))
Ну так вы же ровно этого же хотите от дотнета — чтобы он вам разрешил вызывать методы на null объектах. С т.з. C# это — ill-formed код.
Просто этот ill-formed код всегда ведёт себя предсказуемо. А в плюсах он может себя вести одним способом, а может — другим.

V>Это спор слепого со зрячим.

Да я уже понял.
V>Мы были слепы еще примерно в 90-х. Ближе к концу пошли анализаторы и общее понимание важности well-formed кода.
Ваша слепота частично развеялась за последние несколько дней, но ещё не до конца.
Вот буквально только что, пять дней тому, вы писали:

В данном случае ты даёшь оценку своим фантазиям, бо архитектуры/компиляторы явно определяют своё поведение при переполнении.
Т.е. UB тут для стандарта в общем, но не для конкретной связки платформы и компилятора.

То есть не понимали разницы между implementation-specific behavior и undefined behavior. А тут, гляди-ка, недели не прошло — начало брезжить понимание важности well-formed кода.

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

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

V>Помнишь себя в 2004-2005-х годах как ты вещал с трибун: "Представьте, что сейчас вы напишете код, а потом он будет исполняться намного быстре на будущих версиях платформы!!!"

V>Дудки!
Ну так самое забавное, что именно так и произошло. Вы прямо в этой ветке приводите примеры кода, который стал исполняться намного быстрее на будущих версиях платформы.

V>Сначала убери из кода потенциальные ошибки. ))

В дотнете потенциальных ошибок изначально меньше.
V>Например, в дотнете (и особенно в Джаве) есть ошибки, когда в метод подают не копию объекта, а сам объект, т.е. провоцируют убегание побочных эффектов, а компилятор помочь не может, т.к. он понятия не имеет о семантике.
Это вы сейчас про defensive copying или про что? Мне прямо интересно, есть ли пределы вашей некомпетентности.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Отредактировано 24.04.2024 6:32 Sinclair . Предыдущая версия . Еще …
Отредактировано 24.04.2024 1:57 Sinclair . Предыдущая версия .
Отредактировано 24.04.2024 1:55 Sinclair . Предыдущая версия .
Re[15]: Carbon
От: rFLY  
Дата: 24.04.24 10:47
Оценка:
Здравствуйте, netch80, Вы писали:

N>Мне как-то сложно понять её иначе, чем то, что "паскалевская" декларация не ведёт ни к чему полезному.

N>Если это не так, объясните, что имелось в виду...
Это о сишной декларации. Внезапно языкоделы начали от нее отказываться в пользу паскалевской, как будто считают, что сишный подход себя изжил и с ним отрасль зашла в тупик.
Хотя ни яве, ни шарпу, это не мешает развиваться. Или я не догоняю и это действительно мешает.
Re[15]: Carbon
От: rFLY  
Дата: 24.04.24 11:10
Оценка:
Здравствуйте, CreatorCray, Вы писали:

CC>Да банально всё идёт по спирали: просто подросли дети, которые никогда не видели паскаля и считают раз это другое и не как у всех — значит однозначно лучше существующего.

Да вариантов может быть масса, от "Все побежали и я побежал" (с), до не желания "отпугнуть" аудиторию незнакомым синтаксисом объявления.
Re[7]: Carbon
От: netch80 Украина http://netch80.dreamwidth.org/
Дата: 02.05.24 07:35
Оценка:
Здравствуйте, Alekzander, Вы писали:

A>И вот он, момент, когда бизнес-требования к диапазону выросли. Незначительно. Температура на Земле, как известно, снизу ограничена не -50, а чуть поменьше. -70 или -80, не помню точно.

A>И ты приплыл. С кодебазой, где на каждом повороте аж компилятором проверяется, что температура не меньше -50. Ты даже последовательно модули обновить не сможешь. Только перевыкатывать всю систему целиком!

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

Вот возьмём "очевидный" чистый пример на расширение такого рода. До какого-то момента в Linux был набор системных вызовов под 16-битные uid и gid и под 32-битные позиции файлов. Потом оба пришлось расширить: uid, gid — до 32 бит, off_t — до 64 бит. И что случилось? Пришлось перекомпилировать все программы? Нет, конечно. Просто добавились новые вызовы на бо́льшую ширину параметра.
Но было ли это единственной причиной добавления системных вызовов? Нет, конечно — эти дали дай чтобы 3% от общего количества. Остальные — под новый функционал.

Далее, берём протокол типа CBOR или ASN.1 BER (намеренно не беру текстовые типа JSON). Внутренние диапазоны им неинтересны, есть метод передать параметр любого размера (и целый, и float, и строку), 8 байт не проблема, и 8888 байт не проблема (если кому нужны такие безумные числа). Но значит ли это, что все проблемы решены? Конечно, нет. Кому-то надо добавить параметр, кому-то удалить, кому-то сделать его обязательным или, наоборот, опциональным, кому-то перенести в итеративный список (когда вместо одного значения на всех — разные, своё на каждый подэлемент).

Чтобы испортить впечатление, возьмём ASN.1 PER (я с ним вокруг всякого 5G возился). О, вот тут полный рост чудес. Для полей не длиннее 2 байт длина значения не передаётся, и считается в количестве битов, на границу байта не выравнивается. Влезает в 5 бит — передаём 5 бит. Надо 6 — передаём 6 бит. Будет расширение поля? Формат сломается. И что, не расширяют? Расширяют. Я сам участвовал в этом безобразии Просто технология версионности, с согласованием форматов, где требуется.

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

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

Это был момент номер раз. Момент номер два — что и на такое можно заложиться, если всего лишь помочь существующим средствам использовать то, что у них в наличии. Те же соглашения о вызовах: да, обычно кладётся одно значение в один регистр или в одну ячейку стандартного размера на стек. Как поступать, если у структур с вроде бы одинаковым именем разные правила размещения данных? Не знаю, был ли Rust первым, но в нём в названиях добавляется хэш описателя всех деталей. Можно и так.

Ну и, наконец:

A> Температура на Земле, как известно, снизу ограничена не -50, а чуть поменьше. -70 или -80, не помню точно.


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

N>>И как именно они протекут?


A>Возьми какой-то реальный сценарий. Не знаю, "Мужик фотографирует кису и отправляет фотку в мессенджере". И посмотри, во что превратится жизнь, если ты слезешь с байтов. Отринешь проклятое наследие Си. Выберешь язык с типами-диапазонами, и с нуля всё напишешь.


A>Для начала, сколько цветов будет иметь у тебя пиксел? Как у Эппл, "тысячи цветов", "миллионы цветов"? Наверно, ты всё-таки возьмёшь цветовую модель. Скажем, RGB. Спросишь у производителя матрицы, сколько он даёт градаций на канал, и напишешь тип {r: 0..64, g: 0..64, b: 0..64} (когда я последний раз интересовался вопросом, аппаратные возможности моников были именно такие, а сенсоров ещё хуже). И вот с этим ужасом тебе придётся таскаться всю дорогу.


И что в этом "ужас"? (Кстати, скорее 0..63, а не 0..64). Вообще-то обычно вывод в современных средствах идёт в диапазоне 0..255. Если в матрице урезано, то это следующий вопрос. И именно в диапазоне 0..255 работают самые старые средства работы с цветом, вроде команд MMX. Так что не так-то?

A>Сколько будет весить необработанная фотография 1920*1080? Ага, ты, конечно, в уме округлил до байтиков и радуешься. А ты попробуй как в суде присяжных: когда говорят, мол, вы должны игнорировать эту улику, она получена незаконно. Ведь твоя цель — мыслить без привязки к байтам.


И снова — весить где? В файле типа bmp? В файле типа raw? В памяти в каком из множества возможных форматов? Упакованное в файле в, например, в твоём случае в 8 мегабайт (считаю по 4 байта на пиксель) в оперативке может занимать от тех же 8 до сотен в зависимости от деталей представления.
А если это не фото с реальности, а рисовка, то вероятно, что, наоборот, через RLE, которому уже 40 лет, можно ужать сильно меньше 8MB без потери качества (используется как вариант всяких GIF и PNG).

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

A>Как получить данные? У тебя идёт от драйвера матрицы сплошной поток. И он, конечно, байтовый. Ты его остановишь и будешь перепаковывать в {r: 0..64, g: 0..64, b: 0..64}?


Останавливать я точно ничего не буду
Перепаковывать — в зависимости от задачи. Где-то это нужно. Где-то — нет. Я могу использовать packed-структуры, если это надо, но работа с ними может быть сложнее в ряде случаев. Ты вот говоришь про 6 бит на канал. Это в сколько байтов вкладывается на пиксель — 3 или 4?

A>А как кису вывести на экран? Здравствуй, for { for {} } ? В случае байтов тебе, скорее всего, максимум, придётся порядок строк инвертировать при подготовке буфера. (Это зависит от "тараканов"). В лучшем случае — просто скопировать буфер целиком.

A>Короче, куда же ты денешься от байтов с подводной-то лодки.

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

N>>Файл с данными это уже случай упаковки — в значительном смысле, тех самых тараканов нижнего уровня.

A>И что с того? Сериализация — не пример, из чего состоит работа чуть менее, чем полностью?

Нет, конечно. Совсем не для всех задач, и совсем не обязательно именно такая сериализация. Зачем мне она, например, при манипулировании JSON-структурами?
Или при работе с БД? Там этим занимается драйвер БД, я же работаю с числами и строками.

Или это у тебя такие задачи последние надцать лет, что из сериализации "состоит работа чуть менее, чем полностью"? Хм, ну бывает. У меня как-то совсем другая обстановка. Вот последняя законченная задача была и в текстовых представлениях, и в локальных структурах данных с проецированием на них (причём местами хитром).

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


A>Но хранятся-то реально данные всё равно в int'е (в реальном мире, где не изобрели чипы переменной битности).


1) При чём тут какая-то мифическая "переменная битность", откуда ты это взял? У меня такого не было.
2) Нет и ещё раз нет. Не хранятся они (подразумевается — только) в int. Данные хранятся, в основном, в группах битов. Они могут храниться в 1, 2, 4, 8 байтах, с соответствующим расширением. Они могут храниться в битовых полях (это уже как-то похоже на твою "переменную битность", но не переменную на самом деле, а просто разную). Они могут быть разорваны на несколько полей. Они могут быть сдвинуты на константу, с развёрнутым порядком байтов или битов, сложены в контейнер в виде float или в виде текста.
Про соглашения по вызову я уже писал. У тебя есть 64-битный регистр. Сложенный в него любой тип короче 64 бит должен быть знаково расширен до нужного. int? Всё равно заполняй старшие 32 бита копиями 31-го.

Или индексация. Сейчас от этого идут уже реальные эффекты на LP64 и LLP64 архитектурах, когда индекс int, он 32 бита, а режим 64-битный, и процессор берёт все 64 бита из индекса.

int moo(int* arr, int idx) {
  return arr[idx];
}

Компилируем (Linux)
moo:
        endbr64
        movslq  %esi, %rsi
        movl    (%rdi,%rsi,4), %eax
        ret


Ой, а что это за movslq вылезла? Ты всё ещё выступаешь за int?

То же на ARM:

moo:
        ldr     w0, [x0, w1, sxtw 2]
        ret


из-за топорной завязки на int по твоему образцу получился лишний sxtw.

A> А ты просто предлагаешь абстрагироваться от этого. Во имя чистоты подхода.


Я предлагаю, да, абстрагироваться от того, как компилятору вздумается представлять данные, там, где мне это не нужно.

Мне пофиг, сложит он 4 значения в диапазоне 0..1000 в один регистр или нет. Мне пофиг, применит он RADIX-50 для упаковки 3 значений диапазона -13..+26 в одно 16-битное значение, или нет. Пока это всё уровень переменных, внутренних структур, регистров передачи в функцию и назад — пусть творит что хотит, но я ему могу помочь, уточнив множество значений.

А там, где мне твёрдо надо разместить число, например, в битах 120..124 всей структуры ещё и в BE порядке бит — я это ему явно скажу и пусть соблюдает. Но тогда, если я гарантирую, что значение принадлежит набору {0,1,8,11,29,30}, он не будет писать лишнюю проверку перед укладкой в битовое поле, а если я ему подам int, он должен будет сделать эту проверку.

A>По сути, если разобраться, всё сведётся к тому, чтобы нагрузить компилятор дополнительными проверками, вместо того, чтобы проверять диапазоны в рантайме. Пока всё хорошо, да? Мины обезвредили.


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

A>>> Когда пляшут от байтиков, проблема диапазона возникает раз в несколько лет,


Она и так возникает раз в несколько лет. Почти никто не разрабатывает так, чтобы добавлять на два градуса в месяц, перестраивая на каждый раз, или чтобы добавлять по битику в поле фиксированной ширины.

A>>> и тогда берут тип экспоненциально большей размерности. И это не вечный процесс. Он прекращается, когда компьютеры становятся достаточно взрослыми. Тип общего назначения i128 уже никому не нужен.

N>>То-то он присутствует в расширениях одновременно GCC и MSVC.
A>Ты, наверно, пропустил слова "общего назначения". Может и присутствует, но не как таковой.

Что значит "не как таковой"? Тип есть, его можно применить именно как целочисленный.

A>>> Там уже вектора, SIMD и прочее подобное роялит. С паскалевским же подходом вся твоя жизнь превратится в одну большую проблему диапазонов.

N>>Вызываю обосновательную бригаду.

A>Лучше вызови поисковую бригаду, и поищи на Гитхабе примеры, где бы i128 использовался как тип общего назначения. Как ты понимаешь, мне мой тезис доказать примерами невозможно, а тебе опровергнуть примерами легко.


Сформулируй, что такое "тип общего назначения".
Если это значит целочисленный, то поиск по int128_t на гитхабе выдаёт сходу толпу таких использований. Да, они почти все библиотечные. Это неизбежно.

A>Никакие случаи "мини-вектор", "мини-структура" и т.п., конечно же, не принимаются.

A>А если окажется, что я ошибся, я просто заменю i128 на i256. Потому что изначальный тезис был — что процесс экспоненциального увеличения массово используемых типов — не бесконечен. Об этом легко забыть, если не следить за мыслью, не воспринимать ответ как единое целое, а механически дробить его на предложения и доковыриваться к каждому по отдельности (что я не слишком уважаю).

Этот намёк неадекватен.

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


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

A>Да, их тут тысячи! ©

И этот — тоже.

Zig supports arbitrary bit-width integers, referenced by using an identifier of i or u followed by digits. For example, the identifier i7 refers to a signed 7-bit integer. The maximum allowed bit-width of an integer type is 65535.


Процесс пошёл. Ну а инерция она всегда инерция...
The God is real, unless declared integer.
Re[16]: Carbon
От: netch80 Украина http://netch80.dreamwidth.org/
Дата: 02.05.24 07:43
Оценка: 3 (2)
Здравствуйте, rFLY, Вы писали:

N>>Мне как-то сложно понять её иначе, чем то, что "паскалевская" декларация не ведёт ни к чему полезному.

N>>Если это не так, объясните, что имелось в виду...
FLY>Это о сишной декларации. Внезапно языкоделы начали от нее отказываться в пользу паскалевской, как будто считают, что сишный подход себя изжил и с ним отрасль зашла в тупик.

Почему "как будто", если это так и есть?

FLY>Хотя ни яве, ни шарпу, это не мешает развиваться. Или я не догоняю и это действительно мешает.


В случае Java и С# проблема не стоит так остро потому, что там случаи, когда в C++ возникают основные конфликты, исключены иначе. Случай Most Vexing Parse это когда невозможно разобрать, что такое A B(C); — декларация функции или определение переменной через конструктор — так вот в этих двух языках декларация функции таким образом посреди исполняемого кода или задание значений переменной напрямую в объявлении класса невозможны (или специально сделаны с '=', но это сильно позже введено), и это устраняет первый конфликт. Второй, в котором надо выворачивать в голове определения сложных типов с указателями, типа int *(*moo_t)(int*) (и это ещё относительно простой случай), аналогично, устранён потому, что указателей нет.

А вот в домене, близком к C/C++, где второй конфликт неустраним, а первый требует переломки системы модульности — самым простым и дешёвым спасением видится таки разворот деклараций к паскалевскому типу, за счёт добавления лишнего слова.
The God is real, unless declared integer.
Re[13]: Carbon
От: netch80 Украина http://netch80.dreamwidth.org/
Дата: 02.05.24 08:34
Оценка:
Здравствуйте, CreatorCray, Вы писали:

CC>Здравствуйте, netch80, Вы писали:


N>>Ты, кажется, и не пытался понять, что я написал.

CC>Тебя порой сложновато понять.
CC>Теория меня интересует исключительно с точки зрения применения на практике. Всякие "внутренние красоты" и "концептуальные значения" давно уже не интересуют.

А я и говорю про чистую практику. Если ты будешь в C, а не в ассемблере, считать адреса просто беззнаковыми числами, ты быстро получишь граблями по какой-то выступающей детали.
(В ассемблере, может быть, тоже — см. pointer authentication в ARM. Но это отдельная тема.)

N>>Да-да, я помню твой рассказ про индексацию LBA где-то под NVMe.

N>>это не unsigned. Это modulo, замаскированный из-за кривости C под простой unsigned.
CC>В каком месте в концепции LBA ты увидел modulo?

А в каком месте ты в моих словах увидел, что я такое увидел?
Как постоянно с тобой, знак "facepalm" переводится "мне лень подумать даже пару секунд".
Это и про "сложновато понять".
The God is real, unless declared integer.
Re[17]: Carbon
От: CreatorCray  
Дата: 02.05.24 09:54
Оценка: +1
Здравствуйте, netch80, Вы писали:

N>самым простым и дешёвым спасением видится таки разворот деклараций к паскалевскому типу, за счёт добавления лишнего слова.

Лишняя сущность, которую надо добавлять всегда ради редкого случая неоднозначности — как по мне так не стоит оно выделки.
... << RSDN@Home 1.3.110 alpha 5 rev. 62>>
Забанили по IP, значит пора закрыть эту страницу.
Всем пока
Re[14]: Carbon
От: CreatorCray  
Дата: 02.05.24 09:54
Оценка:
Здравствуйте, netch80, Вы писали:

N>Если ты будешь в C, а не в ассемблере, считать адреса просто беззнаковыми числами, ты быстро получишь граблями по какой-то выступающей детали.

Больше 20 лет именно так и считаю, сколько надо ещё ждать твоих граблей?

N>см. pointer authentication в ARM.

Это отдельные ужимки

CC>>В каком месте в концепции LBA ты увидел modulo?

N>А в каком месте ты в моих словах увидел, что я такое увидел?

Вот тут:

N>>>LBA где-то под NVMe.
N>>>это не unsigned. Это modulo, замаскированный из-за кривости C под простой unsigned.


N>Как постоянно с тобой, знак "facepalm" переводится "мне лень подумать даже пару секунд".

Мне не интересно гадать что ты там на самом деле хотел написать. Я читаю то, что написано, понимаю как это написано, скрытые смыслы в чужой писанине искать — это ты как нить без меня.
... << RSDN@Home 1.3.110 alpha 5 rev. 62>>
Забанили по IP, значит пора закрыть эту страницу.
Всем пока
Re[37]: Carbon
От: vdimas Россия  
Дата: 03.05.24 19:44
Оценка: :))
Здравствуйте, Sinclair, Вы писали:

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

S>Ложные утверждения ликбезом быть не могут.

А что поделать, если плаваешь в стадиях трансформации кода, путаешься в терминологии?


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

S>Оставляет от всего кода xor eax, eax.

Это не оптимизирует, это выполняет подстановку в общем случае, т.е. производит бета-редукцию с заменой известных значений (распространение констант).
Я же говорю — для тебя это ликбез.

Оптимизация — это процесс преобразования/упрощения вычислений, в том числе в символьном виде.

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


V>>Оптимизаций много разных.

S>О, началось виляние.

Это продолжение ликбеза.
Если для тебя простая подстановка кода уже оптимизация, то для у меня для тебя плохие новости... ))


V>>"Вся оптимизация" — это какая?

S>Примерно вся. Распространение констант

Распространения констант без подстановок не бывает, ваш КО.


S>инлайн вызовов


Это еще не оптимизация, это штатное ср-во языка С++, где явно указывается, какие методы будут инлайными.
По умолчанию — все, определённые в теле класса, или помеченные соотв. ключевым словом определения вне класса.

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


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


А достаточно было помнить основной признак — это неявная трансформация кода.
Демонстрируешь непонимание.

При инлайне происходит явная трансформация исходника, примерно как при макроподстановке.
Разве требовалось озвучивать, что простой инлайн, без дальнейшей трансформации кода, лишь экономит на call и соотв.манипуляциях со стеком/регистрами при передаче и возврате значений и управлении фреймом стека для ф-ий высокоуровневых ЯП?

Например, global optimization — это уже неявное преобразование кода, которое может инлайнить даже то, что не заказывали инлайнить. А так же может подменять тождественные в терминах низлежащих регистров вычисления, т.к. такая оптимизация происходит уже после стирания типов, т.е. несколько тождественных с т.з. логики происходящего в регистрах ф-ий могут заменяться одной. И если подстановки — это простая бета-редукция, то на этом этапе уже эффективны альфа- и эта-преобразования.


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


Просто надо понимать, что и как ускоряется.

Например, до эпохи спекулятивных вычислений и длинной предвыборки, экономия на удалении call была существенной. Но сегодня call и jump стоят примерно ноль тактов процессора, т.к. эти операции выполняются еще на стадии предвыборки кода, а переименование регистров решает задачу манипулирования стеком (в Windows и Linux немного разные ABI для x64, но общее то, что для первых нескольких аргументов используются регистры, и только при их нехватке — стек). Оптимизировать манипуляции со стеком невозможно, потому что нельзя — это внешняя память. А вот оптимизировать распределение регистров унутре процессора — это он уже сам оптимизирует, как раз через альфа-преобразования.

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

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

Всё это именно так и называется в С++ — "кодогенерацией", потому что верно описывает происходящее.
Но кодогенерация не есть оптимизация.


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

V>>Рассуждения уровня детсада.
S>

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


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

S>Я? Нет, не понял. Поясните своими словами.

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

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

Еще момент — а вдруг тебе захочется походить по методу пошагово и убедиться, что да, ты правильно понимаешь насчёт переполнения и код твой верный?
Достаточно будет рядом с *.exe положить *.ini-файл примерно такого содержания:
[.NET Framework Debugging Control]
GenerateTrackingInfo=1
AllowOptimize=0

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

=========================

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

При этом, происходит резкое переделывание реализации базовых и прикладных либ дотнета для целей AOT-compatible и эффективного тримминга неиспользуемых типов.

Понимаешь, на сегодня практически все неагрессивные оптимизации уже выполняются.
Помнится, я долго сетовал насчёт хреновой или отсутствующей оптимизации вычислений с плавающей точкой (и уже давно перегорел с кое-какой своей идеей), но за плавучку уже взялись и буквально за 2-3 версии дотнета упрутся уже в полоток векторных оптимизаций. По крайней мере, догонят плюсы на O1 и местами на O2...

Но есть же еще O3! ))

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

Ну так что, реально поставят крест на всей технологии?
Спишут в утиль?

Ну вот ты стучал себя кулаком в грудь "да никогда такого не будет!!!" (С)
Ты ж уже не мальчик из 2005-го...
Как ты вообще мог такое задвинуть, что дотнет через единицы версий опять похоронят с концами, как это было после выхода второго дотнета в том самом 2005-м?

Я вангую, что ты сильно заблуждаешься — достаточно посмотреть на общее направление вносимых в платформу и тулчейн изменений.
Я вангую, что через 2-3 версии дотнет как платформа упрётся уже в предел неагрессивных оптимизаций, что потребует пересмотра всего того, что будет мешать агрессивным.

Заметь, как с лёгкой руки в целях АОТ отказались примерно от половины возможностей дотнета, Ы? ))
Из того, что на поверхности — JSON и XML-сериализацию приделали в виде кодогенерации, чтобы не лезть в рефлексию...

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

Да, в дотнете меньше UB, чем в С++, например, прямо в стандарте указана очередность вычисления аргументов вызовов методов (как позиционных, так и именованных), указана очередность вычисления в условных выражениях и в выражениях присваивания...

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

Сейчас вернули value-типам честные конструкторы без аргументов...
Сейчас я могу получать и запоминать для дальнейших вычислений ссылки на value-типы...
Завезли в них константность в духе С++...

Кароч, оттягивают момент борьбы с основным UB как могут, но однажды оттягивать станет нечего — не зря же активно приучают к nullable-нотации.
Однажды станет неотъемлемой частью языка, и всё...
(а не как сейчас — опциональной)

И тогда мне будет интересно посмотреть на код после AOT, где я обращался потенциально по нулевому указателю, вызывая метод, который не обращается к this или к виртуальным методам.
Будет ли тогда AOT генерировать ненужную проверку на null для ссылки, по которой не обращаются?
Делаем ставки?

==================

V>>Шаблонного/инлайного, а не любого.

S>Из любого. Я же вам дал ссылку на код, где инлайна нет, а устранение арифметики — есть.
S>Инлайн просто протаскивает оптимизацию на один шаг дальше, позволяя устранить не только арифметику, но и бранч.

Статические ф-ии или обитающие в безымянной неймспейсе автоматически инлайнятся — это тоже такая кодогенерация. ))
В любом случае, без инлайна невозможно распространение констант за пределами методов и ф-ий.
Если же ты мне показывал подставновку констант в вычисления в рамках метода-ф-ии — то это продолжения детсада а-ля Синклер.


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

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

Решил оправдать своё непонимание? ))
Абсолютно всё то же самое в императивном программировании, просто меньше допущений.

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

И да, как раз readonly-структуры и readonly-методы структур эффективно отсекают такие ограничения в процессе бета-редукции.
В плюсах этому же служит const, хотя до некоторой глубины анализа компилятор отслеживает жизненный цикл значений и безо-всяких подсказок const.

В общем, опять твоё "слышал звон".


V>>Мы тут рассуждали чуть о другом — почему компилятор ведёт себя определённым образом при обнаружении UB?

V>>И ответ там на поверхности, вообще-то.
S>Ну, так я его вам дал восемь постов назад.

Вы десятки постов выдвигали неверные предположения с коллегой, пока не ткнул тебя носом, написав код-аналог на C#, показав происходящее.
При этом напомнил, что в С++ промежуточные вычисления можно допустимо производить в более широких типах прямо по стандарту.
Именно в этом месте я ожидаю агрессивной оптимизации и для C# — это исключение лишних преобразований типов на каждом шаге вычислений.

А что ты там уже после того, как тебя ткнули носом — да какая уже разница...
Уже все лулзы были собраны. ))


V>>И почему же в дотнете не превратилась?

S>Я же вам объяснил — потому что в дотнете нет UB.

Стоп-стоп.
Какое в опу UB, если этот код не был соптимизирован на уровне байт-кода?
Т.е. компилятор не стал выводить "формулу" даже в режиме компиляции "optimize".


V>>Но почему в теле ф-ии остались честные вычисления от параметров? ))

S>Потому, что в дотнете эта функция возвращает true только для одного значения — int.MaxValue. А для всех остальных она, в соответствии со стандартом, возвращает false.

Это не отвечает на вопрос — почему компилятор C# не решил эту формулу?
Зачем он каждый божий раз выполняет вычисления, вместо сравнения с единственным числом? ))


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

S>Вы продолжаете феерически тупить. Я такого давно не видел.

Тише, тише, выпей корвалола. ))
Про эту проблему я писал еще в нулевые.
И эта проблема никуда не делась.

Даже в режиме optimize дотнет даёт практически неоптимизированный код.
Максимум что делает — это переиспользует слоты под локальные переменные, отслеживая их жизненный цикл, а так же убирает ненужные init locals и прочие мелочи, которые, вообще-то, можно было делать даже в дебажной сборке, бо это никакие не оптимизации, никакая не трансформация кода, это простое распределение ресурсов.

Поэтому, тупите тут вы с дотнетом, как и все годы до этого.

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


S>Повторю вопрос: почему С++ с агрессивными оптимизациями не превратил is_max<unsigned long> в xor eax, eax?


Тебе уже 100 раз объявняли — там unsigned, т.е. переполнение не являются UB.
А со знаковыми — допускается промежуточно вычислять в болеее широких типах для избегания переполнения.


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

S>Нет, не дали.

Дал.
Это поведение прямо по стандарту плюсов — можно расширить тип на промежуточном значении в формуле.
Просто для тебя это всё сложно, смотрю... ))

Ты путаешь значение всего выражения со значением отдельных его компонент в процессе вычисления.
Тебя же не смущает в точности аналогичный эффект в плавучке, когда 80-битные промежуточные значения давали совсем не тот результат, который получился бы на 64-битных промежуточных?
А ты думал — откуда взялось упомянутое правило?
Вот оттуда.
Иначе пришлось обрезать до 64 бит все промежуточные значения, гробя производительность вычислений.


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

S> А для long, значит, запрограммировали?

Чего? ))
При чём тут, вообще, long?

Даже если ты хочешь решить исходное уравнение для произвольного N и предположения, что типы не расширяются и это не считается за UB, то получим:
(x+N)<x
С учётом переполнения, можно записать: x>max(T)-N
С учётом знаковых/отрицательных N: x=>min(T)-N, что оно же.
Всё-равно остаётся сравнение, а стоимость перемещения в регистр через lea [cx+1] равна стоимости простого перемещения, т.е. фактического выигрыша не получается — стоп для попыток дальнейшей оптимизации.

Я ж говорю — ты не понимаешь, даже о чём ты рассуждаешь...
Тебе хочется, чтобы компилятор распознал тот уникальный вариант, когда N==1, и дя него сделал особый выверт — сравнение на равенство с единственным числом!
Ну, допустим, компилятор сам такой выверт не сделает — это должны запрограммировать программисты компилятора, из расчёта что ты, пользователь компилятора, совсем тупой, и сам не смог описать этот уникальный случай.

Ловил я лулзы именно тут, бо ты всегда веселишь, когда на голубом глазу отключаешь банальный здравый смысл.
Как бы тебе это объяснить помягче — оптимизировать имеет смысл лишь то, что имеет смысл оптимизировать.
Таких задач еще овердофига, просто отрасль не готова для их решения.
Языки не готовы.
По факту, С++ не готов, как не готов C# и дотнет.

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


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

S>Это — досужие рассуждения. Было принято осознанное решение, в котором ошибкой является вызов метода на null-объекте, а не дереференсинг this.

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


V>>Почему дотнетные компилятор/JIt/AOT неспособны сделать escape analysis для простейших случаев?

S>JIT и AOT не имеют права это делать. Потому что семантика инструкции callvirt прописана в стандарте, и отклонение от неё — это бага.

Забавно, ты еще и не понимаешь, что такое escape analysis?


V>>Джава с такими вещами хорошо справляется уже лет 15, если не больше.

V>>В рантайме в джаве, если код пойдёт по ошибочной ветке, будет просто выброшено исключение, а основная ветка выполнится без создания ненужного экземпляра объекта.
S>Джава была вынуждена заниматься ескейп-анализом потому, что в ней нету value типов. Из-за этого простейшие вещи, вроде комплексной арифметики, приводят к очень быстрому накоплению мусора.

Блин, умудряешься самому себе противоречить в соседних абзацах! ))
Апплодирую стоя.

Ликбез — escape analysis не изменяет семантику происходящего, а значит, не нарушает никаких стандартов.
В Jave есть аналогичные пункты стандартов, и что происходит при этом — написал.
Это очередной ликбез, повод помедитировать и над происходящем в JIT дотнета, и над своими наивными представлениями.


S>Ну так в дотнете этого не делается в основном потому, что Complex в нём — value-тип


Понятно, что весь RSDN давно в курсе, что ты бессовестный манипулятор, демагог и сознательно прибегающий ко лжи редиска...
Но не до такой же степени!
Побойся бога, как грится...

Escape analisys занимает доп.время, является доп. сложностью реализации, других причин там нет.
Банально не дошли еще руки у разработчиков дотнета.

Плюс, сам этот анализ нетривиален в модели регистров и ячеек стека, ведь из-за value-типов "размер" ячеек стека в дотнете может быть произвольный, в отличие от джавы, где размер ячейки условно всегда long (и сужается, где это является возможным для более простых числовых типов).

Плюс, в дотнете ссылочный тип может быть полем структуры — сама эта структура может убежать или нет и т.д. В общем, реализация для дотнета однозначно обещает быть намного сложнее, чем для Джавы. И если это оправдывает отсутствие такой оптимизации для JIT, то не оправдает для AOT, понятно.

Поэтому, всю свою демагогию сверни в трубочку и знаешь куда затем деть. ))


S>А C# принял такое решение. Что непонятного-то?


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


S>А дотнет держит экземпляры Complex прямо в массиве, без лишней косвенности и object headers.

S>Так что в этой гонке он выигрывает ещё до того, как джава выходит на забег.

Это если бы в Джаве еще были комплексные числа в базовой библиотеке...
Я в тихом шоке, если честно, от твоей непорядочности, от всех этиьх попыток манипулировать...

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

А теперь, как обстоят дела в реальности, а не в выдуманном мире Синклера:
— Джава гораздо раньше дотнета научилась пользоваться векторными регистрами современных процов.
— Дотнет этого не умел примерно в течении 20-ти лет своей жизни, позорище!
И до сих пор толком не умеет, в этом направлении движения пока мест зачаточные.
— в реальных расчётах не нужны массивы из миллиона комплексных чисел — эти вычисления почти всегда промежуточные.
— даже если нужны такие длинные данные, то в джаве их хранят очень просто — в чётных ячейках одна компонента числа, в неёчетных другая. При вычислении результат помещается в эти же ячейки (допустим, взяли любой третьесторонний библиотечный тип Complex, который обязательно будет иметь ср-ва записи и чтения из массива согласно правилам хорошего тона, принятым для этого языка), так вот, временные переменные Complex на стеке никуда не убегут, поэтому дотнет в таких задачах сливает Джаве от слова всегда.

А этот нечистоплотный коллега тупо врёт на весь интернет с весьма сомнительными целями — оправдать свою наивную позицию примерно 20-тилетней давности.
Синклер, уже давно не актуально, уже переварили и высрали.
Ты был не прав всегда.
Но тогда ты заблуждался добросовестно, от непонимания как всё устроено и работает, а сегодня ты вводишь людей в заблуждение сознательно.


S>На производительность перемещение единичных объектов на стек не особо влияет.


Что тоже ложь.
Тут как-то в начале 10-х годов соревновались с реализацией круговой очереди, я сделал её как value-type и бенчмарки показали прирост быстродействия примерно в 2.4 раза!
Помнится, AVK тогда резко среагировал, примерно на твой сегодняшиний манер... ))
Потому что тыкаешь вас фейсом в ваши порядком надоевшие предрассудки, а толку ноль.

Избавление от лишней косвенности даёт много не только в синтетических тестах.
Да, в дотнете есть value-типы, но они практически не используются для построения иерархий объектов, не приносят пользу.
Зато escape-анализ из-за них отсутствует.
В итоге, вреда больше чем пользы.
Это как обстоят дела на сегодня, если пользоваться только родными либами.
Или можно поступать на наш манер — сделать огромную библиотеку value-типов, взамен многих десятков из базовой инфраструктуры, и получать совсем другую эффективность.
Увы, ввиду того, что в дотнете пока мест нельзя переопределить оператор присваивания для value-типов, использовать эти типы приходится с аккуратностью, потому что с ними нарваться на "прикладное UB" — как два пальца об асфальт... опасный язык, на самом деле... его сложно правильно готовить...


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

S>Покажите пример, где дотнет проверяет на null дважды.

Классика жанра — как написаны унутре базовые библиотеки дотнета:
https://godbolt.org/z/o7xjqYzfj

В публичном методе обычно проверяются параметры, затем вызывается приватный или internal некий Impl-метод, где аргументы уже не проверяются в коде, но проверяются фреймворком.
Итого две проверки одного аргумента вдоль всей цепочки.
И это происходит даже на таких критических вещах, как манипулирование массивами, Span, Memory и т.д., что в итоге приходится писать свою дублирующю функциональность, в т.ч. unmanaged, ради избегания лишнего бранчинга.

Заметь, вызов NotNull заинлайнился, иначе было бы 3 проверки.
Но у таких методов специально выставляют AgressiveInlining, ес-но.


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

S>Всё верно, так и должно быть.

Вообще-то, нет.
Эта техника растёт из техники зависимых типов в некоторых языках, но в этих языках гарантии строгие.
А дотнете можно сделать так:
#nullable enable

string s = null!;

В языках с зависимыми типами в рантайме возникает исключение при невозможности привести null-значение к not-null.

Впрочем, мы это уже обсуждали, эта техника касается не только распространения гарантий тще-null, а вообще вопроса допустимых значений.
Так-то в дотнете через Unsafe.As можно присвоить управляемой ссылке любой мусор:
    private struct S
    {
        public string s;
    }

    private static void Main(string[] args) {
        IntPtr i = 124563;
        var s1 = Unsafe.As<IntPtr, S>(ref i);

        Console.WriteLine(s1.s);
    }

При этом даже не нужно компилять свой код в режиме unsafe, достаточно подключить соотв. внешнюю либу.


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

S>Именно. Потому что нет гарантий, что код по обе стороны вызова скомпилирован с одной и той же конфигурацией нуллабельности.

Это временно, ес-но.
Многие АПИ дотнета уже признаны obsolete, т.е. ничего из того, что когда-то утверждалось, не обязано быть вечным.
Разумеется, тотальный переход на nullable займёт приличное время, а потом эта опция просто исчезнет как obsolete, бо станет неотъемлимой частью языка. ))


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

S>Какой именно?

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

Для сравнения, я показывал тут как-то инлайный хелпер для плоюсов, для реализации строгой идиомы NotNull.
Как обычно, в плюсах не добавляет ничего в рантайм, не требуется изменять код, т.е. внешний АПИ умного указателя совпадает с АПИ обычного, но гарантии при этом получаются строгие.
И в язык вмешиваться не пришлось, просто используется NotNull<SomeType>, плюс удобство typedef-ов.


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


Во-первых, отлаживать можно и релизный код — поставь флаг генерацию pdb и отлаживай себе.
В дотнете аналогично, запросто можно отлаживать релизный код.

Во-вторых, в дотнете в различных UB та же херня, хотя там список UB меньше.

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

Понятно же, что знаковые целые — это компромисс, размен эффективности вычислений на ограниченность допустимых значений.
Это сейчас int по дефолту 32 бита, а мы-то выросли когда int был 16 бит, когда проблема выбора типа интегрального значения стояла в полный рост.
За long (32 бита) можно было поплатиться падением быстродействия более чем вдвое.

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

Твой дотнет тоже на плюсах писан, если что.
Да и вообще всё, хоть сколько-нибудь работающее и важное для современного IT.
Все работающие платформы пляшут от этого правила, а некий Синклер против! ))


V>>Но Clang — это не про производительность, это некий референс плюсов как таковых.

S>Теперь уже и про производительность. Потому что в LLVM вваливают очень много ресурсов, в том числе — и в оптимизации.

Да, процесс начался примерно 6-7 лет назад, это из-за WebAssembly, тулчейн которого настроен через промежуточный LLVM.
Но там еще далековато до MSVC или gcc.
https://github.com/llvm/llvm-project/releases
Дохрена еще банальных багов...
Постоянно находят и исправляют...

ИМХО, я потому плевался на поспешное введение этого WebAssembly когда-то, что готовность инфраструктуры хреновая.
Это еще не налили воды в бассейн, а уже заставляли прыгать. ))

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

Сейчас уже не актуально, т.к. MSVC опять набрал обороты и не отстаёт от всех новых стандартов — постоянно приходят обновления студии и компиляторов.

И да, в этой связи две стадии оптимизации: от исходника в LLVM-машинку и от неё в конкретную архитектуру железа.
Деньги вваливают в основном во вторую стадию, т.к. это "для всех", а в первой стадии генерации с исходника С++ CLang улучшается очень небыстро...

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


V>>Т.е. в реальных проектах, где никто в здравом уме никогда не сравнивает this с nullptr, а передаёт его куда-то дальше, где уже и возникает ошибка, Clang никак не помогает.

S>С чего вы взяли, что "где-то уже" будет происходить ошибка? Покажите мне ошибку в коде Holder.

Слушай, если сложности с пониманием, ты мог бы и сам исправить код так, чтобы возникла описанная ситуация, когда объекту был подан this=null, но объект не проверил, что this!=null (бо так никто в здравом уме не делает). Потом можно посмотреть на код и увидеть, что имелось ввиду. Ты там похвалил Clang, а я описал ситуацию, где он не поможет уже через один доп. вызов.


S>В реальных проектах такой код упадёт в рантайме, несмотря на интуитивно понятную логику и наличие проверки на null.


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

Ты, вообще, активно в Студии работаешь?
Решарпером пользуешься?
Я им уже лет почти 20 лет пользуюсь, но он до сих пор десятки раз в день выдаёт ошибки падения в Студии.
Вот тебе и "безопасный" язык...
Я плохо представляю, чтобы так много падали нейтивный программы...

Из моего опыта, C# — очень простой язык для чего-то простого и очень сложный и опасный язык для чего-то сложного.
Относительно безопасный он только, если всё расписывать исключительно в ссылочных типах, как на Джаве.
Тогда ты нарываешься лишь на предсказуемые NullReferenceExceptions, с которыми понятно что делать.
Фейерверки регулярно начинаются с применением value-типов.
Если в плюсах я могу обложить любой тип, который будет использоваться по-значению, своими описанными операторами присвоения или копирования, то в C# можно зазеваться и многими способами всё испортить. ))

Компилятор глотает, всё вроде бы работает, но с ошибками, а где шибка — а вот далеко, это тебе не null reference, которую проверяют перед каждым вызовом. ))

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

А в C# тут приходится напоминать коллегам, что конструктор без параметров для value-типов — это не то же самое, что дефолтная инициализация, и что компилятор даже не предлагает указать явную инициализацию даже при её наличии.
    struct SomeStruct() {
        public string Value = "Initialized";
    }

    class SomeClass {
        public SomeStruct S1;
        public SomeStruct S2 = new();
    }

    private static void Main(string[] args) {
        var obj = new SomeClass();

        Console.WriteLine(obj.S1.Value ?? "Oops!");
        Console.WriteLine(obj.S2.Value ?? "Oops!");

        obj.S1.Value = "!";
    }

В плюсах такой дичи нет, ес-но.


V>>Ты допускаешь ill-formed код в своих рассуждениях, просто ждешь от компилятора некоей предсказуемой реакции на такой код в рантайм.

V>>Т.е. утекание ошибки в продакшен — "а чо такого?" ))
S>Ну так вы же ровно этого же хотите от дотнета — чтобы он вам разрешил вызывать методы на null объектах. С т.з. C# это — ill-formed код.

Но есть существенная разница — для C# это условие умозрительное, искусственное.
Это из разряда защиты от дурака.
Причём, история появления этого ограничения характерна — оно появилось далеко не сразу, а как раз когда дураки обратились за такой просьбой. ))

В ссылочной семантике null часто является допустимым значением, это позволяет обходиться без игрищ с идиомой NullObject (одноимённый паттерн), тем более, что такая идиома способна раскрыться во всей красе только при наличии строгих not-null аннотаций, иначе является профанацией, т.к. проверка на null всё-равно будет генерироваться платформой, а значит, эту проверку с тем же успехом можно делать и на прикладном уровне. ))

Зато в плюсах идиома NullObject в сочетании с NotNull<T> — это 100% надёжная и эффективная весчь, бо строгое распространение гарантий позволяет исключать лишние проверки в рантайм.


S>То есть не понимали разницы между implementation-specific behavior и undefined behavior.


Походу, ты продолжаешь не понимать написанного.
Или настолько слабо признать свою неправоту?

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

UB — это не отсутствие поведения, и не обязательно ошибочное поведение.
Это неопределённое поведение, т.е. разное, в зависимости от чего угодно.

Ты там спрашивал, мол, что не так с long, т.е. с __int64?
Всё так, с тех пор как в ведущей тройке компиляторов уже лет 15 обитает встроенный тип __in128.
(у кого-то чуть раньше появился, у кого-то позже)
Просто ты был не в курсе, верно?

И если implementation-defined разное для разных компиляторов, то UB может быть разным для одного и того же компилятора.
И это прямо по стандарту.
Об этом тоже тебе было сказано.

И да, компиляторы по-прежнему специфицируют, как они действуют при переполнениях.
Неужели не веришь?

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

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

Я даже привел тебе уже пример 80-битной плавучки и 64-битной, должно было уже дойти, наконец...
Например, ты мог написать код, который давал некое ожидаемое поведение для 64-битной плавучки, но на 80-битной или 128-битной получить другие значения.

И что-то мне подсказывает, что в случае плавучки ты бы не стал позориться столько постов подряд, а сразу сделал бы умное лицо "ну это же плавучка!" и вопрос был бы закрыт, мол, "всё понятно, к плавучке вопросов нет!".
А к целым у тебя, смотрю, есть...
А оно касается не только целых, а любых типов, которыми ты оперируешь локально.

Или помнишь задачу на нахождение средних?
Наиболее дешево её решить через раширение бит.
Потому что в противном случае надо будет пользоваться делением с остатком, складывать отдельно результаты деления и отдельно остатки, а потом еще делить остатки, т.е. кол-во делений будет N+1, вместо 1. И при этой технике еще надо не забыть проверить само это N, чтобы оно было как минимум вдвое короче в битах ширины значения.
(при удвоении ширины промежуточных вычислений такая проверка не нужна, бо гарантия получается автоматом)

Есть еще способ — добавить if на каждом шаге цикла, где проверять накопительную сумму остатков на превышение N, но это совсем уж неэффективный алгоритм будет.


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

S>Вы ставите телегу впереди лошади.

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

Не задумывался, зачем есть как отдельные флаги компиляции, так и именованные готовые группы их, вот эти Ox, O1, O2, O3?
Казалось бы — достаточно указать целевое поколение процессора и просто выставить один флаг "optimize!" — ан нет!

Даже писаный на чистом Си код безо-всякого плюсового инлайна чуть ли не весь мировой код долго не мог исполнятся на O2, а тем более на O3.
Все нулевые и половину десятых шла борьба за O2!
А это ядро Linux и других Unix, это компиляторы, это код браузеров (угу, в том числе ядра Хрома, на котором сегодня пашет весь мир), это код виртуальной машины Джава, это базы данных (Postgre и Oracle) и т.д. до бесконечности.

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

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

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

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


V>>Помнишь себя в 2004-2005-х годах как ты вещал с трибун: "Представьте, что сейчас вы напишете код, а потом он будет исполняться намного быстре на будущих версиях платформы!!!"

V>>Дудки!
S>Ну так самое забавное, что именно так и произошло. Вы прямо в этой ветке приводите примеры кода, который стал исполняться намного быстрее на будущих версиях платформы.

Нет, не произошло.

Стало чуть быстрее исполняться под x64 из-за другого ABI передачи аргументов.
Т.е. стало меньше гулять данных через стек, а значит процессор может лучше исполнять код, т.к. он не имеет право выполнять спекулятивные вычисления над стеком, бо стек, он хоть и локальный, но считается внешней памятью. И не может эффективно переименовывать регистры, т.е. тусовать данные из стека.

А в x86 практически всё то же самое осталось.
А что я показал — так это отсутствие двойной инициализации одного байта нулём — оно на быстродействие толком не влияло.
И да, это всё еще не настоящая оптимизация, это банальный инлайн, содержащий баг.
И этот баг мы разбирали еще в середине 10-х годов, там достаточно было написать так:
T answer = default;
if(answer.Value) { ...

И всё работало как ожидалось.

Баг проявлялся (и продолжает проявляться в 6-м и 7-м дотнетах) в такой конструкции:
if(default(T).Value) { ...


В общем, просто исправили давний-давний баг.
Еще чуть ускорили ASCII-UNICODE преобразование, но я там так и не понял, что это было.//

Я когда-то походя за пол-дня расписал примерно полтора десятка своих таких хелперов на всю комбинаторику сочетаний типов, профит был в 3-5 раз, сейчас примерно такое же быстродействие показывают встроенные либы конверсии.

Но это не ускорение платформы, это был кривой библиотечный код, коль на той же платформе мой код работал в 3-5 раз быстрее.
А я хочу, чтобы мой код, который уже лет 20 работает в 3-5 раз быстрее некоторых встроенных либ, ускорился на 8-м дотнете.
А он нифига не ускоряется.

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


V>>Сначала убери из кода потенциальные ошибки. ))

S>В дотнете потенциальных ошибок изначально меньше.

На порядки больше, из-за обилия ссылочных типов.
Меня не далее как сегодня Решарпер и встроенные анализаторы Студии задолбали десятками оповещений, что они крашнулись, что им плохо, что их надо перезапустить...

Это какой-то пипец, положа руку на...

Да, C# и сам дотнет поначалу производили впечатление очень уютного языка и самого окружения.
Милота и только!
Это до тех пор, пока я впервые не столкнулся с доменами и ремотингом м/у ними, и не выгреб оттуда целую пачку багов, часть из которых я отправлял MS, а с частью придумывал как бороться.
За все годы мной было отправлено в MS боллее полутора десятков найденных багов (некоторые из них видел потом в обсуждениях в сети), из них только по дотнетному драйверу для их MS SQL три штуки.

Даже не могу себе представить, чтобы аналогичные баги были в нейтивном драйвере, который у них идёт в поставке.

Та блин, да только для .Net Core (а он был просто портом готовой mature-технологии) уже у меня 3 репорта и 2 одобренных pool request, а что сделал ты для дотнета?

В целом, это какой-то всё пипец, конечно, качество основной массы дотнетного кода.

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

Потому что в своих рассуждениях ты или вечный нуб, или банально злостный манипулятор в попытках оправдать свои заблуждения 20-тилетней давности.
Несерьёзны оба варианта.
Отредактировано 03.05.2024 23:36 vdimas . Предыдущая версия . Еще …
Отредактировано 03.05.2024 20:57 vdimas . Предыдущая версия .
Отредактировано 03.05.2024 20:55 vdimas . Предыдущая версия .
Отредактировано 03.05.2024 20:42 vdimas . Предыдущая версия .
Отредактировано 03.05.2024 20:12 vdimas . Предыдущая версия .
Отредактировано 03.05.2024 19:58 vdimas . Предыдущая версия .
Re[38]: Carbon
От: Sinclair Россия https://github.com/evilguest/
Дата: 04.05.24 06:05
Оценка: 2 (1)
Здравствуйте, vdimas, Вы писали:

V>А что поделать, если плаваешь в стадиях трансформации кода, путаешься в терминологии?

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

V>Это не оптимизирует, это выполняет подстановку в общем случае, т.е. производит бета-редукцию с заменой известных значений (распространение констант).

V>Я же говорю — для тебя это ликбез.
V>Оптимизация — это процесс преобразования/упрощения вычислений, в том числе в символьном виде.
1. То, что делает компилятор в рассмотренном примере (xor eax, eax), вполне укладывается даже в ваше определение. Почему вы при этом утверждаете, что он "не оптимизирует" — . Противоречите сами себе.
2. В вашем определении почему-то опущена цель преобразований. "упрощение" — не самый удачный термин, т.к. очень многие оптимизации "усложняют" код.
Вот если взять что-то общепринятое, например википедию, то там сразу будет написано

Оптимизация — модификация системы для улучшения её эффективности


V>Но, в любом случае, у него протекает дофига вычислений в рантайм, которые могли быть сделаны в compile-time, просто в рантайме эти вычисления происходят лишь однажды, что несколько нивелирует беспомощность его компилятора.

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

V>Это продолжение ликбеза.


V>Если для тебя простая подстановка кода уже оптимизация, то для у меня для тебя плохие новости... ))
Да, увы, "простая подстановка кода" — это тоже оптимизация. Одна из самых, кстати, полезных — именно потому, что разблокирует большое количество других оптимизационных техник, которые без инлайна неприменимы.

V>Распространения констант без подстановок не бывает, ваш КО.

Продолжаете стендап?
Распространение констант прекрасно работает безо всяких "подстановок". Вам привести пример кода, где инлайна нету, а constant propagation есть?

V>Это еще не оптимизация, это штатное ср-во языка С++, где явно указывается, какие методы будут инлайными.


Компилятору уже давно наплевать на ваши "указания", сделанные в рамках языка. У него — свои критерии того, что инлайнить, а что нет.

V>Демонстрируешь непонимание.

Нет, это вы не понимаете, что вам пишут.

V>Например, global optimization — это уже неявное преобразование кода, которое может инлайнить даже то, что не заказывали инлайнить.

В рассмотренном примере никто никаких инлайнов не "заказывал".
Граница между глобальной и локальной оптимизациями — только в применяемых технологиях. А в современных компиляторах зачастую и технология та же самая — вы же представляете себе механику LTCG в том же самом MSVC?

V>А так же может подменять тождественные в терминах низлежащих регистров вычисления, т.к. такая оптимизация происходит уже после стирания типов, т.е. несколько тождественных с т.з. логики происходящего в регистрах ф-ий могут заменяться одной. И если подстановки — это простая бета-редукция, то на этом этапе уже эффективны альфа- и эта-преобразования.

Склейка одинакового кода происходит ещё до фазы распределения регистров.

V>Просто надо понимать, что и как ускоряется.

Отож.
V>Например, до эпохи спекулятивных вычислений и длинной предвыборки, экономия на удалении call была существенной. Но сегодня call и jump стоят примерно ноль тактов процессора, т.к. эти операции выполняются еще на стадии предвыборки кода,
Зачем вы пишете чушь? Напишите бенчмарк и посмотрите, сколько тактов стоит call.

V>Всё это именно так и называется в С++ — "кодогенерацией", потому что верно описывает происходящее.

Никем это так в C++ не называется. Кодогенерация — это последняя фаза работы компилятора, когда строится код целевой платформы.
V>Но кодогенерация не есть оптимизация.
Смотря что использовать для генерации кода. Например, тот же rust использует для неё LLVM, что означает возможность применения ещё каких-то оптимизаций.


V>Из-за ангажированности дотнетом у тебя в голове обитает хорошо заметная каша из независимых изначально вещей, которые в твоём представлении стали неотделимыми друг от друга, смотрю. ))

Ваша проблема — в том, что вы спорите с голосами в голове. У меня нет никакой ангажированности дотнетом.
V>Например, JIT не просто инлайнит методы, он часто одновременно с этим пытается их оптимизировать.
V>Странно, что ты сам не соизволил отделить мух от котлет, ведь при запрете оптимизации и указании агрессивного инлайна он всё равно имеет право инлайнить, но не оптимизировать.
Ну так и более-менее любой C++ компилятор можно понагибать, разрешая одни оптимизации и запрещая другие.
В чём вопрос-то? В том, что вы не понимаете, что существует множество разных оптимизаций, и инлайнинг — это одна из них? Ну, да, похоже. Особенно с учётом злоупотребления терминами редукции — похоже, что вы пытаетесь все методики свести к трём выученным вами словам. Ну, так-то можно и к одному термину свести: "неявное преобразование кода". Это никак не помогает разобраться ни в том, чем одни оптимизации отличаются от других, ни в том, как именно компилятор принимает решения о применении тех или иных трансформаций.

V>Потому что C# не может позволить себе агрессивные оптимизации на уровне байт-кода.

V>Агрессивные оптимизации стирают типы, а в байт-коде типы должны оставаться.
Нда. Всё ещё хуже, чем я думал.
1. Какие именно типы стёрты вот в такой оптимизации?
public static bool is_max(long a) => a == long.MaxValue;

2. Почему моё "уравнение" также не думает решать компилятор С++? Ведь в нём агрессивные оптимизации разрешены?
Каша, каша в голове.

V>Понимаешь, на сегодня практически все неагрессивные оптимизации уже выполняются.

V>Помнится, я долго сетовал насчёт хреновой или отсутствующей оптимизации вычислений с плавающей точкой (и уже давно перегорел с кое-какой своей идеей), но за плавучку уже взялись и буквально за 2-3 версии дотнета упрутся уже в полоток векторных оптимизаций. По крайней мере, догонят плюсы на O1 и местами на O2...

V>Но есть же еще O3! ))

Хохотался. Пока вы думаете об оптимизациях в терминах "O1"/"O2"/"O3", успеха вам не видать. Вообще, формулировка "догонят плюсы на O1" уже показывает опасный уровень некомпетентности. Потому, что O1 — это опция компилятора, а не языка. И в каждом компиляторе — свой набор того, что включается на каждом из этих уровней. Иногда код с -O3 может работать медленнее, чем с -O2.
Поэтому сама идея "чем больше O, тем сильнее оптимизации" — порочна, забудьте её.


V>Ну вот ты стучал себя кулаком в грудь "да никогда такого не будет!!!" (С)

V>Как ты вообще мог такое задвинуть, что дотнет через единицы версий опять похоронят с концами, как это было после выхода второго дотнета в том самом 2005-м?
Опять голоса в голове....
Вы, если хотите меня в чём-то опровергнуть, приведите цитату. Где я утверждал про "похоронят с концами"?

V>Я вангую, что ты сильно заблуждаешься — достаточно посмотреть на общее направление вносимых в платформу и тулчейн изменений.

Да в чём именно я заблуждаюсь?
V>Я вангую, что через 2-3 версии дотнет как платформа упрётся уже в предел неагрессивных оптимизаций, что потребует пересмотра всего того, что будет мешать агрессивным.
У вас неверное понимание термина "агрессивность оптимизации". Вы его трактуете так, как будто она связана с изменением семантики. Типа, "дружелюбные" оптимизации обходятся с кодом аккуратно, а "агрессивные" изменяют его поведение, и чем агрессивнее, тем сильнее.
Нет. Агрессивность оптимизации — это просто количество усилий, потраченных оптимизатором.
У JIT агрессия зарезана искусственным образом, потому что ему нужно уложиться в ограниченное время. Для (частичного) обхода этого ограничения придумана tiered compilation. (На всякий случай, с учётом вашей ангажированности дотнетом, напишу: придумана не в дотнете, а в java, и в дотнет пока что толком не завезена).
Но даже с учётом tiered compilation никакой джит не может себе позволить тратить столько же ресурсов, сколько AOT-компилятор. Например, те же плюсы или раст.
И никакая, самая агрессивная оптимизация, не имеет права ломать семантику кода.

Поэтому все ваши рассуждения про то, что в будущих версиях дотнета целая арифметика станет вести себя как в плюсах — полная чушь. Точно так же, как и никакие версии С++ в будущем не станут превращать в xor eax, eax код функции is_max<unsigned long>. Ровно и именно поэтому — семантика жёстко задана.
И в оптимизациях, которые можно делать без жертвования семантикой, в дотнете ещё конь не валялся. Всякие loop peeling, автовекторизация, спекулятивные оптимизации — дофига ещё работы. Не на две-три версии, а, скорее, на пять-шесть. Не при нашей жизни этот колодец вычерпают.

V>Из того, что на поверхности — JSON и XML-сериализацию приделали в виде кодогенерации, чтобы не лезть в рефлексию...

Xml-сериализация существует в виде кодогенерации примерно с самого начала дотнета.

V>Да, в дотнете меньше UB, чем в С++, например, прямо в стандарте указана очередность вычисления аргументов вызовов методов (как позиционных, так и именованных), указана очередность вычисления в условных выражениях и в выражениях присваивания...

О чём и речь.

V>Но главный-то UB остаётся, причём его остаётся намного больше чем в плюсах — это разыменование ссылочных типов.

Чего? Где вы увидели UB в разыменовании ссылочных типов?

V>И тогда мне будет интересно посмотреть на код после AOT, где я обращался потенциально по нулевому указателю, вызывая метод, который не обращается к this или к виртуальным методам.

V>Будет ли тогда AOT генерировать ненужную проверку на null для ссылки, по которой не обращаются?
V>Делаем ставки?
Можем и сделать. Когда расчёт?
V>Если же ты мне показывал подставновку констант в вычисления в рамках метода-ф-ии — то это продолжения детсада а-ля Синклер.
Я дал конкретную ссылку. Где тело функции is_max<long> по-прежнему есть, call есть, но всё тело — это
xor eax, eax
ret


V>В общем, опять твоё "слышал звон".

Скорее, наоборот. Попробуйте найти упоминание бета- или alpha-редукции в исходниках компилятора С++ или llvm.

V>Вы десятки постов выдвигали неверные предположения с коллегой, пока не ткнул тебя носом, написав код-аналог на C#, показав происходящее.

)
V>При этом напомнил, что в С++ промежуточные вычисления можно допустимо производить в более широких типах прямо по стандарту.
Ну приведите же уже ссылку на пункт стандарта — вместе посмеёмся.
V>Уже все лулзы были собраны. ))
Но вы же продолжаете, правда уже не так смешно, как вначале.

V>Какое в опу UB, если этот код не был соптимизирован на уровне байт-кода?

Ну не был и не был — какая нам разница, на каком этапе проводить оптимизацию?
Кстати, кланг все оптимизации, которые мы наблюдали в этом топике, делает после уровня байт-кода.

S>>Потому, что в дотнете эта функция возвращает true только для одного значения — int.MaxValue. А для всех остальных она, в соответствии со стандартом, возвращает false.

V>Это не отвечает на вопрос — почему компилятор C# не решил эту формулу?
V>Зачем он каждый божий раз выполняет вычисления, вместо сравнения с единственным числом? ))
Хороший вопрос. Тогда давайте так — а зачем те же вычисления выполняет C++? Вместо сравнения с единственным числом?

V>Про эту проблему я писал еще в нулевые.

Проблема того, что вы тупите?

V>Тебе уже 100 раз объявняли — там unsigned, т.е. переполнение не являются UB.

Это место — верно.
V>А со знаковыми — допускается промежуточно вычислять в болеее широких типах для избегания переполнения.
А вот это — уже нет.

V>Это поведение прямо по стандарту плюсов — можно расширить тип на промежуточном значении в формуле.

V>Просто для тебя это всё сложно, смотрю... ))
Ну, раз для вас это просто, давайте разберём.
Вот у нас формула: (x+1)<x
Посмотрим, какие типы будут у промежуточных результатов. Например, для int:
(
 (
  x  // int
   + 
  1  // int
 )   // ?

 < 
 x   // ?
)    // bool

Давайте, расскажите, какие типы нужно вписать вместо ?.
Можете сослаться на пункты стандарта, которые это описывают.

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

Я-то как раз нет.
V>Тебя же не смущает в точности аналогичный эффект в плавучке, когда 80-битные промежуточные значения давали совсем не тот результат, который получился бы на 64-битных промежуточных?
Это — совершенно другой эффект. Совершенно отдельно и очень внятно описанный в стандарте языка C++.
И применяется он ровно потому, что вычисления с плавающей точкой изначально "приблизительные", и гоняться за побитовой эквивалентностью результатов нужно гораздо реже, чем за скоростью математики.

V>А ты думал — откуда взялось упомянутое правило?


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

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

S>> А для long, значит, запрограммировали?

V>Чего? ))

V>При чём тут, вообще, long?
Повторюсь: для is_max<long> генерируется вот такой код:
xor eax, eax,
ret

Как видите — тут компилятор "решил уравнение". А для is_max<unsigned long> остаются вычисления в рантайме.
Рассуждения про "право вычислять в расширенных типах" вам ничем не помогут — ведь если бы можно было так делать, то компилятор бы их точно так же применил к беззнаковому типу.
Более того — в исходной версии кода is_max всё так и есть. is_max(unsigned short x) { return (x+1) < x;} всегда будет возвращать false.
Почему же is_max(unsigned long x) { return (x+1) < x;} так себя не ведёт?

V>Я ж говорю — ты не понимаешь, даже о чём ты рассуждаешь...

Да я-то как раз понимаю, а вы продолжаете писать какую-то чушь.

V>Тебе хочется, чтобы компилятор распознал тот уникальный вариант, когда N==1, и дя него сделал особый выверт — сравнение на равенство с единственным числом!

Нет, не хочу. Это вы этого хотите — пятью абзацами выше вы требовали этого от дотнета, помните? Там это было поводом критиковать C# и JIT. А тут, оказывается, всё в порядке, "компилятор сам такой выверт не сделает".
То, чего я хочу — это чтобы вы поняли, как работает оптимизатор С++. Что вот в этом коде нет никаких "вычислений в расширенных типах", и что инлайн никак не связан с обсуждаемой оптимизацией:
#include <climits>
#include <iostream>

bool is_max(int x)
{
  return (x+1) < x;
}

typedef bool (* Check) (int);

int main()
{
   volatile Check f = is_max;
   std::cout << (f(INT_MAX) ? "ok": "fail") << std::endl;
}

(https://godbolt.org/z/qEGsnEPhY)
V>И хотя в дотнеет описано, что "просто отбрасываются биты" — это лишь описание происходящего при потере данных, хотя оно всё-равно UB, но уже на прикладном уровне.
Нет там никакого UB. Это — implementation-specific. То есть биты будут отбрасываться одинаковым образом, что в дебаге, что в релизе.

V>Банально не дошли еще руки у разработчиков дотнета.

Мы говорим об одном и том же. Вы — о том, что затраты на EA велики, а я — что выигрыш маленький.

V>Плюс, сам этот анализ нетривиален в модели регистров и ячеек стека, ведь из-за value-типов "размер" ячеек стека в дотнете может быть произвольный, в отличие от джавы, где размер ячейки условно всегда long (и сужается, где это является возможным для более простых числовых типов).

А вот это — чушь. Размер ячеек стека тут совершенно ни при чём. JIT всё равно видит его в терминах "список типов", а уж одинаковый ли там размер — дело десятое.
Для EA нужен только анализ CFG, больше там ничего нет.

V>Теряешь лицо каждый божий раз.

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

V>Это если бы в Джаве еще были комплексные числа в базовой библиотеке...

Да какая разница, в базовой или не в базовой?
На Java невозможно написать тип Complex идиоматическим образом так, чтобы он не тормозил в массовых операциях.
И escape analysis тут будет припаркой мёртвому.
Не нравится пример с Complex — ну, возьмите какой-нибудь Vector4 для 3d-графики.

V>- Джава гораздо раньше дотнета научилась пользоваться векторными регистрами современных процов.

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

V>- в реальных расчётах не нужны массивы из миллиона комплексных чисел — эти вычисления почти всегда промежуточные.


V>- даже если нужны такие длинные данные, то в джаве их хранят очень просто — в чётных ячейках одна компонента числа, в неёчетных другая.
Отож, это понятно. Техника — вполне известная, называется "дрочить вприсядку". Думаете, вы первый, кто мне рассказал про хранение произвольных данных в byte[] и о том, как здорово JIT в джаве натренирован распознавать все эти data[x] << 24 + data[x+1] << 16 + data[x+2] << 8 + data[x+3]?
Но всё же, куда удобнее записывать операции над value-типами в терминах операций над value-типами, а не в терминах манипуляций с элементами массивов.

V>При вычислении результат помещается в эти же ячейки (допустим, взяли любой третьесторонний библиотечный тип Complex, который обязательно будет иметь ср-ва записи и чтения из массива согласно правилам хорошего тона, принятым для этого языка)

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


V>, так вот, временные переменные Complex на стеке никуда не убегут, поэтому дотнет в таких задачах сливает Джаве от слова всегда.

Да вы смеётесь. Я же говорю — джава в таких задачах сливается ещё до выхода на рубеж. Хотите побенчмаркать? Опыт с интегрированием массивов на дотнете вас ничему не научил?

V>Что тоже ложь.


V>Тут как-то в начале 10-х годов соревновались с реализацией круговой очереди, я сделал её как value-type и бенчмарки показали прирост быстродействия примерно в 2.4 раза!
Надо смотреть на конкретный пример. Очень сомневаюсь, что дело было именно в "одном объекте".
V>Помнится, AVK тогда резко среагировал, примерно на твой сегодняшиний манер... ))
V>Потому что тыкаешь вас фейсом в ваши порядком надоевшие предрассудки, а толку ноль.

V>Избавление от лишней косвенности даёт много не только в синтетических тестах.
V>Да, в дотнете есть value-типы, но они практически не используются для построения иерархий объектов, не приносят пользу.
А из них особо и не построишь. Напомню — нет наследования, нет косвенности. Вся "иерархия" встраивается по месту.
V>Зато escape-анализ из-за них отсутствует.
V>В итоге, вреда больше чем пользы.
Если выигрыш в реальных задачах от escape analysis станет заметным — его тоже прикрутят. Просто дело-то именно в том, что реальный код, который мог бы сильно выиграть от EA, уже написан на value-типах.
V>Или можно поступать на наш манер — сделать огромную библиотеку value-типов, взамен многих десятков из базовой инфраструктуры, и получать совсем другую эффективность.
Можно, и именно так всё и работает. Но вообще, конечно, подозрительно. Что же это за типы из базовой инфраструктуры, которые вы переписали на value?
И почему их не переписали на value-типы прямо в Microsoft?

V>Классика жанра — как написаны унутре базовые библиотеки дотнета:

V>https://godbolt.org/z/o7xjqYzfj
1. Первую проверку вижу. G_M3253_IG02: test rdi, rdi; je SHORT G_M3253_IG04. А где вторая?
2. Вы уверены, что унутре базовых библиотек дотнета столь же щедро рассыпаны [MethodImpl(MethodImplOptions.NoInlining)]?
3. А если их убрать, то сколько будет проверок?

V>В публичном методе обычно проверяются параметры, затем вызывается приватный или internal некий Impl-метод, где аргументы уже не проверяются в коде, но проверяются фреймворком.

Как мы видим, никакой "проверки аргументов фреймворком" не возникает, если к этому не прилагать искусственных усилий: https://godbolt.org/z/svh8Gf69E

V>Итого две проверки одного аргумента вдоль всей цепочки.


V>И это происходит даже на таких критических вещах, как манипулирование массивами, Span, Memory и т.д., что в итоге приходится писать свою дублирующю функциональность, в т.ч. unmanaged, ради избегания лишнего бранчинга.
Очень, очень сомневаюсь. Массивы, спаны и т.д. прекрасно инлайнятся, а Memory заменить value-типом невозможно в силу дизайна.
V>Заметь, вызов NotNull заинлайнился, иначе было бы 3 проверки.
V>Но у таких методов специально выставляют AgressiveInlining, ес-но.
Даже без выставления всё прекрасно работает. Дотнет обязан проверить на null — дотнет проверяет на null.
При этом, если написать аналог класса Holder, который я приводил в прошлом посте, то он будет работать одинаково на любой платформе (а не так, что в MSVC он летает, а в GCC вызывает segfault).
И если даже программист перепишет код с C++ 1-в-1, то обнаружит ошибку сразу же при запуске (неважно, в дебаге или релизе), почитает интернет, и починит код так, чтобы он работал именно таким способом, каким работает код Holder на MSVC. И опять — код будет верно работать на любой платформе. При этом перформанс будет ровно таким же, как в случае "кода MSVC".
То есть дело опять не в том, что С++ типа как-то "более агрессивно" оптимизирует. А в том, что в стандарте разыменование null объявлено UB вместо IS. Было бы IS — код Holder отлично бы работал и в GCC.

V>Вообще-то, нет.

V>Эта техника растёт из техники зависимых типов в некоторых языках, но в этих языках гарантии строгие.
Понятно, откуда растёт техника. Но я имел в виду не "теоретическую достижимость", а реальное ТЗ. Которое пришлось писать с учётом реального мира.
Идеи сделать NotNull строгим были, но их пришлось отложить по причинам невозможности немедленного внедрения.
В будущем — да, возможно NotNull сделают частью системы типов, и JIT станет его учитывать при генерации кода. А пока — всё работает ровно так, как спроектировано, создавая минимум сюрпризов для прикладных разработчиков. И есть перспектива навсегда остаться с нынешней реализацией, потому что от legacy избавляться сложнее, чем хочется.

V>В языках с зависимыми типами в рантайме возникает исключение при невозможности привести null-значение к not-null.


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

V>Так-то в дотнете через Unsafe.As можно присвоить управляемой ссылке любой мусор
V>При этом даже не нужно компилять свой код в режиме unsafe, достаточно подключить соотв. внешнюю либу.
Отож. Но это — специальная возможность, про которую нужно знать; нужно иметь причины её использовать, и в документации прямо написано

his API is used to cast an object to the given type, suppressing the runtime's normal type safety checks. It is the caller's responsibility to ensure that the cast is legal.

Сравним это с C++, где в поисках UB не надо специально копать. Оно лежит прямо в знаковой арифметике, разыменовании указателей, и обращения к переменным. Крайне тяжело написать программу на C++, не используя ничего из этого.


V>Но это всё лирика, тут всё понятно — проклятое легаси, которое будет жить неизвестно сколько.

Ну, вот видите.
V>Для сравнения, я показывал тут как-то инлайный хелпер для плоюсов, для реализации строгой идиомы NotNull.
V>Как обычно, в плюсах не добавляет ничего в рантайм, не требуется изменять код, т.е. внешний АПИ умного указателя совпадает с АПИ обычного, но гарантии при этом получаются строгие.
V>И в язык вмешиваться не пришлось, просто используется NotNull<SomeType>, плюс удобство typedef-ов.
Тут главное — чтобы умный компилятор не убрал весь этот ваш хелпер, посчитав, что "указатель и так не может быть нулевым".

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

V>Во-первых, отлаживать можно и релизный код — поставь флаг генерацию pdb и отлаживай себе.
Ну, попробуйте пошагово отладить is_max, чтобы понять, как же там получается false для INT_MAX.

V>В-третьих, правильный ответ я сказал сразу же — переполнение знаковых означает потерю результата.

Что такое "потеря результата"? Там нет никакой "потери результата". Результат есть, ведь платформа в реальности выполняет переполнение совершенно конкретным способом. Или вы мне не верите? Сохраните результат в переменную, выведите на экран.
V>С тех пор так и было принято, что для упражнений над битами рекомендуется использовать беззнаковые, а знаковые — это компромисс для тех вычислений, результат которых должен уложиться в выбранную ширину переменной. Таковой консенсус сложился в отрасли с миллионами разработчиков уже очень давно, стандарт плюсов лишь фиксирует этот консенсус на бумаге.


V>И да, в этой связи две стадии оптимизации: от исходника в LLVM-машинку и от неё в конкретную архитектуру железа.

V>Деньги вваливают в основном во вторую стадию, т.к. это "для всех", а в первой стадии генерации с исходника С++ CLang улучшается очень небыстро...
Вот и я о том же. Вваливать в первую стадию — не так эффективно.

V>>>Т.е. в реальных проектах, где никто в здравом уме никогда не сравнивает this с nullptr, а передаёт его куда-то дальше, где уже и возникает ошибка, Clang никак не помогает.

S>>С чего вы взяли, что "где-то уже" будет происходить ошибка? Покажите мне ошибку в коде Holder.

V>Слушай, если сложности с пониманием, ты мог бы и сам исправить код так, чтобы возникла описанная ситуация, когда объекту был подан this=null, но объект не проверил, что this!=null (бо так никто в здравом уме не делает).

А зачем? Я пишу объект именно для того, чтобы он проверял this на null.
V>Потом можно посмотреть на код и увидеть, что имелось ввиду. Ты там похвалил Clang, а я описал ситуацию, где он не поможет уже через один доп. вызов.
Не, вы всё же покажите ошибку в коде Holder.

S>>В реальных проектах такой код упадёт в рантайме, несмотря на интуитивно понятную логику и наличие проверки на null.

V>В реальных проектах этот код упадёт в рантайме и безо-всяких оптимизаций, бо содержит ошибку.
Покажите, как он падает без оптимизаций.
Покажите, как он падает с оптимизациями под MSVC++.

V>Оптимизация может повлиять лишь на то, в какой точке возникла ошибка, т.е. как далеко код убежал от места, где ошибся программист.



V>Если в плюсах я могу обложить любой тип, который будет использоваться по-значению, своими описанными операторами присвоения или копирования, то в C# можно зазеваться и многими способами всё испортить. ))

Если под "зазеваться" вы имеете в виду Unsafe.As(), то это не так, чтобы частый способ всё себе испортить.
А если нет, то как бы присвоение корректного значения в другое место всегда даст корректное значение Дотнет не даёт просто так ошибиться при "присвоении или копировании".

V>В общем, разработка эффективного кода на C# лично у меня отнимает в разы больше квантов внимания на единицу функциональности, чем аналогичная разработка на плюсах, бо в плюсах всё просто — ошибиться невозможно. Обложился всеми перегрузками операторов, мышь не проскочит, вся семантика детерминирована.

Ну, да, вся, кроме UB.

V>А в C# тут приходится напоминать коллегам, что конструктор без параметров для value-типов — это не то же самое, что дефолтная инициализация,

V>В плюсах такой дичи нет, ес-но.
В плюсах заместо этого — просто возможность вообще ничего не инициализировать. На фоне этого то, что для value типов гарантируется default initialization — уже великий прогресс.


V>В ссылочной семантике null часто является допустимым значением, это позволяет обходиться без игрищ с идиомой NullObject (одноимённый паттерн), тем более, что такая идиома способна раскрыться во всей красе только при наличии строгих not-null аннотаций, иначе является профанацией, т.к. проверка на null всё-равно будет генерироваться платформой, а значит, эту проверку с тем же успехом можно делать и на прикладном уровне. ))

Ну вот вам Holder как пример попытки обойтись без игрищ с идиомой NullObject.

S>>То есть не понимали разницы между implementation-specific behavior и undefined behavior.

V>Походу, ты продолжаешь не понимать написанного.
Не, я-то как раз понимаю
V>Или настолько слабо признать свою неправоту?
В чём именно?
V>Еще раз, медленно — типом обсуждаемого выражения является bool, а какой тип у промежуточного значения в процессе вычисления — не ваше дело.
Да блин это просто неверно. Ваше утверждение является враньём. Типы всех промежуточных выражений чётко прописаны в стандарте.
Как вы вообще столько лет пишете на языке, основ которого не понимаете?
V>Это неопределённое поведение, т.е. разное, в зависимости от чего угодно.
Совершенно верно.

V>Всё так, с тех пор как в ведущей тройке компиляторов уже лет 15 обитает встроенный тип __in128.

V>(у кого-то чуть раньше появился, у кого-то позже)
V>Просто ты был не в курсе, верно?
Конечно в курсе Но я правильно понимаю, что ваше утверждение — компилятор ведёт вычисления для long в типе _int128?
Покажите пункт стандарта, который это описывает.

V>И если implementation-defined разное для разных компиляторов, то UB может быть разным для одного и того же компилятора.

V>Об этом тоже тебе было сказано.
Это как раз я об этом говорил вам. А вы стояли на позиции, что, дескать, это для языка целое переполнение — UB, а в конкретном компиляторе под конкретную платформу всё очень даже defined.

V>И да, компиляторы по-прежнему специфицируют, как они действуют при переполнениях.

V>Неужели не веришь?
Неа, не верю. Примеры кода из этого топика опровергают ваше утверждение.
Если бы ваша гипотеза была верна, компилятор не мог бы одновременно делать две разных вещи при переполнении.

V>И это надо учитывать при написании кода.

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

V>Например, ты мог написать код, который давал некое ожидаемое поведение для 64-битной плавучки, но на 80-битной или 128-битной получить другие значения.

Мог. И другие значения — это ожидаемое поведение. Потому, что, сюрприз-сюрприз,

Formally, the C++ standard makes no guarantee on the accuracy of floating-point operations.


V>А оно касается не только целых, а любых типов, которыми ты оперируешь локально.

Неа. Не любых, а только тех, которые описаны в стандарте.

V>Еще немного столкнёшься с подробностями — глядишь, начнёшь уважать коллег по цеху.

Я их и так уважаю.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.