Как стать автором
Обновить

Комментарии 51

Вопрос к пункту 1: а .Where() разве не возвращает IQueryable который уже автоматом имплементирует IEnumerable? Или я что-то путаю?

Всё верно! Но так обычно не делают — небезопасно. Например, в данном случае connceton к БД может быть закрыт до того, как данные реально попадут в слой представления. И это не единственная причина. Нельзя представлению никак влиять на БД. В случае возврата IQueryable объекта, такая возможность есть.

Ну это спорный пункт. У нас например бизнес-логика специально получает IQueryable чтобы добавить к нему свои LINQ выражения. И таким образом запрос к базе данных формируется динамически и выполняется ровно в тот момент когда БЛ действительно нужны данные. И следовательно не надо тащить из базы данных ненужные Item's. И это как бы тоже приличная оптимизация получается.


Ну и даже если такое не нужно, то на мой взгляд логичнее всё равно вещи, которые касаются БД, закапсулировать в ваш storage. Как раз таки чтобы каждый раз не думать есть там ещё открытое соединение или нет. И не думать при написании БЛ надо ставить.ТoArray() или.ТoList() или вообще ничего не ставить....


П.С. А если взять проекты где используется какой-нибудь NHibernate, то там за "совет" везде ставить.ТoArray() или.ТoList() с вами могут и очень нехорошие вещи сделать :) Это я к тому что на мой взгляд ваш пункт 1 является хорошим советом далеко не во всех ситуациях.

НЛО прилетело и опубликовало эту надпись здесь
  1. ToArray vs ToList

Не согласен. ToArray для IEnumerable — это сначала ToList, а потом ToArray, тримминг лишних элементов (TrimExcess), т.е. лишняя аллокация. Так что если вам не нужен массив или это не какие-то постоянные коллекции, особенно большие, то ToList выигрывает. Узнал это от Игоря Лабутина в докладе Коллекционируем данные в .NET.


  1. Параметр «путь к файлу» не всегда лучший выбор для вашего метода
  2. Избегайте использования потоков в качестве параметров и возвращаемого результата ваших методов

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


Минимальное изменение в коде и все значения вашего enum’а занимают вдвое меньше памяти навсегда.

Навсегда, но не всегда. Если enum используется как член класса или структуры, то разницы никакой не будет из-за выравнивания как минимум по 4 байта. А это используется почаще, чем массивы из енамов.


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

Почти не использую, и у меня вопрос. А разве JIT не в состоянии сам заинлайнить маленькие методы, если даже этот атрибут не используется? Если может и JIT умнее, то зачем вообще помечать?


ILSpy

А еще есть бесплатные dotPeek, dnSpy и кроссплатформенный AvaloniaILSpy. dnSpyне боится обфусцированных сборок.


Если у меня возникает вопрос о том, какой механизм .NET более эффективный, в первую очередь, я открываю ILSpy (а не Google или StackOverflow), и уже там смотрю, как он реализован.

Если нужно посмотреть код именно самого .NET, то можно использовать онлайн https://referencesource.microsoft.com/. Если надо узнать как оптимизируется код на уровне ассемблера, то https://sharplab.io/ незаменим.

KvanTTT, спасибо за информативный комментарий! +1, именно такой фидбек я очень люблю! Про выравнивание enum'ов не знал, это не противоречит статье, однако с этой точки зрения я даже не оценивал.

А вот по рекомендациям Игоря Лабутина не всё так очевидно. Возможно, в его докладе фигурировала более старая версия .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 аналогичные Вашим, так что выкладывать не буду. А вот под .NET Framework ситуация другая:
image
В .NET Core ToArray и ToList были оптимизированы:
  • Многие linq операторы начали возвращать не голый IEnumerable, а IIListProvider, в котором есть информация о количестве элементов в последовательности. Это позволяет сразу выделять под массив/список нужное количество памяти. В Вашем бенчмарке это как раз и проявилось — Repeat точно знает, сколько элементов будет в последовательности, а Select эту информацию передаёт дальше.
  • ToArray для материализации последовательностей неизвестной длины стал использовать хитрые оптимизации. В определённый момент он переходит от использования одного промежуточного буфера с его ресайзом к списку буферов, что уменьшает memory traffic.

