Оптимизации в JIT-компиляторе для .NET 5

    Некоторое время назад я начал удивительное путешествие в мир JIT-компилятора с целью найти места, куда можно засунуть свои руки и что-нибудь ускорить, т.к. по ходу основной работы накопился небольшой багаж знаний в LLVM и его оптимизаций. В этой статье я хотел бы поделиться списком моих улучшений в JIT (в .NET он называется RyuJIT в честь какого-то дракона или аниме — я не разобрался), большая часть которых уже попала в master и будет доступна в .NET (Core) 5. Мои оптимизации затрагивают разные фазы JIT, которые очень схематично можно показать следующим образом:



    Как видно из схемы, JIT — это отдельный модуль, связанный с рантаймом узким Jit-Interface, по которому JIT консультируется по некоторым вещам, например, можно ли скастить один класс к другому. Чем позже JIT компилирует метод в слой Tier1, тем больше информации может предоставить рантайм, например, что static readonly поля можно заменить константой, т.к. класс уже статически проинициализирован.

    Итак, начнем по списку.

    PR #1817: Оптимизации boxing/unboxing в pattern matching


    Фаза: Importer
    Многие новые фичи C# частенько грешат вставкой CIL-опкодов box/unbox. Это очень затратная операция, которая является по сути аллокацией нового объекта на куче, копированием значения из стека в него, а потом еще и нагружает GC в итоге. В JIT уже есть ряд оптимизаций для этого случая, но я нашел пропущенные в C# 8 pattern matching, например:

    public static int Case1<T>(T o)
    {
        if (o is int x)
            return x;
        return 0;
    }
    
    public static int Case2<T>(T o) => o is int n ? n : 42;
    
    public static int Case3<T>(T o)
    {
        return o switch
        {
            int n => n,
            string str => str.Length,
            _ => 0
        };
    }

    И посмотрим asm-кодген до моей оптимизации (например, для int специализации) для всех трех методов:



    А теперь после моего улучшения:



    Дело в том, что оптимизация нашла паттерны IL кода

    box !!T
    isinst Type1
    unbox.any Type2

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

    PR #1157 typeof(T).IsValueType ⇨ true/false


    Фаза: Importer
    Тут я обучил JIT сразу заменять Type.IsValueType на константу если это возможно. Это минус вызов и возможность вырезать целые условия и ветки в дальнейшем, пример:

    void Foo<T>()
    {
        if (!typeof(T).IsValueType)
            Console.WriteLine("not a valuetype");
    }

    И посмотрим кодген для Foo<int> специализации до улучшения:



    И после улучшения:



    Тоже самое можно сделать и с другими свойствами Type если это необходимо.

    PR #1157 typeof(T1).IsAssignableFrom(typeof(T2)) ⇨ true/false


    Фаза: Importer
    Практически тоже самое — теперь можно проверять на принадлежность к иерархии в генерик методах без страха, что это не оптимизируется, пример:

    void Foo<T1, T2>()
    {
        if (!typeof(T1).IsAssignableFrom(typeof(T2)))
            Console.WriteLine("T1 is not assignable from T2");
    }

    Точно так же заменится на константу true/false и условие может быть удалено целиком. В таких оптимизациях, конечно, не без корнер-кейсов, о которых надо всегда помнить: System.__Canon shared генерики, массивы, ко(нтр)вариантность, нуллаблы, COM-объекты и т.п.

    PR #1378 "Hello".Length ⇨ 5


    Фаза: Importer
    Не смотря на то, что оптимизация максимально очевидная и простая, для реализации её в JIT-e пришлось хорошенько попотеть. Все дело в том, что JIT не знал о содержимом строки, он видел строковые литералы (GT_CNS_STR), но ничего не знал о конкретном содержимом строк. Пришлось помочь ему путем обращения в VM (расширить вышеупомянутый JIT-Interface), а сама оптимизация по сути — это несколько строк кода. Юзкейсов довольно много, помимо очевидных, таких как: str.IndexOf("foo") + "foo".Length до неочевидных, в которых задействован инлайнинг (напоминаю: Roslyn не занимается инлайнингом, поэтому эта оптимизация в нем была бы неэффективна, в прочем, как и все другие), пример:

    bool Validate(string str) => str.Length > 0 && str.Length <= 100;
    
    bool Test() => Validate("Hello");

    Посмотрим на кодген для Test (Validate заинлайнился):



    а теперь кодген после добавления оптимизации:



    Т.е. заинлайнили метод, заменили переменные строковыми литералами, заменили .Length от литералов на реальные длины строк, зафолдили константы, удалили мертвый код. Кстати, так как теперь JIT может проверять содержимое строки, то открылись двери для других оптимизаций связанных со строковыми литералами. Сама оптимизация была упомянута в анонсе первого превью .NET 5.0: devblogs.microsoft.com/dotnet/announcing-net-5-0-preview-1 в разделе Code quality improvements in RyuJIT.

    PR #1644: Оптимизации баунд чеков.


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

    int Foo(int[] array, int index)
    {
        // if ((uint) array.Length <= (uint) index)
        //     throw new IndexOutOfRangeException();
        return array[index];
    }

    Такая проверка полезна, но она может сильно повлиять на производительность: во-первых, добавляет операцию сравнения и делает ваш безбранчевый код бранчевым, во-вторых — добавляет в ваш метод код вызова исключения со всеми вытекающими. Однако, JIT способен во многих случаях убрать эти проверки если сам себе докажет, что индекс никогда не выйдет за пределы и так, или что уже есть какая-то другая проверка и еще одну добавлять уже не нужно — Bounds(Range) Check Elimination. Я нашел несколько случаев, в которых он не справляется и поправил их (и в дальнейшем планирую еще несколько улучшений этой фазы).

    var item = array[index & mask];

    Вот в этом коде, я подсказываю JIT что & mask по сути ограничивает индекс сверху на значение mask, т.е. если JIT-у известно значение mask и длина массива — можно не вставлять баунд чек. Тоже самое для операций %, (& x >> y). Пример использования этой оптимизации в aspnetcore.
    Так же, если мы знаем что в нашем массиве, например, 256 элементов и более, то если наш неизвестный индексер имеет тип byte — тот как бы он не старался, он никогда не сможет выйти out of bounds. PR: github.com/dotnet/coreclr/pull/25912

    PR #24584: x / 2 ⇨ x * 0.5


    Фаза: Morph
    C этого PR и началось моё удивительное погружение в мир JIT оптимизаций. Операция «деление» медленнее, чем операция «умножение» (а если для целых чисел так и вообще — на порядок). Работает для констант только равных степени двойки, пример:

    static float DivideBy2(float x) => x / 2; // = x * 0.5; 

    Кодген до оптимизации:



    и после:



    Если сравним эти две инструкции для Haswell то всё станет понятно:

    vdivss (Latency: 10-20,  R.Throughput: 7-14)
    vmulss (Latency:     5,  R.Throughput:  0.5)
    

    Далее последуют оптимизации, которые еще в стадии code-review и не факт, что будут приняты.

    PR #31978: Math.Pow(x, 2) ⇨ x * x


    Фаза: Importer
    Тут всё просто: вместо вызова pow(f) для довольно популярного случая, когда степень — константа 2 (ну и бесплатно еще для 1, -1, 0) можно развернуть в простое x * x. Можно разворачивать и любые другие степени, но для этого необходимо подождать реализации режима “быстрая математика” в .NET, при котором можно пренебречь спецификацией IEEE-754 в угоду производительности. Пример:

    static float Pow2(float x) => MathF.Pow(x, 2);

    Кодген до оптимизации:



    и после:



    PR #33024: x * 2 ⇨ x + x


    Фаза: Lowering
    Тоже довольно простая микро(нано) пипхол-оптимизация, позволяет выполнить умножение на 2 без загрузки константы в регистр.

    static float MultiplyBy2(float x) => x * 2;

    Кодген до оптимизации:



    После:



    В целом, инструкция mul(ss/sd/ps/pd) одинакова по latency и throughput как и add(ss/sd/ps/pd), но необходимость подгрузить константу «2» может слегка замедлить работу. Вот, в примере кодгена выше vaddss всё сделала в рамках одного регистра.

    PR #32368: Оптимизация Array.Length / c (или % с)


    Фаза: Morph
    Так уж вышло, что поле Length у Array — знаковый тип, а деление и остаток на константу намного эффективнее делать от беззнакового типа (и не только на степень двойки), просто сравните этот кодген:



    Мой PR просто напоминает JIT-у что Array.Length хоть и знаковый, но по сути, длина массива НИКОГДА (если вы не анархист) не может быть меньше нуля, а значит можно смотреть на нее как на беззнаковое число и применять некоторые оптимизации как для uint.

    PR #32716: Оптимизация простых сравнений в branchless код


    Фаза: Flow analysis
    Это уже другой класс оптимизаций, который оперирует базовыми блоками вместо выражений в пределах одного. Тут JIT немного консервативен и имеет пространство для улучшений, например вставки cmove где возможно. Я начал с простой оптимизации для вот такого случая:

    x = condition ? A : B;

    если А и Б — константы и разница между ними равна единице, например condition ? 1 : 2 то мы, зная что операция сравнение сама по себе возращает 0 или 1, можем заменить jump на add. В терминах RyuJIT это выглядит примерно так:



    Рекомендую посмотреть описание самого PR, надеюсь там всё понятно описано.

    Не все оптимизации одинаково полезны


    Оптимизации требуют довольно высокую плату:
    * Увеличение = усложнение существующего кода для поддержки и чтения
    * Потенциальные баги: тестировать компиляторные оптимизации безумно сложно и легко что-то упустить и получить какой-нибудь сегфолт у пользователей.
    * Замедление компиляции
    * Увеличение размера бинаря JIT

    Как вы уже поняли, далеко не все идеи и прототипы оптимизаций принимают и необходимо доказывать, что они имеют право на жизнь. Один из принятых способов доказать это в .NET является запуск утилиты jit-utils, которая совершит АОТ компиляцию некоторого набора библиотек (весь BCL и corelib) и сравнит ассемблерный код для всех методов до и после оптимизаций, вот как этот отчет выглядит для оптимизации "str".Length. Помимо отчета, есть еще определенный круг людей (таких как jkotas), которые одним взглядом могут оценить полезность и зарубить всё на корню с высоты своего опыта и понимания какие именно места в .NET могут быть бутылочным горлышком, а какие — нет. И еще: не судите оптимизации аргументом «так никто не пишет», «лучше бы просто показывали warning в Roslyn» — вы никогда не знаете, как будет выглядеть ваш код после того, как JIT заинлайнит всё, что возможно и зафолдит константы.

    Only registered users can participate in poll. Log in, please.

    Про что ещё хотелось бы прочитать?

    • 70.4%Обзор всех новых JIT-оптимизаций для .NET 5.0107
    • 57.2%Как именно работает Inlining и как написать свою эвристику87
    • 59.2%Подробный гайд как написать свою первую простую JIT-оптимизацию90
    • 32.9%SSA-форма и Value-Numbering оптимизации50

    Similar posts

    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 81

      +4

      Что-то грусто стало: многие пишут код, не вдаваясь в мысли о производительности с аргументом "JIT всё заоптимайзит", а на деле некоторые простейшие оптимизации, давно существующие во всяких C++ добавляют только в 2020 году.

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

            Кажется, в C# нет того, что Вы имеете ввиду под фиксированным массивом, по крайней мере доступными и распространенными средствами. Так что да, проверка есть всегда.

              0
              Массив фиксированной длины. Странно, что проверка есть на такие массивы
                +3

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

                  0
                  В некоторых простых кейсах JIT всё же не вставляет проверки.
                  — если для прохода по массиву используется foreach(для остальных коллекций foreach медленнее for)
                  — если массив объявлен внутри метода или как параметр метода, никуда не передаётся, в форе используется сравнение arr.Length, а не с другим значением и вроде итератор должен быть типа uint(с int у них какая-то проблема)

                  p.s. если обращение идёт к массиву в поле или свойстве любого класса, то JIT не может гарантировать, что другой поток(race condition) не поместит в это поле\свойство другой массив иного размера и всегда вставляет проверки, но это можно обойти просто присвоив массив переменной внутри метода
                  p.p.s. Где-то могу ошибаться, т.к. читал\смотрел на эту тему уже достаточно давно
                    0
                    если для прохода по массиву используется foreach

                    Разница с for только в том, что форич над полем класса сперва сохранит его в локальную переменную, а так разницы особо нет. Т.е. фор над массивом-полем да, не сработает (что и логично).

                      0
                      То есть, существуют люди которые осознанно могут подменить массив пока другой поток перебирает в нем элементы?
                        +1

                        Не знаю насчет "осознанно", но я такие ошибки регулярно вижу.

                          0

                          Слава богу что у нас 99% классов без сеттеров на пропертях, и где можно используют массивы вместо списков. Для гарантии можно было бы бахнуть ImmutableArray, но вроде у нас никто не злоупотребляет мутацией массивов по индексам.

                  0
                  Не совсем Вас понял. Задать размер массива, разумется, можно, но никто не запрещает выходить за его границы. Из статьи следует, что в JIT имеется достаточное количество оптимизаций (если уж имплементят хитрые кейсы), которые убирают ненужную проверку. Полагаю, что здесь важнее дать понять компилятору, что индекс не будет больше чем нужно. Видимо, если явно создавать массив определённого размера и явно обращаться к нему, то проверок не будет (имею в виду, не возвращать индекс из метода и тд.).
                    0
                    Опять же интересуюсь, как выйти за границы например int[1]
                      0

                      В том и дело, что в C# все массивы это new type[size]. Они всегда в куче.

                        –4
                        А как же динамические массивы List?
                          +3

                          А они не массивы.

                            –2
                            По сути list — это массив. С этим будешь спорить?
                              +2

                              Я не знаю, что такое "по сути". В моей системе определений массив — это Array, а list — это List. В C# List — это не Array.

                                –2
                                Разработчики языка разделили массивы на «урезанный» неизменяемый массив, занимающий меньше памяти, и «продвинутый» изменяемый массив, занимающий больше памяти. Но в твоей системе определения это могут быть конечно и разные галактики, я тут спорить не буду )

                                  +1
                                  Разработчики языка

                                  Не языка, а фреймворка.


                                  Но в твоей системе определения это могут быть конечно и разные галактики,

                                  Ну да, так и есть, это разные вещи.

                        +7

                        Очень просто:


                        var a = new int[1];
                        a[1] = 2;
                          –8
                          А вы точно программист?

                          using System;
                          					
                          public class Program
                          {
                          	public static void Main()
                          	{		
                          		var a = new int[1];
                          		a[1] = 2;
                          	}
                          }


                          Run-time exception (line 9): Index was outside the bounds of the array.

                          Stack Trace:

                          [System.IndexOutOfRangeException: Index was outside the bounds of the array.]
                          at Program.Main() :line 9
                            +2

                            А вы точно английский знаете? "Index was outside the bounds of the array." — это что, не выход за границы массива?

                              –2
                              Ну так причем здесь jit компиляция
                                +4

                                код для выброса исключения, которое вы видите, вставил jit за вас.

                                  –5
                                  То есть получается, что данная ошибка возникла уже в момент исполнения. А компилятор сказал все ок. Я не вижу, что ты мне за дичь пишешь. Сейчас исполним и проверим
                                    +3
                                    Я не вижу, что ты мне за дичь пишешь.

                                    Возможно, если читать внимательнее, будет немного легче. Вы спросили, "для фиксированного массива компилятор тоже ставит проверку на выход за границы массива?". Да, ставит. То поведение, которое вы видите — это поставленная компилятором проверка.

                                      –1
                                      Я не вижу, что ты мне за дичь пишешь.

                                      Это было образно и от лица компилятора. Так как в коде была явная ошибка. Но почему-то компилятор решил скомпилировать приложение и его исполнить, чтобы получить Run-time exception

                                      p.s. Некоторым программистам стоит заиметь чувство юмора, а не брать все на свой счет и обижаться )
                                        +1
                                        Но почему-то компилятор решил скомпилировать приложение и его исполнить

                                        Можно было бы показывать ворнинг но я большого смысла не вижу

                                          0

                                          А я лично вижу очень большой смысл в проверке границ в компайл тайм. Желательно, чтобы еще это число могло быть не константой, но это уже слишком продвинутые фичи для такого ЯП как C#.

                                            0

                                            Roslyn очень гибкий и расширяемый, вы можете легко сами написать такой анализатор, это вопрос 10 строк кода ;-)

                                              0

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

                                          +2
                                          Но почему-то компилятор решил скомпилировать приложение и его исполнить, чтобы получить Run-time exception

                                          Потому что разработчики решили не тратить время на обработку таких случаев. Это вполне разумное решение.


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

                                          Некоторым комментатором стоить заиметь умение более ясно выражаться.

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

                        К примеру вот тут JIT вставит проверку в каждой строке:
                        array[0] = 1;
                        array[1] = 2;
                        array[2] = 3;


                        А вот тут только на первой.
                        array[2] = 3;
                        array[1] = 2;
                        array[0] = 1;


                        В цикле for, когда условия выхода из цикла — сравнение индекса с размером массива JIT компилятор не делает проверки в теле цикла.

                        Если проверить размер массива руками, JIT компилятор тоже может убрать свою.

                        UPD: Пара примеров из кода .NET Core
                        System.DateTimeFormat
                        System.Buffers.Text.Utf8Formatter
                          0
                          К примеру вот тут JIT вставит проверку в каждой строке:

                          И вроде бы совершенно логично, разве нет?

                            0
                            Да, вполне.
                      +3

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

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

                        Ну тут имхо очень сильно зависит от целевой архитектуры: если оптимизировать для in-order ISA, то там может сыграть большую роль, особенно в горячих циклах.
                        Но все эти peephole же сделаны ещё 40 лет назад (и тогда они ЕМНИП были среди 6 самых важных по эффекту оптимизаций) и алгоритмически легки, так что отсутствие их в JIT сейчас меня сильно удивляет. Это же must have для любого оптимизирующего компилятора, что AOT, что JIT.

                        дебоксинга

                        Что это?

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

                        Любопытно, а у Вас случайно нет данных по приросту производительности от этих оптимизаций? Чтобы сравнить их.

                        Подход «Компилятор всё заоптимайзит» плох и в С++ для перфоманс-критикал кода.

                        Не согласен в части относительно низкоуровневых оптимизаций: имхо в тех же локальных оптимизациях (в т.ч. и peephole) или большей части цикловых всё же лучше отдать оптимизации на откуп компилятору, так как иначе код становится зависимым от архитектуры. Например, на условном in-order нужно делать программную конвейеризацию цикла (а без знания о том, сколько на целевом процессоре исполнительных устройств, это сделать трудно), а на условном out-of-order это de facto будет сделано железом.
                          +1
                          гораздо важнее оптимизации дебоксинга, девиртуализации и хорошего инлайнинга.

                          … и удачного расположения данных же ещё!


                          PS: у вас всегда прекрасные статьи, да и disasmo помогает в простых случаях, за него отдельное спасибо.

                            0

                            Спасибо :-)

                            0
                            Про хороший инлайн, не могу не заметить, что тут надо уметь как «правильно инлайнить», так и «правильно не инлайнить» из-за потенциально экспоненциального разрастания кода на фазах оптимизации его дублирующих (для ООО-архитектур это не так критично, поскольку многие фазы дублирования кода не нужны совсем или дают незначительный эффект).
                          +1

                          Тут вообще странная история. Про оптимизацию IsValueType например я слышал еще на дотнексте году так в 2016. Есть позозрение, что она есть в x64 JIT полного фреймворка, но при релизе рюджита её протеряли.


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

                          0
                          .NET он называется RyuJIT в честь какого-то дракона или аниме — я не разобрался

                          RyuJIT, вероятно, это Ryu (Street Fighter)
                          0
                          Ждем оптимизацию GC, все равно фризы от него, причем в самых непредсказуемых моментах — черная метка на шарпе.
                          Вот бы, на выбор несколько, и среди них подход как в Rust…
                            +3

                            Maoni уже затизерела low latency gc: https://github.com/dotnet/runtime/issues/33916#issuecomment-602141042
                            Но в целом если у вас сильные фризы, возможно вам стоит применить зеро-гц практики типа спанов и т.п.

                              0
                              И еще вопросик Егор, волнующий многих, как насчет Urhosharp, будет последний рывок? Или в урне проект?(
                                0

                                Сорян, но заброшено, как автор оригинального проекта забросил так и я сразу же :-(

                            –4
                             public static int Case2<T>(T o) => o is int n ? n : 42;
                            
                            public static int Case3<T>(T o)
                            {
                                return o switch
                                {
                                    int n => n,
                                    string str => str.Length,
                                    _ => 0
                                };
                            } 

                            Нет ли у вас ощущения, что язык перегружен сахаром и пора уже немножко притормозить?
                              +4

                              Сколько людей — столько и мнений, но в целом паттерн-матчинг в C# просили очень давно и настойчиво.

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

                                  А с ним-то что не так?

                                    –2
                                    Зачем этот огород когда есть { }?
                                    public static int Case2<T>(T o) { return o is int n ? n : 42; } 
                                      +3

                                      Ну вот и сравните:


                                      public static int Case2<T>(T o) { return o is int n ? n : 42; }
                                      public static int Case2<T>(T o) => o is int n ? n : 42;

                                      Для меня второе намного проще читать, меньше синтаксического мусора.

                                        0
                                        логично, чтобы как раз вашего огорода не было. => — дело привычки
                                          –5
                                          Если c-образный {} синтаксис это уже огород, гоу в пайтон)
                                          Когда лямбды используют для описания анонимных делегатов это здорово, действительно выглядит симпатично, удобно, главное позволяет реализовать замыкание.
                                          Зачем писать вот так, чем фишка?

                                                  public int AnyFunction(int x, int y) => x * y;
                                          
                                                  public int AnyFunction(int x, int y){
                                                      return x * y;
                                                  }
                                            0
                                            это здорово, действительно выглядит симпатично, удобно, главное позволяет реализовать замыкание.

                                            Вот как раз для замыканий этот синтаксис не обязателен. Не в смысле в C# (не помню просто), а теоретически.


                                            Зачем писать вот так, чем фишка?

                                            Выше уже написал, могу еще раз повторить: компактнее, легче читается.

                                              –9
                                              Вами — воспринимается компактней и легче читается. Отлично, ваше мнение выше услышано, хотелось бы услышать не только вашу точку зрения.
                                              –2

                                              В C# не принято писать открывающую скобку на той же строке. А еще между большими функциями принято писать по пробелу (а для маленьких однострочных — не обязательно).


                                              В итоге у вас в 4 раза больше места занимает код. вместо 50 строк в файле будет 200 — как-то не очень.

                                    0
                                    Думаю, это от части верно для тех кто только начинает изучать язык, так как достаточно много вариантов как можно написать одно и тоже, посему нужно выучить «много синтаксиса». А так, как по мне, то особо не заметно. Новые фишки не так часто и появляются. А те что появляются — достаточно логичны. Ваш же пример на C# 2.0 выглядел бы несколько длиннее))

                                    Последние сахарные плюхи, которые реально зашли это Индексы. Мне казалось, это нужно было сделать еще лет 15 назад:)
                                      –3
                                      Внедрять сахар нужно крайне осторожно, если не взлетит то выпилить его потом сохранив совместимость будет сложно, где то был пример с автосвойствами. Усложняет читаемость кода, одну и туже операцию каждый будет писать по своемому.

                                      Car car = new Car { Name = "Corvette", Color = Color.Yellow };

                                      Car car = new Car();
                                      car.Name = "Corvette";
                                      car.Color = Color.Yellow;

                                      или

                                      Car car = new Car();
                                      car.Name = "Corvette";
                                      car.Color = Color.Yellow;
                                      car.Manufacturer = new CarManufacturer();
                                      car.Manufacturer.Name = "Chevrolet";
                                      car.Manufacturer.Country = "USA";

                                      визуально воспринимается как определение метода
                                      Car car = new Car { 
                                                      Name = "Corvette", 
                                                      Color = Color.Yellow, 
                                                      Manufacturer = new CarManufacturer { 
                                                          Name = "Chevrolet", 
                                                          Country = "USA" 
                                                      } 
                                                  };


                                      объем кода сильно не вырос, но насколько по разному они выглядят,
                                        +2
                                        одну и туже операцию каждый будет писать по своемому.

                                        Да, будет, вы этого не избежите, если не поставите принудительное форматирование.


                                        визуально воспринимается как определение метода

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

                                    0
                                    О, а напишите как-нибудь, почему JIT до сих пор не умеет в автовекторизацию.
                                      +1
                                      почему JIT до сих пор не умеет в автовекторизацию

                                      Я не шарпист, но первая причина, которая приходит в голову, — вычислительная сложность. Как я понимаю, автовекторизация вычислительно сложна, а JIT — такой подвид компиляторов, в котором большую роль играет compile time.
                                      Впрочем, я могу быть не прав.
                                        0

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

                                          0

                                          Спасибо, я в курсе. В дотнете есть прямое апи ко всем SIMD интринсикам (разработанное с участием intel) + высокоуровенове апи.
                                          К тому же у нас есть LLVM backed, который умеет в автовекторизацию. Опять же толку от нее я особо не вижу, она легко отваливается в слегка сложных циклах. А так все апишки, которые вам могут пригодится уже векторизованы. Хотите сравнить два массива? Перемножить матрицы? найти символ в строке? везде симд будет.

                                        +1

                                        Ценность векторизации тоже очень сильно переоценена, все места в BCL которые могу и обычно участвую в hot path уже и так завекторизированы (всякие поиски, строки, utf8-unicode конверсии). Автовекоризации штука очень хрупкая и работает на совсем простых кейсах, к тому же что бы она работала это надо включать разворот циклов, увеличивать размер бинаря (в хкоде, например, анролл отключе по умолчанию емнип). У меня был где-то прототип совсем просто автовекторизации для JIT.

                                        0
                                        PR #31978: Math.Pow(x, 2) ⇨ x x
                                        PR #33024: x
                                        2 ⇨ x + x

                                        Т.е. в итоге Math.Pow(x, 2) ⇨ x + x?


                                        Можно разворачивать и любые другие степени, но для этого необходимо подождать реализации режима “быстрая математика” в .NET, при котором можно пренебречь спецификацией IEEE-754 в угоду производительности.

                                        Это было бы очень хорошо — видел много кода, где используется Math.Pow, который можно заменить на обычное умножение (сложение). Следование спецификации в подавляющем большинстве случаев не нужно. fast math будет как отдельная опция компиляции? Кстати, а в .NET есть коцепция чистых функций, чтобы можно было их сворачивать?


                                        Про что ещё хотелось бы прочитать?

                                        Отметил все :)

                                          0
                                          Т.е. в итоге Math.Pow(x, 2) ⇨ x + x?

                                          Эм, почему? x^2 != x+x :-)


                                          fast math будет как отдельная опция компиляции?

                                          Неизвестно, я пропозал оформлял но пока движения нет

                                            0
                                            Эм, почему? x^2 != x+x :-)

                                            Прощу прощения — это я считать разучился :(

                                            +2
                                            Кстати, а в .NET есть коцепция чистых функций

                                            Есть атрибут.

                                              0

                                              Атрибут есть, только влияет ли он хоть на что-то? Вроде решарпер изредка ругался что значение не используется, но и всё.

                                                0

                                                Сложно сказать. А атрибут AggressiveInlining влияет на что-то, если JIT и так может инлайнить мелкие функции, с другой стороны не будет инлайнить большую даже с этим атрибутом?

                                                  0

                                                  AggressiveInlining — влияет. А вот Pure — нет.

                                            0
                                            А почему такие оптимизации не делаются во время компиляции в IL? Почему «substr».Length нельзя заменить на константу сразу?
                                              0

                                              Чтобы оптимизация работала не только с C#, но и с другими .NET языками, например Visual Basic и F#.

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

                                                Хм, в статье же вроде прямым текстом написано почему :-) И даже пример есть, показывающий что на этапе IL эта оптимизация неэффективна.

                                            Only users with full accounts can post comments. Log in, please.