JiT действительно может, но Roslyn просто не эмитит ему эти инструкции, потому что это меняет семантику, а оптимизации по определению не должны менять результат исполнения программы.
Мне было бы очень интересно посмотреть на примеры, которые стабильно эмитят tail call потестить
Должен отметить, предложение о хвостовой рекурсии есть в IL (поскольку есть в F#), но было отклонено на уровне LDM. Причина: ломает стек трейс, сильно травмирует отладку, есть комментарий Мэдса на тему.
Из планов на .NET11 известны ещё closed модификатор, nullable async/foreach, развивается unsafe блок в статический анализатор как было с nullable reference types, top-level methods и ещё всякого разного по мелочи.
Так-то да, из крупных анонсов ждем Union types и Runtime async (+extension everything), но об этом уже давно известно, ещё со времён .Net9. Самое интересное как по мне происходит в нишевых гитхаб коммитах
Вас ведь никто не спрашивал, вы посчитали очень уместным исправить по вашему мнению ошибку в определении. Знаете, это ведь не технический ресурс, а лингвистическим разбором слов пускай занимаются филологи
Полагаю требуется некий контекст, вполне есть ситуации где можно гарантировать отсутствие как освобождения в try, так и использования после finally, а исключения остаются проблемой.
Ещё finally намертво предотвращает инлайнинг, что наверно стоит иметь ввиду, раз уж мы уже говорим об оптимизациях
extension members просили довольно давно и это отличное нововведение, хотя два года я их и не ждал. Возможность расширять свойства и статические классы (вроде Math) также даёт много возможностей, которые я весьма приветствую. Уже печально известный .IsNullOrEmpty в виде свойства разойдется наверно по тысячам кодовых баз.
По некоторой причине есть категория людей, которая катастрофически не хочет давать разработчикам новую силу, потому что найдется тот, кто применит ее во зло. Видимо стакан у некоторых людей всегда наполовину пуст
Есть статическая compile-time рефлексия, она реализуется генераторами в том же C# без проблем. А вот runtime рефлексия требует переписывание кода в процессе работы приложения, что требует виртуальную машину. Это ведь крайне банальная логика
Так ведь и на C# нет популярных игровых движков, Unity компилирует C# в C++ код под капотом, он вообще не использует CLR даже в debug режиме, шарпы там не нативные. И использует его юнити потому что IL удобный и синтаксис простой, а не потому что производительность.
В C# есть очень мощный контроль, можно писать через указатели если нужно и стандартная библиотека этим пользуется. Там все оптимизировано вусмерть, часть вообще написана на C. У джавы ситуация другая, она не даёт программистам инструментов, но JVM берет эту ответственность за себя и решает за программиста где и что она будет оптимизировать.
Если очень просто, C# будет производительней и дешевле по памяти, если его реально оптимизировать, но обычный "просто рабочий" код высока вероятность будет лучше и дешевле по памяти на Java. Что мы собственно и видим со стандартным стеком.
Это касается ссылочных типов тоже, но на пользовательских классах может сломаться escape-analysis и JiT решит, что выделять на стеке небезопасно. "Небольшие размеры" очень грубо это около 10000 элементов. В целом не сомневаюсь, что в .Net11 список сильно расширится.
Аллоцировать массивы на стек можно было ещё в .Net9, но это касалось только знаковых примитивов и часто не срабатывало, поэтому и хайпа избежало. Сейчас это полноправная фишка
Причина в ограничении размера в том, что стек ограничен в размере операционкой, около 8Мб. Я здесь уже спекулирую, но я думаю на кучу уходят объекты, которые должны попасть в Large object heap, а это 80 килобайт
BCL написан через Span и ArrayPool во многом, но делегаты, и перечислители так заменить было нельзя (и ещё ряд объектов, так сходу не вспомню). Больше всего счастливо от этих изменений LINQ, хотя у них были ещё свои оптимизации сверху, они добавили больше реализаций перечислителей для частных случаев в цепочках.
И вы не плюйтесь на JVM, она в разработке намного дольше CLR. JiT научился грузить массивы на стек, но Java могла так уже очень давно и даже больше. Из-за отсутствия структур может показаться, что все выделяется на куче, но в реальности как раз JVM может выделять ваше классы на стеке, если считает что это безопасно.
C# производительный за счёт языковых фич, за счёт очень низкоуровневой библиотеки и штук вроде async await и true generics. Java имеет ужасный дизайн языка, но сама JVM очень крутая технология. Даже с точки зрения самого банального — инлайнинга
Впрочем команда .Net в последние годы выпускают больше оптимизаций, чем любой другой язык, так что этот разрыв стремительно сокращается и в некоторых областях CLR действительно начала выигрывать, что до недавнего времени было не так
Мне кажется что люди во многом недооценивают важность этого релиза
Выделение коллекций на стеке это game changer. Многие думают, "когда мне вообще нужно выделять такие буферы, это редкость? Да и есть ArrayPool для этого", но здесь речь не только про массивы, как можно подумать. RyuJiT научился аллоцировать на стек и сами объекты, те же делегаты(да, делегаты в некотором смысле коллекции) или Enumerator могут быть стек аллоцированными. И список будет только расширяться.
И тут стоит обратить внимание, что это используется везде в BCL, от этих оптимизаций выигрывает огромная часть стандартной библиотеки, давая бесплатный прирост производительности почти везде.
Фантастика! Я ждал этого ещё со времён анонса .NET9.
Когда этот барьер наконец-то преодолен, это открывает множество возможностей для оптимизаций в будущем. CLR догоняет JVM с огромной скоростью. Реальный список оптимизаций просто километровый, советую почитать официальные статьи на тему.
И мы уже знаем о планах runtime async и discriminated Unions в .NET11! Когда я начинаю думать "ну куда дальше", выходит Мэдс и предлагает новый game changer.
В общем бэнгер, а не релиз, закрывает старые гештальты. Вымирающий пример случая, когда Microsoft сделала что-то как надо
В Native AOT есть рефлексия! Но только ее урезанная версия, например typeof и Enum.Parse<T> будут работать, эти данные сохраняются. Это просто статические метаданные, они нужны банально для работы BCL.
Впрочем речь здесь явно не об этом, поэтому стоит упомянуть, что это невозможно, это логически невыполнимо. Native AoT прекомпилирует все методы и большинство метаданных удаляется, в этом весь ее смысл. Также как в C++ не может быть рефлексии, при ahead of time сборке в C# ее также не может быть. Рефлексия следствие JiT компилятора и везде где есть рефлексия есть JiT (ну или интерпритация)
Довольно поздно, но об этом как-то не упоминают, поэтому оставлю здесь так сказать "для будущих поколений".
Одна из критических особенностей Span заключается в обобщении коллекций, даже не оптимизации.
Можно написать метод T Sum (ReadOnlySpan<T> data) where T : INumber<T> и вызывать его для любой коллекции, представленной как последовательный блок памяти. Работает для T[], List<T>, IList<T>, ReadOnlyCollection<T> и даже T*
Без Span это бы требовало дубликации кода и огромная часть BCL написана именно так. Раньше им приходилось писать методы на C/C++ и дергать их с PInvoke. Конечно, это не что-то новое, просто теперь работать с этим несколько проще.
В данном простом примере откровенно говоря было бы проще сделать абстрактный класс с protected полем и унаследовать оба от него. А глобально мы создаём не очевидные зависимости и раздуваем код сложно читаемыми (по крайней мере для стороннего программиста) атрибутами и псевдонимами. Решение, имхо, вызывает больше проблем, чем проблема internal поле, которое оно решает. Keep It Simple Stupid. Может быть грубовато, но на западе такое зовут overengineered garbage code.
Генераторы кода вещь крутая, несомненно, но для замены рефлексии, а подобным выкрутасам я ещё должен увидеть применение. Возможно в будущем кто-то докажет мою неправоту. А причина почему в C# нет friend не функциональность, а скорее безопасность, это нарушает контракт private и ломает гарантии, в том числе для JiT
assert имеет на самом деле весьма узкое применение, аргументация о падении программы в дебаге понятна, но в дебаге и не суть важно, если она продолжит работать некорректно, но с логами. А вот отсутствие проверок в рантайме может привести к катастрофическим последствиям. И я могу сходу представить огромную палитру багов от разработчика, которые очень сильно пострадают от assert. assert актуален только для стабильных compile-time ошибок, что большая редкость.
Допустим у нас какая-то data-driven разработка, мы читаем информацию из xml, собранного нами же самими. Делается assert на ошибку разработчика, мы множество раз протестировали в дебаге все работает. А потом в рантайме файл меняется, assert в релизе не срабатывает и у нас посыпалась вся логика без какой либо обработки. И все что бы сохранить парочку cpu инструкций!
Не чушь ли, товарищи. assert имеет место быть, но в узком направлении ошибок, статья гипер-обобщает, на мой взгляд
Продолжать лить, вывести предупреждение, что нужно срочно съехать на обочину и остановить машину, разумеется. Смысл таких обработок, что бы смягчить последствия и дать минимальную работоспособность для ручной обработки пользователем. Мгновенный abort практически всегда вызывает куда более катастрофические последствия, чем некорректная работа, в данном случае авария с потенциально летальным исходом
abort всегда можно вызвать и без assert, если нужно, но с ним как раз получится, что указанная ошибка в релизе обработана не будет вовсе
Это действительно байки, поскольку режима у ленивой компиляции в .Net всего два.
RuyJiT собирает метод максимально быстро и пытается его оптимизировать, если считает его важным (обычно один раз!). При этом при такой рекомпиляции могут возникать проблемы всех цветов и оттенков. Например при функциях с узкими циклами компилятор выполняет OSR (on-stack replacement), т.е. компилирует две версии функции и выполняет jump из одного в другую прямо во время его выполнения. Технология то крутая! Но она не поддерживает PGO и получается комическая ситуация, где метод с циклом в 5000 итераций выделяет массив на стеке, а на 50000 итераций массив выделяется на куче, поскольку OSR сломала escape-analysis, который часть PGO.
Сейчас многие подобные угловые случаи активно чинятся и команда .Net выкатывает больше оптимизаций, чем любой другой язык в последнее время, но текущие решения очень далеки от идеального кода, изредка даже от Джавы.
И это в пределах x86, сборки под ARM известно менее оптимальны (хотя и неплохие), а вот RISC-V это тихий ужас, лишённый даже базовых оптимизаций.
Так что в целом ты вы правы, но винить GC стоит не всегда. Java и C# оба умеют аллоцировать классы и коллекции на стеке, а в .NET и вовсе можно написать функциональный код без единого вызова GC, главное захотеть
С каких это пор проекты на тысячи строк стали 'огромными'? Можно согласится, что программу однодневку они набросают быстро и возможно даже сносно, но мало кто подразумевает подобные мелочи, говоря об эффективности ИИ. Как только проект превышает 10,000 строк, контекстное окно просто не вывозит. Дело даже не в том что код плохой получается, ИИ часто в целом не способна выдать рабочую функцию, поскольку не понимает контекста проекта. И это далеко ведь не Энтерпрайз, это примерно уровень пет-проекта на недельку-две. Все что меньше делается тяп-ляп по определению, поскольку подобный проект особо не собираются сопровождать и поддерживать. Конечно, это можно пытаться компенсировать мультиагентами, умными промтами и контролировать передаваемый контекст, но это все пластырь на пулевое ранение. Становится терпимей, но фундаментально все тоже самое
JiT действительно может, но Roslyn просто не эмитит ему эти инструкции, потому что это меняет семантику, а оптимизации по определению не должны менять результат исполнения программы.
Мне было бы очень интересно посмотреть на примеры, которые стабильно эмитят tail call потестить
Должен отметить, предложение о хвостовой рекурсии есть в IL (поскольку есть в F#), но было отклонено на уровне LDM. Причина: ломает стек трейс, сильно травмирует отладку, есть комментарий Мэдса на тему.
Из планов на .NET11 известны ещё closed модификатор, nullable async/foreach, развивается unsafe блок в статический анализатор как было с nullable reference types, top-level methods и ещё всякого разного по мелочи.
Так-то да, из крупных анонсов ждем Union types и Runtime async (+extension everything), но об этом уже давно известно, ещё со времён .Net9. Самое интересное как по мне происходит в нишевых гитхаб коммитах
Вас ведь никто не спрашивал, вы посчитали очень уместным исправить по вашему мнению ошибку в определении. Знаете, это ведь не технический ресурс, а лингвистическим разбором слов пускай занимаются филологи
Полагаю требуется некий контекст, вполне есть ситуации где можно гарантировать отсутствие как освобождения в try, так и использования после finally, а исключения остаются проблемой.
Ещё finally намертво предотвращает инлайнинг, что наверно стоит иметь ввиду, раз уж мы уже говорим об оптимизациях
extension members просили довольно давно и это отличное нововведение, хотя два года я их и не ждал. Возможность расширять свойства и статические классы (вроде Math) также даёт много возможностей, которые я весьма приветствую. Уже печально известный .IsNullOrEmpty в виде свойства разойдется наверно по тысячам кодовых баз.
По некоторой причине есть категория людей, которая катастрофически не хочет давать разработчикам новую силу, потому что найдется тот, кто применит ее во зло. Видимо стакан у некоторых людей всегда наполовину пуст
В go compile time рефлексия, это просто метаданные. В C# runtime рефлексия (RTTI). C# поддерживает статическую рефлексию в native aot уже!
В общем ваши слова не противоречат моим, а мои не противоречат вашим. Было бы проще, если бы немного внимательнее читали написанное
Есть статическая compile-time рефлексия, она реализуется генераторами в том же C# без проблем. А вот runtime рефлексия требует переписывание кода в процессе работы приложения, что требует виртуальную машину. Это ведь крайне банальная логика
Например Array.Sort написан на C. Или по крайней мере был написан, так сразу с головы тяжело сказать.
Да, в шарпе больше контроля, Шарп крутой. Но это не вина JVM и не заслуга CLR, это вина самого языка
Так ведь и на C# нет популярных игровых движков, Unity компилирует C# в C++ код под капотом, он вообще не использует CLR даже в debug режиме, шарпы там не нативные. И использует его юнити потому что IL удобный и синтаксис простой, а не потому что производительность.
В C# есть очень мощный контроль, можно писать через указатели если нужно и стандартная библиотека этим пользуется. Там все оптимизировано вусмерть, часть вообще написана на C. У джавы ситуация другая, она не даёт программистам инструментов, но JVM берет эту ответственность за себя и решает за программиста где и что она будет оптимизировать.
Если очень просто, C# будет производительней и дешевле по памяти, если его реально оптимизировать, но обычный "просто рабочий" код высока вероятность будет лучше и дешевле по памяти на Java. Что мы собственно и видим со стандартным стеком.
stackalloc это явная ref struct, а здесь на стек отправляется стандартный int[], большая разница
Это касается ссылочных типов тоже, но на пользовательских классах может сломаться escape-analysis и JiT решит, что выделять на стеке небезопасно. "Небольшие размеры" очень грубо это около 10000 элементов. В целом не сомневаюсь, что в .Net11 список сильно расширится.
Аллоцировать массивы на стек можно было ещё в .Net9, но это касалось только знаковых примитивов и часто не срабатывало, поэтому и хайпа избежало. Сейчас это полноправная фишка
Причина в ограничении размера в том, что стек ограничен в размере операционкой, около 8Мб. Я здесь уже спекулирую, но я думаю на кучу уходят объекты, которые должны попасть в Large object heap, а это 80 килобайт
BCL написан через Span и ArrayPool во многом, но делегаты, и перечислители так заменить было нельзя (и ещё ряд объектов, так сходу не вспомню). Больше всего счастливо от этих изменений LINQ, хотя у них были ещё свои оптимизации сверху, они добавили больше реализаций перечислителей для частных случаев в цепочках.
И вы не плюйтесь на JVM, она в разработке намного дольше CLR. JiT научился грузить массивы на стек, но Java могла так уже очень давно и даже больше. Из-за отсутствия структур может показаться, что все выделяется на куче, но в реальности как раз JVM может выделять ваше классы на стеке, если считает что это безопасно.
C# производительный за счёт языковых фич, за счёт очень низкоуровневой библиотеки и штук вроде async await и true generics. Java имеет ужасный дизайн языка, но сама JVM очень крутая технология. Даже с точки зрения самого банального — инлайнинга
Впрочем команда .Net в последние годы выпускают больше оптимизаций, чем любой другой язык, так что этот разрыв стремительно сокращается и в некоторых областях CLR действительно начала выигрывать, что до недавнего времени было не так
Мне кажется что люди во многом недооценивают важность этого релиза
Выделение коллекций на стеке это game changer. Многие думают, "когда мне вообще нужно выделять такие буферы, это редкость? Да и есть ArrayPool для этого", но здесь речь не только про массивы, как можно подумать. RyuJiT научился аллоцировать на стек и сами объекты, те же делегаты(да, делегаты в некотором смысле коллекции) или Enumerator могут быть стек аллоцированными. И список будет только расширяться.
И тут стоит обратить внимание, что это используется везде в BCL, от этих оптимизаций выигрывает огромная часть стандартной библиотеки, давая бесплатный прирост производительности почти везде.
Фантастика! Я ждал этого ещё со времён анонса .NET9.
Когда этот барьер наконец-то преодолен, это открывает множество возможностей для оптимизаций в будущем. CLR догоняет JVM с огромной скоростью. Реальный список оптимизаций просто километровый, советую почитать официальные статьи на тему.
И мы уже знаем о планах runtime async и discriminated Unions в .NET11! Когда я начинаю думать "ну куда дальше", выходит Мэдс и предлагает новый game changer.
В общем бэнгер, а не релиз, закрывает старые гештальты. Вымирающий пример случая, когда Microsoft сделала что-то как надо
В Native AOT есть рефлексия! Но только ее урезанная версия, например typeof и Enum.Parse<T> будут работать, эти данные сохраняются. Это просто статические метаданные, они нужны банально для работы BCL.
Впрочем речь здесь явно не об этом, поэтому стоит упомянуть, что это невозможно, это логически невыполнимо. Native AoT прекомпилирует все методы и большинство метаданных удаляется, в этом весь ее смысл. Также как в C++ не может быть рефлексии, при ahead of time сборке в C# ее также не может быть. Рефлексия следствие JiT компилятора и везде где есть рефлексия есть JiT (ну или интерпритация)
Довольно поздно, но об этом как-то не упоминают, поэтому оставлю здесь так сказать "для будущих поколений".
Одна из критических особенностей Span заключается в обобщении коллекций, даже не оптимизации.
Можно написать метод T Sum (ReadOnlySpan<T> data) where T : INumber<T> и вызывать его для любой коллекции, представленной как последовательный блок памяти. Работает для T[], List<T>, IList<T>, ReadOnlyCollection<T> и даже T*
Без Span это бы требовало дубликации кода и огромная часть BCL написана именно так. Раньше им приходилось писать методы на C/C++ и дергать их с PInvoke. Конечно, это не что-то новое, просто теперь работать с этим несколько проще.
Сложное решение несуществующей проблемы.
В данном простом примере откровенно говоря было бы проще сделать абстрактный класс с protected полем и унаследовать оба от него. А глобально мы создаём не очевидные зависимости и раздуваем код сложно читаемыми (по крайней мере для стороннего программиста) атрибутами и псевдонимами. Решение, имхо, вызывает больше проблем, чем проблема internal поле, которое оно решает. Keep It Simple Stupid. Может быть грубовато, но на западе такое зовут overengineered garbage code.
Генераторы кода вещь крутая, несомненно, но для замены рефлексии, а подобным выкрутасам я ещё должен увидеть применение. Возможно в будущем кто-то докажет мою неправоту. А причина почему в C# нет friend не функциональность, а скорее безопасность, это нарушает контракт private и ломает гарантии, в том числе для JiT
assert имеет на самом деле весьма узкое применение, аргументация о падении программы в дебаге понятна, но в дебаге и не суть важно, если она продолжит работать некорректно, но с логами. А вот отсутствие проверок в рантайме может привести к катастрофическим последствиям. И я могу сходу представить огромную палитру багов от разработчика, которые очень сильно пострадают от assert. assert актуален только для стабильных compile-time ошибок, что большая редкость.
Допустим у нас какая-то data-driven разработка, мы читаем информацию из xml, собранного нами же самими. Делается assert на ошибку разработчика, мы множество раз протестировали в дебаге все работает. А потом в рантайме файл меняется, assert в релизе не срабатывает и у нас посыпалась вся логика без какой либо обработки. И все что бы сохранить парочку cpu инструкций!
Не чушь ли, товарищи. assert имеет место быть, но в узком направлении ошибок, статья гипер-обобщает, на мой взгляд
Продолжать лить, вывести предупреждение, что нужно срочно съехать на обочину и остановить машину, разумеется. Смысл таких обработок, что бы смягчить последствия и дать минимальную работоспособность для ручной обработки пользователем. Мгновенный abort практически всегда вызывает куда более катастрофические последствия, чем некорректная работа, в данном случае авария с потенциально летальным исходом
abort всегда можно вызвать и без assert, если нужно, но с ним как раз получится, что указанная ошибка в релизе обработана не будет вовсе
Это действительно байки, поскольку режима у ленивой компиляции в .Net всего два.
RuyJiT собирает метод максимально быстро и пытается его оптимизировать, если считает его важным (обычно один раз!). При этом при такой рекомпиляции могут возникать проблемы всех цветов и оттенков. Например при функциях с узкими циклами компилятор выполняет OSR (on-stack replacement), т.е. компилирует две версии функции и выполняет jump из одного в другую прямо во время его выполнения. Технология то крутая! Но она не поддерживает PGO и получается комическая ситуация, где метод с циклом в 5000 итераций выделяет массив на стеке, а на 50000 итераций массив выделяется на куче, поскольку OSR сломала escape-analysis, который часть PGO.
Сейчас многие подобные угловые случаи активно чинятся и команда .Net выкатывает больше оптимизаций, чем любой другой язык в последнее время, но текущие решения очень далеки от идеального кода, изредка даже от Джавы.
И это в пределах x86, сборки под ARM известно менее оптимальны (хотя и неплохие), а вот RISC-V это тихий ужас, лишённый даже базовых оптимизаций.
Так что в целом ты вы правы, но винить GC стоит не всегда. Java и C# оба умеют аллоцировать классы и коллекции на стеке, а в .NET и вовсе можно написать функциональный код без единого вызова GC, главное захотеть
С каких это пор проекты на тысячи строк стали 'огромными'? Можно согласится, что программу однодневку они набросают быстро и возможно даже сносно, но мало кто подразумевает подобные мелочи, говоря об эффективности ИИ. Как только проект превышает 10,000 строк, контекстное окно просто не вывозит. Дело даже не в том что код плохой получается, ИИ часто в целом не способна выдать рабочую функцию, поскольку не понимает контекста проекта. И это далеко ведь не Энтерпрайз, это примерно уровень пет-проекта на недельку-две. Все что меньше делается тяп-ляп по определению, поскольку подобный проект особо не собираются сопровождать и поддерживать. Конечно, это можно пытаться компенсировать мультиагентами, умными промтами и контролировать передаваемый контекст, но это все пластырь на пулевое ранение. Становится терпимей, но фундаментально все тоже самое