А в .NET Framework всё работает так как описал KvanTTT, поэтому ToList там аллоцирует меньше.
Это было познавательно с IIListProvider. Будем знать.
А тест надо переписать чтобы забрать зависимость от этой фичи. Умом понимаешь, что ToList будет быстрее чем ToArray, а тут как обухом.
Про выравнивание 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?

у вас ошибка — там имелось в виду ToArray, а не ForArray?
Да, вы правы. Поздно заметил, не успел исправить комментарий. В полном коде бенчмарка, приведённого выше, ошибка в названии метода уже исправлена.
Прошу прощения за ошибку в названии метода, не «ForArray», а «ToArray».
Почти не использую, и у меня вопрос. А разве JIT не в состоянии сам заинлайнить маленькие методы, если даже этот атрибут не используется? Если может и JIT умнее, то зачем вообще помечать?

Возможно, если будет несколько кандидатов на инлайн, то JIT сначала будет рассматривать методы, помеченные данным атрибутом.

JIT примерно знает, даст встраивание прирост производительности или нет. Кроме сокращения затрат на вызов метода, встраивание плохо отразится на кэшировании кода и увеличит его объем. Но данный товарищ считает себя умнее компилятора и предлагает лепить везде принудительное встраивание. Многие другие пункты, например, тот же пункт про ToArray, тоже похожи на вредные советы и повторять их в реальной жизни не стоит. КГ/АМ, короче.
Но данный товарищ считает себя умнее компилятора и предлагает лепить везде принудительное встраивание.

Я даже не знаю, что на это ответить.
:-)

В разделе про Inlining, в принципе нет никаких советов и рекомендаций. Я как автор, ещё не до конца сформировал своё личное отношение к принудительному встраиванию. Как раз поэтому в статье описан лишь сам механизм, сама возможность. Всё остальное вы додумали. И на основе своих фантазий обвинили человека, вам должно быть стыдно.
В каком месте автор считает себя умнее компилятора и предлагает везде лепить принудительное встраивание? Или вы статью не читали/читали не внимательно?
Почти не использую, и у меня вопрос. А разве JIT не в состоянии сам заинлайнить маленькие методы, если даже этот атрибут не используется? Если может и JIT умнее, то зачем вообще помечать?

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

Бывают случаи, когда это действительно необходимо.

Почему это такой случай? Методы статические, маленькие, с инлайнингом не должно возникнуть проблем по идее.

Навсегда, но не всегда. Если enum используется как член класса или структуры, то разницы никакой не будет из-за выравнивания как минимум по 4 байта. А это используется почаще, чем массивы из енамов.

Так, а если в структуре больше одного enum поля, тогда выигрыш, хоть и небольшой, но есть

НЛО прилетело и опубликовало эту надпись здесь
Ссылку на оригинальный пост Microsoft не нашёл. Лет 5 назад попадалась статья с рекомендациями, когда нужно использовать Dictionaty. Цифра условная (для Capacity по умолчанию, с каким-нибудь простым ключом, типа int). Основной смысл 6-го раздела в том, что на очень малом кол-ве элементов, Dictionary результата не даст, так же как и с малым соотношением добавлений к поиску в рамках критичного кода.
Суть в том, что локальность ссылок в словарях меньше, чем в массивах, проще держать в одной строке кэша L1 небольшой массив, чем собирать данные словаря по разным уголкам памяти и подгружать несколько строк в кэш. Увы, сам автор этого не понимает.
Дополню: размер строки кэша данных L1 в семействе x86 равен 64 байтам, соответственно, в нее помещается 8 64-разрядных ссылок на объекты. Если целевая платформа 32 разрядная, то 16 ссылок. Все они в случае массива могут быть загружены за одну операцию чтения.
по умолчанию enum наследуется от int. Однако его можно наследовать от byte, который вмещает 256 значений (или 8 «flaggable» значений). Что почти всегда покрывает функциональность «среднего» enum’а.

