Здравствуйте, 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-тилетней давности.
Несерьёзны оба варианта.