Комментарии 51
Вопрос к пункту 1: а .Where() разве не возвращает IQueryable который уже автоматом имплементирует IEnumerable? Или я что-то путаю?
Ну это спорный пункт. У нас например бизнес-логика специально получает IQueryable чтобы добавить к нему свои LINQ выражения. И таким образом запрос к базе данных формируется динамически и выполняется ровно в тот момент когда БЛ действительно нужны данные. И следовательно не надо тащить из базы данных ненужные Item's. И это как бы тоже приличная оптимизация получается.
Ну и даже если такое не нужно, то на мой взгляд логичнее всё равно вещи, которые касаются БД, закапсулировать в ваш storage. Как раз таки чтобы каждый раз не думать есть там ещё открытое соединение или нет. И не думать при написании БЛ надо ставить.ТoArray() или.ТoList() или вообще ничего не ставить....
П.С. А если взять проекты где используется какой-нибудь NHibernate, то там за "совет" везде ставить.ТoArray() или.ТoList() с вами могут и очень нехорошие вещи сделать :) Это я к тому что на мой взгляд ваш пункт 1 является хорошим советом далеко не во всех ситуациях.
- ToArray vs ToList
Не согласен. ToArray
для IEnumerable
— это сначала ToList
, а потом ToArray
, тримминг лишних элементов (TrimExcess), т.е. лишняя аллокация. Так что если вам не нужен массив или это не какие-то постоянные коллекции, особенно большие, то ToList
выигрывает. Узнал это от Игоря Лабутина в докладе Коллекционируем данные в .NET.
- Параметр «путь к файлу» не всегда лучший выбор для вашего метода
- Избегайте использования потоков в качестве параметров и возвращаемого результата ваших методов
Это все сводится к общему совету "не плодите промежуточные сущности без необходимости", к которому можно приписать и другие ситуации.
Минимальное изменение в коде и все значения вашего enum’а занимают вдвое меньше памяти навсегда.
Навсегда, но не всегда. Если enum используется как член класса или структуры, то разницы никакой не будет из-за выравнивания как минимум по 4 байта. А это используется почаще, чем массивы из енамов.
Этот атрибут подскажет системе, что этот метод можно встраивать. Это вовсе не значит что метод, помеченный этим атрибутом, будет обязательно встроен.
Почти не использую, и у меня вопрос. А разве JIT не в состоянии сам заинлайнить маленькие методы, если даже этот атрибут не используется? Если может и JIT умнее, то зачем вообще помечать?
ILSpy
А еще есть бесплатные dotPeek, dnSpy и кроссплатформенный AvaloniaILSpy. dnSpyне боится обфусцированных сборок.
Если у меня возникает вопрос о том, какой механизм .NET более эффективный, в первую очередь, я открываю ILSpy (а не Google или StackOverflow), и уже там смотрю, как он реализован.
Если нужно посмотреть код именно самого .NET, то можно использовать онлайн https://referencesource.microsoft.com/. Если надо узнать как оптимизируется код на уровне ассемблера, то https://sharplab.io/ незаменим.
А вот по рекомендациям Игоря Лабутина не всё так очевидно. Возможно, в его докладе фигурировала более старая версия .NET. Или наоборот, его доклад был по .NET Core 3 Preview. Или иная причина расхождения результатов.
Если запустить простой бенчарк, то по времени выполнения мы получим эквивалентный результат, нельзя выявить победителя. А вот по выделению памяти ToArray стабильно расходует меньше.
Выложите, пожалуйста, код бенчмарка, сравнивающего ToList и ToArray.
public class ListvsArrayBenchmark
{
private const int VALUE = 1;
private IEnumerable<int> _source;
[Params(1000, 10000, 100000, 1000000)] public int N;
[GlobalSetup]
public void Setup()
{
_source = Enumerable.Repeat(0, N).Select(x => VALUE);
}
[Benchmark]
public List<int> ToList()
{
return _source.ToList();
}
[Benchmark]
public int[] ToArray()
{
return _source.ToArray();
}
}
Не забудьте поделиться своими результатами запуска.
В .NET Core ToArray и ToList были оптимизированы:
- Многие linq операторы начали возвращать не голый IEnumerable, а IIListProvider, в котором есть информация о количестве элементов в последовательности. Это позволяет сразу выделять под массив/список нужное количество памяти. В Вашем бенчмарке это как раз и проявилось — Repeat точно знает, сколько элементов будет в последовательности, а Select эту информацию передаёт дальше.
- ToArray для материализации последовательностей неизвестной длины стал использовать хитрые оптимизации. В определённый момент он переходит от использования одного промежуточного буфера с его ресайзом к списку буферов, что уменьшает memory traffic.
А в .NET Framework всё работает так как описал KvanTTT, поэтому ToList там аллоцирует меньше.
Про выравнивание enum'ов не знал, это не противоречит статье, однако с этой точки зрения я даже не оценивал.
На самом деле это касается всех типов. Если вы создадите структуру S с одним свойством типа byte, то sizeof(T) вернет 1. Однако если добавить к ней более длинный тип, например int, то уже sizeof(T) == 8, а не 5 как могло бы показаться. C long будет вообще 16. Т.е. если свойства типа байтового enum будет хотя бы с одним свойством int, то оптимизации по памяти не будет. Выравнивание сделано для оптимизации, так как доступ к выровненным элементам гораздо быстрее. Однако это касается архитектуры x64. На x86, возможно, будут другие цифры.
Если запустить простой бенчарк, то по времени выполнения мы получим эквивалентный результат, нельзя выявить победителя. А вот по выделению памяти ToArray стабильно расходует меньше.
По памяти тоже нельзя что-то сказать однозначно. Ок, давайте сравним код обеих имплементаций на вышеупомянутом https://referencesource.microsoft.com
В методе ToList(this IEnumerable collection) вызывается конструктор списка List(IEnumerable collection), внутри которого происходит перебор коллекции и обычное добавление в список с помощью метода Add
, пустая часть не отсекается.
В методе ToArray(this IEnumerable source) вызывается конструктор интернального класса Buffer(IEnumerable source), после чего на результирующем коллекции вызывается метод ToArray(). Buffer
работает аналогично List
, а метод ToArray
, в свою очередь, триммит результирующий массив. Из чего можно я делаю вывод, здесь все же происходит лишняя аллокация.
Кстати, еще более оптимально использовать запись, в которой лишняя коллекция вообще не создается без необходимости:
enumerable as List<Item> ?? enumerable.ToList();
P.S. у вас ошибка — там имелось в виду ToArray, а не ForArray?
Почти не использую, и у меня вопрос. А разве JIT не в состоянии сам заинлайнить маленькие методы, если даже этот атрибут не используется? Если может и JIT умнее, то зачем вообще помечать?
Возможно, если будет несколько кандидатов на инлайн, то JIT сначала будет рассматривать методы, помеченные данным атрибутом.
Но данный товарищ считает себя умнее компилятора и предлагает лепить везде принудительное встраивание.
Я даже не знаю, что на это ответить.
:-)
В разделе про Inlining, в принципе нет никаких советов и рекомендаций. Я как автор, ещё не до конца сформировал своё личное отношение к принудительному встраиванию. Как раз поэтому в статье описан лишь сам механизм, сама возможность. Всё остальное вы додумали. И на основе своих фантазий обвинили человека, вам должно быть стыдно.
Почти не использую, и у меня вопрос. А разве JIT не в состоянии сам заинлайнить маленькие методы, если даже этот атрибут не используется? Если может и JIT умнее, то зачем вообще помечать?
Не просто в состоянии, а даже активно это делает, когда может.
Вот тут есть небольшой текст с рационализацией процесса инлайнинга. Я тоже не до конца понимаю смысла помечать методы агрессивным инлайнингом — это то, что я бы назвал premature optimization. Пока нету метрик перформанса с явным пониманием, что метод мог бы быть быстрее, если заинлайнен — зачем вообще писать лишний код?
Навсегда, но не всегда. Если enum используется как член класса или структуры, то разницы никакой не будет из-за выравнивания как минимум по 4 байта. А это используется почаще, чем массивы из енамов.
Так, а если в структуре больше одного enum поля, тогда выигрыш, хоть и небольшой, но есть
ToArray vs ToList
Дополню ссылкой на SO
по умолчанию enum наследуется от int. Однако его можно наследовать от byte, который вмещает 256 значений (или 8 «flaggable» значений). Что почти всегда покрывает функциональность «среднего» enum’а.
Я как-то читал, что подобные ассоциативно похожие целочисленные типы или расположение структур в памяти компилятор всё равно приводит к Int32 размерам — потому что системе удобнее прыгать со смещением по 32 бита (даж в 64-битных операционках), чем прыгать по byte- или другим менее нестандартным размерам. Т.е. в случае приведённых к Int32 типам — операций требуется в среднем меньше.
Библиотека простая в использовании, но если вдруг её авторы читают сей пост, прошу, дайте более удобную возможность влиять на структуру таблицы результатов.
А чего не хватает?
2) Изменить цвета строк, Цвет групп запуска должен чередоваться, например белого и серого, чтоб не сливались. А метод, с наименьшими значениями по 3-м столбцам (Mean, Median, Allocated) — выделять цветом.
Примерно пол года назад, пробовал реализовать вышеописанное стандартными, через конфигурации. Не помню, во что именно упёрся, но пришлось написать свой конвертер таблицы результатов. Код конвертора получился «грязным», но зато таблицу результатов анализировать стало намного удобнее и приятнее. Картинки с бечмарками из поста как раз и иллюстрируют отображение, которое хотелось бы получить через конфигурацию.
Если вы знаете, как реализовать вышеописанное, буду очень благодарен.
- Красивого способа нет и не будет. Я считаю, что это методологически неправильно смотреть только на значения Mean/Median без Error/StdDev. По такой табличке очень опасно делать выводы, т.к. невозможно прикинуть есть ли статистически значимая разница между бенчмарками. Можно легко обмануть себя и другие людей, которые будут смотреть на результаты подобных экспериментов.
Но если очень-очень хочется, то сделать это всё-таки можно. Нужно скопипастить дефолтный конфиг и поменять реализациюGetColumnProviders
(вот тут можно посмотреть что подставляется по дефолту). - Такой фичи нет, но она выглядит интересной, можно сделать. Буду рад тикету или пул-реквесту.
Внимательный и опытный читатель может заметить что, например ключевое слово sealed никак не влияет на производительность. Сейчас это действительно так, но в следующих версиях всё может измениться.
Еще в первом издании «CLR via C#. Программирование на платформе Microsoft .NET Framework 2.0 на языке C#. Мастер класс» на странице 159 у Рихтера написано:
Производительность. Как уже говорилось, невиртуальные методы вызываются быстрее виртуальных, поскольку для последних CLR во время выполнения проверяет тип объекта, чтобы выяснить, где находится метод. Однако, встретив вызов виртуального метода в изолированном типе, JIT-компилятор может сгенерировать более эффективный код, задействовав невиртуальный вызов. Это возможно потому, что у изолированного класса не может быть производных классов.
Там еще был пример, но я не могу показать его, потому что могу этим нарушить авторские права.
Тем не менее, как раз ваш пример и подтверждает основной посыл, заложенный в 9-ом разделе. Теоретически, любое уточняющее ключевое слово может улучшить производительность, если не сегодня так завтра.
Оптимизация есть, просто sealed нужно ставить не в базовом классе.
Если код сильно виртуальный, то подобная оптимизация экономит одно обращение к памяти.
А вместе с инлайнингом работает ещё эффективнее.
ПС. В sealed классе компилятор не выдает предупреждение на использование виртуальных методов в конструкторе.
Оптимизация есть, просто sealed нужно ставить не в базовом классе.
Класс, помеченный модификатором
sealed
не может быть базовым, потому что sealed
запрещает наследование.Но сейчас, sealed класс с виртуальным методом даже не скомпилируется (код ошибки CS0549).
Error CS0549 'SealedClass.Bar()' is a new virtual member in sealed class 'SealedClass'
Виртуальные методы никогда нельзя было определять в классе, помеченном модификатором
sealed
. Они должны определяться в базовом классе.Это совершенно базовые вещи, без которых, в принципе, нельзя устроиться на работу C#-программистом.
Там еще был пример, но я не могу показать его, потому что могу этим нарушить авторские права.
Мне объясняли, что в «целях цитирования» не нарушите, если явно скажете про Рихтера (что сделано): www.consultant.ru/document/cons_doc_LAW_64629/84bbd636598a59112a4fe972432343dd4f51da1d. Но я не юрист.
Пример из книги Джеффри Рихтера «CLR via C#. Программирование на платформе Microsoft .NET Framework 2.0 на языке C#. Мастер-класс. / Пер. с англ. — М.: Издательство «Русская Редакция»; СПб.: Питер, 2007», который показывает как модификатор sealed
влияет на производительность:
Например, в следующем коде JIT-компилятор может вызвать виртуальный метод
ToString
невиртуально.
using System; public sealed class Point { private Int32 m_x, m_y; public Point(Int32 x, Int32 y) { m_x = x; m_y = y; } public override String ToString() { return String.Format("({0}, {1})", m_x, m_y); } public static void Main() { Point p = new Point(3, 4); // Компилятор C# вставит здесь инструкцию callvirt, // но JIT-компилятор оптимизирует этот вызов и сгенерирует код // для невиртуального вызова ToString, // поскольку p имеет тип Point, являющийся изолированным. Console.WriteLine(p.ToString()); } }
Оптимизация есть, просто sealed нужно ставить не в базовом классе.
Класс, помеченный модификатором
sealed
не может быть базовым, потому что sealed
запрещает наследование.Ээээ…
Что вы подразумеваете под понятием "базовый тип"?
String — являается вроде как базовым классом в .NET, и между тем он вполне себе — sealed.
Базовый класс – это любой класс, от которого можно наследоваться.
Нет, я точно не троллю, а уточняю, для себя. Во избежание непонимания.
И вы тогда немного не точны: Базовый класс — это не любой класс, а не имеющий суперкласса, т.е. тот что находится в основании дерева.
…, а не имеющий суперкласса, …
Это вообще терминология из Java. Вы, наверное, это описание в Википедии подсмотрели?
Прочитав эту статью и комментарии к ней, я прихожу к неутешительному выводу, что Хабр становиться похож на книгу моего детства:
На данный момент, многие хорошие фронтендеры уже мигрировали на Medium. И не хочется, чтобы тоже самое произошло с .NET-разработчиками. Перед тем, как публиковать статьи проверяйте информацию в авторитетных источниках или давайте прочитать их своим старшим коллегам.
Второе правило оптимизации производительности — соотнеси плюсы (выгоду) с минусами (в виде усложнения сопровождения кода и т.п.).
По поводу ToList() vs ToArray(): например, если получать данные из БД в коллекцию, то особой разницы между ними нет. Ведь, условно, 99,9% времени тратится на получение данных, а не на складывание их в коллекцию в памяти :)
Ну а если кто-то дошел до того, что разница между ToArray() и ToList() действительно существенна, лучше вообще не использовать Linq и получить еще больший прирост производительности. Потому как вызовы Where(...), Select(...) и т.п.:
— создают структуру-итератор, которая используется через интерфейс, т.е. упаковывается;
— принимают делегаты, которые сами являются объектами, т.е. размещаются в куче, и их вызов происходит медленнее, чем вызов вашего кода непосредственно без делегата.
Вдобавок, foreach по IEnumerable IL-компилируется в вызовы GetEnumerator(), MoveNext() и т.п., без оптимизаций по конкретному типу коллекции. Для сравнения, foreach по массиву T[] компилируется в куда более быстрый код.
Из подходов, относящихся непосредственно к C# и .NET, которые помогали (по опыту, в порядке выгоды):
1. class vs struct
Если работаете с многими тысячами/миллионами объектов в памяти, и вам не надо ссылаться на одни и те же объекты в разных местах, старайтесь использовать структуры вместо классов. Получите уменьшение потребления памяти и ускорение работы.
Ведь экземпляры классов (объекты) требуют выделения памяти и находятся в управляемой куче. Экземпляры структур не требуют отдельного выделения памяти и находятся там, где объявлены (в стек-фрейме метода, внутри экземпляра класса и т.п.). Так, в массиве объектов хранятся только ссылки на эти объекты, а сами объекты находятся в куче и могут быть разбросаны по разным местам памяти. В массиве структур данные хранятся компактно — в итоге, меньшее использование памяти, более высокая локальность, меньше напрягов для сборщика мусора.
Ньюанс: если ваша структура реализует интерфейс, и вы объявите массив интерфейсов, положив туда структуры, то каждый элемент массива упакуется, и вся выгода от использования структуры сойдет на нет.
2. Методы, возвращающие итераторы (через yield return)
Они компилируются в довольно объемный и относительно медленный итератор. При вызове происходят обращения к Thread.CurrentThread.ManagedThreadId для обеспечения правильной работы итератора в разных потоках. У нас была ситуация, когда уход от yield return уменьшил время выполнения некоторых расчетов на 14%.
3. Task.Run(...)
Класс Task очень удобен для асинхронного программирования. Но если требуется очень часто выполнять метод в пуле потоков, то банальные вызовы ThreadPool.QueueUserWorkItem(...) и ThreadPool.UnsafeQueueUserWorkItem(...) решат вашу задачу с чуть меньшими издержками.
4. Вируальные вызовы (callvirt)
Вызов виртуального метода включает в себя:
— переход к таблице виртуальных методов в типе объекта
— поиск нужного метода в этой таблице
— собственно вызов найденного метода
Поэтому в местах, критичных к производительности, проверяйте наличие виртуальных вызовов, если все остальное уже соптимизировали :)
Ньюанс: если в классе вы неявно реализуете метод интерфейса, это метод неявно помечается как виртуальный.
P.S. А вообще, самые эффективные оптимизации — на уровне архитектуры приложения и используемых алгоритмов.
Оптимизация производительности .NET (C#) приложений