Comments 81

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

А вы точно программист?

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

В 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
К примеру вот тут JIT вставит проверку в каждой строке:

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

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

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

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

дебоксинга

Что это?

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

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

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

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

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


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

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

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


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

Ждем оптимизацию GC, все равно фризы от него, причем в самых непредсказуемых моментах — черная метка на шарпе.
Вот бы, на выбор несколько, и среди них подход как в Rust…
И еще вопросик Егор, волнующий многих, как насчет Urhosharp, будет последний рывок? Или в урне проект?(

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

 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
    };
} 

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

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

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

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

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

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


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;

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

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

        public int AnyFunction(int x, int y) => x * y;

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

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


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

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

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

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


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

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

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

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" 
                } 
            };


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

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


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

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

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

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

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

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

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

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 есть коцепция чистых функций, чтобы можно было их сворачивать?


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

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

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

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


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

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

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

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

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

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

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

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

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

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

Only those users with full accounts are able to leave comments. Log in, please.