Я как-то читал, что подобные ассоциативно похожие целочисленные типы или расположение структур в памяти компилятор всё равно приводит к Int32 размерам — потому что системе удобнее прыгать со смещением по 32 бита (даж в 64-битных операционках), чем прыгать по byte- или другим менее нестандартным размерам. Т.е. в случае приведённых к Int32 типам — операций требуется в среднем меньше.

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

А чего не хватает?

1) Отображать только 4 стоблца: Method, N, Mean, Median, Allocated.
2) Изменить цвета строк, Цвет групп запуска должен чередоваться, например белого и серого, чтоб не сливались. А метод, с наименьшими значениями по 3-м столбцам (Mean, Median, Allocated) — выделять цветом.

Примерно пол года назад, пробовал реализовать вышеописанное стандартными, через конфигурации. Не помню, во что именно упёрся, но пришлось написать свой конвертер таблицы результатов. Код конвертора получился «грязным», но зато таблицу результатов анализировать стало намного удобнее и приятнее. Картинки с бечмарками из поста как раз и иллюстрируют отображение, которое хотелось бы получить через конфигурацию.

Если вы знаете, как реализовать вышеописанное, буду очень благодарен.
  1. Красивого способа нет и не будет. Я считаю, что это методологически неправильно смотреть только на значения Mean/Median без Error/StdDev. По такой табличке очень опасно делать выводы, т.к. невозможно прикинуть есть ли статистически значимая разница между бенчмарками. Можно легко обмануть себя и другие людей, которые будут смотреть на результаты подобных экспериментов.
    Но если очень-очень хочется, то сделать это всё-таки можно. Нужно скопипастить дефолтный конфиг и поменять реализацию GetColumnProviders (вот тут можно посмотреть что подставляется по дефолту).
  2. Такой фичи нет, но она выглядит интересной, можно сделать. Буду рад тикету или пул-реквесту.
Можно добавить ещё про культуры и сравнения. Всякие string.IndexOf, .ToString — которые любят использовать текущую культуру. А если мы точно знаем что у нас есть тупой ascii или нам нужен подобный формат, то можно на подобном не один коробок спичек сэкономить.
Внимательный и опытный читатель может заметить что, например ключевое слово sealed никак не влияет на производительность. Сейчас это действительно так, но в следующих версиях всё может измениться.

Еще в первом издании «CLR via C#. Программирование на платформе Microsoft .NET Framework 2.0 на языке C#. Мастер класс» на странице 159 у Рихтера написано:
Производительность. Как уже говорилось, невиртуальные методы вызываются быстрее виртуальных, поскольку для последних CLR во время выполнения проверяет тип объекта, чтобы выяснить, где находится метод. Однако, встретив вызов виртуального метода в изолированном типе, JIT-компилятор может сгенерировать более эффективный код, задействовав невиртуальный вызов. Это возможно потому, что у изолированного класса не может быть производных классов.

Там еще был пример, но я не могу показать его, потому что могу этим нарушить авторские права.
Для времени, когда был актуален .NET Framework 2.0, скорее всего, всё именно так и было. Но сейчас, sealed класс с виртуальным методом даже не скомпилируется (код ошибки CS0549). Поэтому сейчас такой оптимизации нет. Лично для меня, такое поведение было ожидаемым.

Тем не менее, как раз ваш пример и подтверждает основной посыл, заложенный в 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.

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

Базовый класс – это любой класс, от которого можно наследоваться.

Нет, я точно не троллю, а уточняю, для себя. Во избежание непонимания.


И вы тогда немного не точны: Базовый класс — это не любой класс, а не имеющий суперкласса, т.е. тот что находится в основании дерева.

В C# только один класс не имеет суперкласса, и это System.Object :) Все остальные наследуются от него по умолчанию.

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

Это вообще терминология из 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. А вообще, самые эффективные оптимизации — на уровне архитектуры приложения и используемых алгоритмов.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории