Комментарии 163
И поправьте отображение кода
И поправьте отображение кода
Что не так с отображением кода? Можно подробнее? habrahabr не умеет в нативную подсветку ассемблера и пришлось экспериментировать с другими тулзами.
В целом вставлять код скриншотом это очень плохая практика, т.к. такой код нельзя скопировать и такой код не проиндексируется поисковиками.
Ради интереса. Вот код: pastebin.com/2fhb1fUZ, вот asm (LegacyJit x86): pastebin.com/8uSmwitv
Метод вызывается каждый раз, хотя возвращает каждый раз одного и тоже значение.
Быстрее код не станет, но и медленнее тоже. Скорость написания кода тоже сомнительный аргумент. Перерыв «на кофе» займет больше времени чем сэкономите.
Зато если всегда писать, то это будет делаться машинально и будет хоть какая то однородность в стиле.
Тогда какой смысл не создавать переменную?
Чтобы не было лишней строчки в идиоматичном коде.
Быстрее код не станет, но и медленнее тоже. Скорость написания кода тоже сомнительный аргумент.
Обход массива — идиоматичный код, лучше, если он у всех выглядит одинаково.
Зато если всегда писать, то это будет делаться машинально и будет хоть какая то однородность в стиле.
Однородно должен быть написан как раз обход массива. А, если выделена отдельная переменая — значит можно сразу понять, что там какой-то не тривиальный код
А если размер массива меняется внутри цикла от шага к шагу?
Конечно, хочется сказать — не делайте так. Но на самом деле интересно, в каком случае это может понадобиться. Особенно учитывая, что менять размер массива внутри цикла и при это не ушатать производительность можно только удаляя элементы из конца. Ну или добавляя их туда.
Возможно, что-то изначально уже сделано неправильно, но я не программист и это хобби-проект.
Например, при кодовой генерации интерфейса, когда надо удалить те контролы, которые проходят по условию.
Если из листа надо только удалить элементы, то лучше просто вернуть новый лист, где удалённых элементов нет.
А если работа напрямую со списком контролов, который предоставляет родительский контрол?
Там, наверное, удаление контрола делается вызовом отдельного метода родительского контрола?
Я бы прошёл по списку, получил контролы, которые нужно удалить и сделал ещё один цикл, в котором бы их удалил. Я стараюсь избегать кода, в котором модифицируется размер коллекции, по которой происходит итерация.
var controlsToUpdate = Controls.Where(SomeCondition).ToArray()
Строго говоря вы доказали это лишь для одной структуры данных. Я бы побоялся делать такие выводы на одном примере. А если работа идёт с какой-то коллекцией где логика подсчета длины не совсем константая по сложности? Слишком тонкая тема. Думаю серебряной пули тут нет. Разве что только для встроенных примитивных типов
кстати подобное во многих языках, где есть рефлексия и кеш на короткие строки
Если написать:
void SomeMethod(int[] array) {
foreach(var i in array) {
}
}
то JIT это развернёт в обычный цикл со счётчиком, а если сделать:
void SomeMethod(IEnumerable<int> array) {
foreach(var i in array) {
}
}
то тут уже будут все «прелести» foreach — получение енумератора, MoveNext и т.д. И вот этот код уже будет тормозить даже если передать туда массив интов.
Выходит, что foreach научился оптимизировать циклы, используя дополнительную функциональность, к примеру, IList.
И получается, что с точки зрения производительности, выгоднее не генерализировать код, передавая минимально допустимый интерфейс вроде IEnumerable, а передавать ILIst, давая возможность компилятору оптимизировать код.
Примерно также сделали вот здесь: github.com/dotnet/corefx/blob/master/src/System.Linq/src/System/Linq/Count.cs
Но мне кажется, что если приходится в реальном проекте делать такую оптимизацию, то скорее всего что-то сильно раньше пошло не так.
Жаль в C# нету перегрузок методов по constraint-ам на generic-параметры…
Но нельзя сделать перегрузки вида
void Smth<T, X>(this T obj) where T : IList<X>;
void Smth<T, X>(this T obj) where T : IEnumerable<X>;
Generic-методы дают максимальный выигрыш в скорости только тогда, когда не происходит приведений типов. Получение интерфейсной ссылки и вызов методов через неё обходится дороже чем прямой вызов метода, даже через generic с constraint-ом.
Кроме того, код с кастом выполняется в runtime и способен обнаруживать совместимые конкретизации типов, когда объект передан обобщённо в виде интерфейсной ссылки или приведением к базовому типу.
PS: На хабре отвалилась подсветка C# из списка языков?
Я в последнее время всё, где можно — на IReadonlyCollection переделываю. List, Array и некоторые другие списки её умеют из коробки, соответственно переделываем только сигнатуру, а вызовы можно оставить как есть.
Исключение делается только в одном случае — если состав коллекции действительно IEnumerable, например это результат асинхронного выполнения запроса к базе или результат рекурсивной загрузки дерева файлов с диска, когда мы не можем получить весь результат одним куском и не знаем конечного размера массива элементов.
Во всех остальных случаях IReadonlyCollection — то, что доктор прописал.
Только одна претензия — слишком длинное название интерфейса, но здесь уж ничего не изменишь.
И главное — чистота кода. IReadonly сам собой подразумевает, что список передаётся в метод только для чтения, и не будет использоваться для неявного пополнения внутри метода.
Зачем IList? Передача изменяемых списков в качестве аргументов — попахивает.
Я в последнее время всё, где можно — на IReadonlyCollection переделываю.
IReadonlyCollection даёт произвольный доступ к элементам? Если нет, то вот и причина передавать IList.
Если реализация метода, принимающая IList закрыта от вызывающей стороны интерфейсом, то вызывающая сторона не может быть уверена, что в такой метод можно передать Array — вдруг реализация начнёт менять коллекцию?
Если вам нужен доступ к элементам по индексу — используйте IReadonlyList, но, как по мне, это тоже попахивает какими-то костылями.
Просто вопрос — зачем? Если, например, методу нужен какой-то конкретный элемент массива, это означает, что метод слишком много знает об организации коллекции. Вместо этого метод может принимать нужный элемент, вместо коллекции.
В общем принимать коллекции с доступом по индексу — это должна быть какая-то очень специфическая необходимость.
IList? Например Array реализует IList, но бросает NotSupportedException при попытке вызвать методы, изменяющие коллекцию.
Это у меня из-за недостатка знаний про .net.
Если вам нужен доступ к элементам по индексу — используйте IReadonlyList, но, как по мне, это тоже попахивает какими-то костылями.
Ага, спасибо.
В общем принимать коллекции с доступом по индексу — это должна быть какая-то очень специфическая необходимость.
Есть ещё такое соображение, что в коллекцию можно передать что угодно, в том числе LinkedList. Возможно вы хотите запретить его использование на уровне API.
Согласен, что очень специфичный и редко используемый контейнер.
Я же говорю об остальных 99 случаях из 100 — IReadOnlyCollection будет предпочтительнее — с ним интерфейсы становятся чище и понятнее.
Я же говорю об остальных 99 случаях из 100 — IReadOnlyCollection будет предпочтительнее — с ним интерфейсы становятся чище и понятнее.
Если LinkedList используется редко, то в 99 случаях из 100 можно рассчитывать, что его в метод не передадут. Следовательно в этих 99 случаях из 100 можно сделать явное ограничение в виде IReadOnlyList.
IReadOnlyCollection — явно указывает, что можно передать спокойно передавать LinkedList и так и надо. В 99 случаях из 100 так не надо, поэтому я считаю, что лучше это выразить явно.
IReadOnlyCollection — явно указывает, что можно передать спокойно передавать LinkedList и так и надо. В 99 случаях из 100 так не надо, поэтому я считаю, что лучше это выразить явно.
А что в этом плохого? Вызывающая сторона будет обращаться с LinkedList так же, как и с любой другой коллекцией. Зачем вызывающей стороне IReadOnlyList, который отличается от IReadonlyCollection исключительно геттером по индексу элемента?
Наоборот — ожидая IReadonlyList метод указывает вызывающей стороне, что он собирается вызывать Collection[index], иначе он ожидал бы базовый интерфейс IReadonlyCollection.
Если методу нужна исключительно энумерация входящих в коллекцию элементов — то LinkedList ничем не хуже любой другой коллекции, и ограничение не имеет смысла.
Зачем вызывающей стороне IReadOnlyList, который отличается от IReadonlyCollection исключительно геттером по индексу элемента?
Чтобы нам не передали LinkedList.
Наоборот — ожидая IReadonlyList метод указывает вызывающей стороне, что он собирается вызывать Collection[index], иначе он ожидал бы базовый интерфейс IReadonlyCollection.
Да, тут вы совершенно правы. Если бы был какой-то интерфейс, маркирующий наличие массива под капотом у коллекции, я бы предложил использовать его.
Если методу нужна исключительно энумерация входящих в коллекцию элементов — то LinkedList ничем не хуже любой другой коллекции
LinkedList жрёт память, убивает перформанс, не даёт элементам коллекции ложиться в кеш и так далее.
У него быстрая вставка и удаление по итератору.
Например, он лучше других будет подходить как контейнер для MRU List и ряда других задач.
Как и любой контейнер, двусвязный список предназначен для решения специализированных задач.
Вот когда появляются такие задачи и надо использовать LinkedList.
У него быстрая вставка и удаление по итератору.
И, соотвественно, LinkedList нужен когда есть необходимость итерировать по коллекции в обоих направлениях, при этом постоянно удаляя произвольные элементы и добавляя их. И при это порядок этих элементов в коллекции должен быть важен. Комбинация таких условий встречается редко, если вообще встречается.
Например, он лучше других будет подходить как контейнер для MRU List
И что делает LinkedList лучше других подходящим на роль MRU List? Чем он лучше ArrayList? Почему там нельзя использовать банальный массив фиксированного размера?
И что делает LinkedList лучше других подходящим на роль MRU List? Чем он лучше ArrayList? Почему там нельзя использовать банальный массив фиксированного размера?
Потому что в массиве фиксированного размера операция "взять произвольный элемент и переставить в начало" выполняется за Θ(N)
Потому что в массиве фиксированного размера операция "взять произвольный элемент и переставить в начало" выполняется за Θ(N)
Для того, чтобы переставить в начало произвольный элемент в двусвязном списке нужно:
- Вписать в prev первого элемента ссылку наш элемент
- Вписать в prev нашего элемента null
- Вписать в next нашего элемента ссылку на бывший первый элемент
- Вписать в next предущего элемента ссылку на следующий элемент
- Вписать в prev следующего элемента ссылку на предыдущий элемент
Итого 5 действий. То есть, пока у нас в листе 5 элементов и менее, то массив лучше двусвязного списка, тут даже обсуждать нечего.
Но сдвиг в массиве это не просто N операций, это N операций, операнды которых хорошо легли в кеш, а значит они сильно быстрее, чем операции с Linked List.
Кроме того, сдвиг в массиве оптимизируется на низком уровне, а значит — он ещё быстрее.
Поэтому количество элементов, при котором массив — лучший инструмент для решения нашей задачи существенно больше пяти.
MRU List это такая штука, которая ограничена в размерах каким-то небольшим числом и поэтому массив тут подойдёт лучше, чем LinkedList, если речь только о времени, затрачиваемом на то, чтобы переместить в начало произвольный элемент.
Э… а почему вы считаете что MRU List должен быть ограничен каким-то небольшим числом?
Потому что он нужен для того, чтобы вернуться к файлу или проекту или ещё какой-то сущности с которой вы либо недавно работали, либо работатете постоянно. Хранить там больше какого-то небольшого количества элементов не имеет смысла, если надо найти что-то что вы использовали давно — если другие инструменты.
Перемещение элемента целиком — зависит от размера элемента, но редко бывает элемент размером в 1 машинное слово. Альтернатива — сами элементы хранятся в одном контейнере, а индексы для доступа — в другом. Но такие структуры не кэш-френдли. Ну и напоследок: большие структуры, да ещё хранящие ссылки, например, на строки — тоже не слишком кэш-френдли.
А что страшного случится если вам передадут LinkedList? Почему бы не предположить что тот, кто создает LinkedList и передает его нам, знает что делает?
Представим себе ситуацию. Мы пишем API, в котором нужен метод вычисления какой-нибудь математической функции из входящей коллекции Int. Единственным условием для успешности вычисления этой функции является конечность коллекции.
Вы действительно предлагаете запретить использование всех базовых коллекций, не реализующих IReadonlyList, только для того, чтобы не убить перформанс?
Чей перформанс? Перформансом LinkedList должен озаботиться в первую очередь тот, кто нам его передаёт. Если пользователя API устраивает перформанс LinkedList, и ему нужна именно такая коллекция? Или даже она досталась ему по наследству из другого API. Что же ему теперь — перед каждым вызовом нашего метода API вызывать .ToArray() на своём списке?
Кроме того, запрещая использование IReadonlyCollection, помимо LinkedList вы теряете и другие базовые коллекции — например Stack или Queue. Да даже Dictionary.
Попахивает каким-то рассизмом по отношению к типам.
IList<int>
Тормознутость какого порядка? Микросекунды на миллион итераций или хуже?
Сам не особо парюсь такими оптимизация и использую везде IEnunerable<> и List<>, потому что любой запрос к соседнему микросервису отработает в сотни и тысячи раз дольше.
Но для интереса я накидал бенчмарк. Вот сорец: gist.github.com/tdkkdt/181e3f1e2ee6bb1ce1662bfae1241545
Вот результаты:
gist.github.com/tdkkdt/c3250b9c4d25f13a486cb435870bd7aa
foreach на IEnumerable в среднем в 10 раз медленнее чем foreach на массиве.
Я решил раз и навсегда для себя понять стоит так делать или можно сэкономить своё время и написать без временной переменной.
Можно было и не тратить время на проверку. То, что условие цикла for вычисляется один раз перед входом в цикл, написано в документации на C#. Собственно, точно так же этот цикл ведет себя и в других языках.
условие цикла for вычисляется один раз перед входом в цикл
Странно сформулировано. Условие выхода из цикла в общем случае точно вычисляется на каждой итерации.
Насмешили.
«Условие», а точнее булевое выражение в for вычисляется каждый раз (если оно есть).
public class LimitWithCounter
{
private static readonly Random Rnd = new Random();
public int CurrentLimit = 2;
public int Limit()
{
CurrentLimit = Rnd.Next(15) + 5 ;
Console.Write(CurrentLimit);
return CurrentLimit;
}
}
...
static void Main(string[] args)
{
var limit = new LimitWithCounter();
limit.Limit(); Console.WriteLine($"Limit {limit.CurrentLimit}");
limit.Limit(); Console.WriteLine($"Limit {limit.CurrentLimit}");
limit.Limit(); Console.WriteLine($"Limit {limit.CurrentLimit}");
Console.WriteLine();
for(int i = 0; i < limit.Limit(); i++)
{
Console.WriteLine($"\tStep: {i}");
}
Console.WriteLine();
Console.ReadLine();
return;
}
Результат:
15 Step: 1
15 Step: 2
7 Step: 3
12 Step: 4
8 Step: 5
16 Step: 6
11 Step: 7
16 Step: 8
19 Step: 9
11 Step: 10
10
Вывод: Если б мы фикисровали лимит на входе в цикл, потребовалось бы 16 шагов на завершение цикла. Значит, граница выхода из цикла пересчитывается на каждой итерации.
Очевидно, что компилятор достаточно умный, чтобы отличить свойство Length стандартного Array от метода самописного класса. В первом случае Length измениться не может, и компилятор генерирует код, кеширующий длину массива перед циклом. Во втором случае генерируется код, вызывающий метод на каждом цикле.
То, что условие цикла for вычисляется один раз перед входом в цикл, написано в документации на C#.Я комментировал не сам пост, а комментарий к нему.
Очевидно, что компилятор достаточно умный, чтобы ...А вот это потенциальный отстрел ноги. Нужно работать в очень профессиональной команде, чтобы спокойно применять трюки с компиляторами.
чтобы отличить свойство Length стандартного Array от метода самописного класса. В первом случае Length измениться не может, и компилятор генерирует код, кеширующий длину массива перед циклом.
Просто чтобы продемонстрировать опасность слов «очевино» и «компилятор может» в одном предложении.
var arr5 = new int[5];
var arr10 = new int[10];
var loopArr = arr5;
for(int i =0; i<loopArr.Length; i++)
{
loopArr = arr10;
Console.WriteLine($"\tStep: {i}");
}
Console.WriteLine();
Console.ReadLine();
return;
Собственно, у меня выводится Step: 0… Step: 9. Всего 10 значений, хоть я и использовал переменную типа Array, изначально указывающую на массив из 5 элементов. Вывод: длина массива как минимум, в некоторых случаях, не кешируется при использовании в цикле for.
Я ничего не имею против анализа IL кода. Ничего не имею против бенчмарков (даже тут, на хабре, «продвигал» одну либу). И я приветствую документацию к любому языку\либе\API. Но увы, для клиентов критерий истины — практика. Документация имеет свойство
for (
Container::const_iterator it = container.begin(), end = container.end();
it != end;
++it)
{
// do something
}
Ваши разработчики случаем раньше на С++ не писали?
Да, иногда наш остальной код оптимизирован. А иногда метод, в котором вы оптимизируете, вызывается миллионы или миллиарды раз в час. Ну и там получается нормально.
Да, конечно, в указанных в статье случаях компилятор это делает за вас. Но, однако, бывают и более нетривиальные случаи. Правда?
Должен ли компилятор делать такие оптимизации в данной функции, например?
void whatever(char* s, char* a) {
int k = 0;
for (int i = 0; i < strlen(s); ++i) {
for (int j = 0; j < strlen(s); ++j) {
if (s[i] == s[j]) {
a[k++] = s[i]; // Modifying "a"
}
}
}
}
А вот нет: указатель a
может ссылаться на регион в памяти, принадлежащей s
. В процессе исполнения этого цикла мы модифицируем a
, и, следовательно, можем изменить расположение '\0\'
в s
, которое, в свою очередь, может поменять результат исполнения выражения strlen(s)
, из-за чего компилятор, конечно, оптимизировать этот код не будет.
Не нужно постоянно полагаться на компилятор, т.к.
Compiler is a tool, not a magic stick.
Это не так.
Во-первых, это приветствуется прятать во всякие методы и классы отдельные.
Во-вторых, есть ref/Span, которые полностью безопасны.
В .NET большинство коллекций отслеживают свою версию и итератор проверяет, не поменялась ли версия контейнера по сравнению с той, на которой началось итерирование, но это, строго говоря, не обязательно.
Не приветствоваться должны некорректные обобщения.
Раз уж мы решили обсудить данный момент, то надо ещё посмотреть на скорость компиляции одного и другого фрагмента. И окончательно поставить точку.
И тут выясняется, что не хватает сравнения с новым типом Index из C# 8 :)
https://blogs.msdn.microsoft.com/dotnet/2018/11/12/building-c-8-0/
Конечно серьёзно. Это же property. Скомпилируйте в Debug, будет честный вызов геттера.
Дебаг-версия она как раз для того и существует, чтобы иметь возможность пройти в пошаговом режиме через каждую строчку кода, иначе в чём смысл Debug?
На счет «скрытых» элементов массива не скажу, а вот строки дельфи, например, содержат такие элементы как CodePage, ElemSize, RefCount, ну и наш length. У массивов всё точно так же, за минусом CodePage. Они хранятся по отрицательному смещению от первого элемента строки/массива.
зы Мне кажется я сейчас ещё чуточку больше полюбил Delphi :)
Вы забываете, что в C# компиляция идет не сразу в машинный код, а сначала в байт-код. А потому для определения длины массива есть лишь два варианта — или придумывать отдельную команду IL именно для этой цели, или просто вызвать метод из стандартной библиотеки. Создатели .NET решили пойти по второму пути для упрощения спецификации.
А вот JIT уже никто не мешает вызов известного ему метода get_Length()
заменить простое на чтение переменной из памяти.
FCIMPL2(INT32, ArrayNative::GetLength, ArrayBase* array, unsigned int dimension)
{
FCALL_CONTRACT;
VALIDATEOBJECT(array);
if (array==NULL)
FCThrow(kNullReferenceException);
if (dimension != 0)
{
// Check the dimension is within our rank
unsigned int rank = array->GetRank();
if (dimension >= rank)
FCThrow(kIndexOutOfRangeException);
}
return array->GetBoundsPtr()[dimension];
}
FCIMPLEND
unsigned int rank = array->GetRank();
if (dimension >= rank)
Забавно, тут тоже можно было бы спросить зачем выносить в отдельную переменную.
В общем если есть много времени на то чтобы лишний раз просмотреть как это откомпилировалось то заморачиваться стоит. Но куда проще добавить этот самый «var length» аналог и не нарушать феншуй гармонию кода.
blogs.msdn.microsoft.com/clrcodegeneration/2009/08/13/array-bounds-check-elimination-in-the-clr
stackoverflow.com/questions/16713076/array-bounds-check-efficiency-in-net-4-and-above
В ваших примерах bounds check присутствуют:
cmp esi, eax
jae 056e2d31
Возможно, это ограничение LegacyJIT x86. Либо код запускался без оптимизаций. RyuJIT x64 должен быть умнее (https://stackoverflow.com/a/17138483/136138).
Вы удивитесь, но влияет и очень сильно.
На практике на моем i7-4200HQ разница существенна для RyuJIT x64. Я взял benchmark gist.github.com/aensidhe/0d412e142eb29fd21eea01b5f6462d41 и сравнил For() и ForReverse(). В первом случае RyuJIT убирает bounds check, во втором — оставляет. И у меня на машине получаются такие результаты:
BenchmarkDotNet=v0.11.3, OS=Windows 10.0.17134.472 (1803/April2018Update/Redstone4)
Intel Core i7-4720HQ CPU 2.60GHz (Haswell), 1 CPU, 8 logical and 4 physical cores
Frequency=2533210 Hz, Resolution=394.7561 ns, Timer=TSC
[Host] : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.7.3260.0
Clr LegacyJit : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 64bit LegacyJIT/clrjit-v4.7.3260.0;compatjit-v4.7.3260.0
Clr RyuJit : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 64bit RyuJIT-v4.7.3260.0
Platform=X64 Runtime=Clr
Method | Job | Jit | Mean | Error | StdDev |
----------- |-------------- |---------- |---------:|---------:|---------:|
For | Clr LegacyJit | LegacyJit | 724.0 ns | 1.945 ns | 1.819 ns |
ForReverse | Clr LegacyJit | LegacyJit | 725.5 ns | 9.341 ns | 7.800 ns |
For | Clr RyuJit | RyuJit | 583.3 ns | 2.706 ns | 2.259 ns |
ForReverse | Clr RyuJit | RyuJit | 732.2 ns | 2.568 ns | 2.402 ns |
Явная оптимизация лучше неявной.
Если ничего более важного в голове хранить не нужно, можно хранить и детали внутреннего устройства компилятора, конечно.
for (int i=0; i<array.getLength(); i++) {
if (something) array.nonConstMethod();
}
то оптимизации не произойдет и обращение к array.length() будет происходить при каждой итерации.
Собственно, иногда и нужно сохранять размер контейнера в локальную переменную — когда компилятор не может быть уверенным, а программист может.
Это общий принцип любой оптимизации — не менять наблюдаемого поведения. Если проект работает только при отключенной оптимизации — значит, либо в коде где-то UB, либо в компиляторе баг. Второе возможно, но куда реже чем первое.
А если будет поток?
А многопоточный доступ к изменяемому массиву без синхронизации — это очень плохая идея. Так писать программы не следует.
Например, пишете Вы код, знаете хорошо повадки компилятора. Ок. Потом Вы решили сделать Ваше приложение кроссплатформенным, каков будет масштаб изменений? Явно оптимизированное приложение гораздо проще перенести, в другой ОС будет другой компилятор, который не будет столь же продвинутым, и сделает неэффективный код.
У Вас есть хорошая библиотека, которую Вам нужно перенести на другой ЯП для другого проекта. Опять же неизвестно, что там будет за компилятор, кто будет исполнять полученный код. Известно только одно — явно оптимизированный код с большей вероятностью будет выполняться более оптимально везде, где это может быть. Нормально делай — нормально будет.
И вот куда мне во всей этой куче ситуаций нужно впихнуть знание, что «компилятор С# в такой-то конкретной ситуации неявно догадывается о том, что я хочу сделать». ИМХО, на таком уровне хорошо бы и мне представлять, что именно я написал, и какие есть пути оптимизации.
На мой взгляд, явная оптимизация всё-таки показатель того, что человек понимает, что происходит за ширмой IDE, в которой он работает. И она учит этому пониманию и других, особенно если это прокомментировано в коде.
У Вас есть хорошая библиотека, которую Вам нужно перенести на другой ЯП для другого проекта.
Поскольку я пишу на C#, то и переносить я буду ее с другого языка программирования на C#, и никак иначе. И я не вижу чем знание "повадок" компилятора мне в этом помешает...
А если я захочу перейти на другой ЯП (например, на Rust) — то я просто изучу основные "повадки" компилятора этого самого другого языка.
вот то, что Вы написали — это Ваше 100%-ное знание или просто идеализация компиляторов?
На разумном уровне оптимизации, компиляторы делают только то, в чем уверены. Я не знаю, лезут ли компиляторы каждый раз в тело метода, или просто смотрят на модификаторы (вроде const), решая, когда переменную можно заоптимизировать, а когда нет. Я даже уверен, что для разных языков будет по разному.
Я, конечно, не идеализирую компиляторы, но в плане оптимизаций после этой статьи про предсказание ветвлений, не решаюсь высказываться в духе «Ассемблер быстрее, чем Си».
А если будет поток? Тоже компилятор будет анализировать, используется ли функция в потоке, или меняется ли массив другими потоками?Тогда трындец. Ставьте volatile там, где у вас многопоточный доступ к переменной.
Эта статья скорее о том, что неявная оптимизация может сильно подгадить.
Я не пишу на C#, но например, если в теле цикла меняется размер массива
Ага, «не читал, но осуждаю». В .NET все массивы без исключения фиксированной длины. Она не может измениться, можно только создать новый массив другого размера.
Для подобных вещей есть давно BenchmarkDotNet. Вот, например, вариант кода и результаты.
Как можно увидеть — никакого влияния на производительность вынос Length не имеет.
Спасибо. Сегодня я узнал, что вынесение array.Length из условия цикла в отдельную переменную больше не приводит к замедлению. Проверка выхода за пределы массива оптимизируется в обоих случаях. Видать, компиляторы поумнели. Хотя, надо ещё Моно проверить, там это важно.
Выше бенчмарк, запустите его на моно.
Запущу обязательно. Вечером.
Проверил. Убрал тест Aggregate, добавил несколько с указателями из любопытства. На NET Core не проверял, оно у меня не установлено. Добавил Моно и x86.
Если коротко: нехрен микрооптимизировать наугад, без тестов на целевой платформе, даже если думаешь, что знаешь.
- LegacyJit x64 отдал предпочтение методу с array.Length, вынесенному из условия цикла (540 нс против 770 нс). Но проверка на выход индекса за пределы массива есть в обоих случаях. Нихрена она не убрана.
- В других конфигурациях NET Framework-компиляторам пофиг, скорость одинаковая. Хотя RyuJit догадался убрать проверку в обоих случаях (они вообще идентичные с точностью до замены регистров). Оставил её только в варианте ForReverse. Поэтому без необходимости ходить по массиву в обратную сторону наверное не надо, хотя разница невелика (600 нс против 640 нс).
- Компилятор Mono x64 тоже выбрал вариант с вынесением array.Length из цикла (860 нс против 1040 нс). Так же и в Mono x86 (1800 нс против 1960 нс). Осталась там проверка на выход за пределы или нет, непонятно. Дизассемблер непривычный и без отображения меток. Вообще ХЗ что там происходит.
- Замена статического массива на экземплярный дала изменение только в одном пункте: немного ускорился For на Mono x64, но на общую картину это не повлияло.
- Моно генерирует какие-то бессмысленные портянки машинных инструкций, перетасовывая регистры взад-вперёд. Дизассемблер методов с развёрнутыми циклами сам разворачивается на несколько экранов. В результате чем сильнее цикл развёрнут, тем больше кода и всё медленнее. При том что для LegacyJit x64 это тест даёт лучший результат из всех возможных.
- Лучше таки использовать foreach и для скорости, и для наглядности, и писать быстрее, и негде ошибиться.
- Вариант с указателями тоже неплох (почему-то из трёх похожих лучше всего именно PtrA). Когда очень надо, все средства хороши.
Это зависит от коллекции.
foreach (var x in collection) {}
Это всего лишь сахар для:
using (var e = collection.GetEnumerator())
{
while (e.MoveNext()) {}
}
Так что всё зависит от реализации MoveNext.
Если тип collection — массив и компилятору это точно известно, то там будет просто проход по индексу.
Нестабильной она может быть когда коллекция приходит извне — например это результат выборки из базы данных или файловой системы.
for (int i = 0; i < myObj.MyProperty; i++)
То компилятор может не оптимизировать и вызов будет каждый раз.
blogs.msdn.microsoft.com/vancem/2008/08/19/to-inline-or-not-to-inline-that-is-the-question
Поэтому не вижу ничего страшного, что кто-то выделяет в локальную переменную. Явное лучше неявного.
Advice 2: When possible, use “a.Length” to bound a loop whose index variable is used to index into “a”.
Как минимум раньше эта «оптимизация» приводила к обратному эффекту. Вместо явного (i < a.Length) JIT видел сравнение индекса с какой-то левой переменной и на всякий случай вставлял в код проверку на выход за границы массива.
не вижу обратного перебора.
for (int i = array.Length - 1; i>=0; i--) {
//do smth
}
Очень часто замечаю, что люди пишут вот так
Не знаю, ни разу не встречал. В 99% случаях люди пишут `var result = arr.Sum(x=>x.Something)`, и в оставшемся 1% случае foreach.
Про деоптимизацию из-за сохранения длины в локальную переменную писали уже прилично раз, но раз статья получила столько положительных отзывов, видимо это всё еще не всем ясно.
К слову, for/foreach должны давать одинаковую производительность для массивов. Для список результат иной, насколько я помню.
Нет никакой деоптимизации.
Вообще, не знаю реального кода, который бы на это заявзывался. Такой код навреное только на заре карьеры пишут. Я когда джуном был, тоже в циклах где мог byte/short использовал, если позволяло максимальное значение циклов. И про себя думал «вот поэтому мне 4гб памяти и не хватает, во всех примеров программисты разбазаривают её, int пишут!».
теоретически разумеется.
Теория понятна и даже местами её можно как-то логически подтвердить. Однако Ваше утверждение пахнет чрезмерным обобщением.
От себя добавлю, что вы не рассмотрели несколько разных случаев.
Во первых, у вас вообще не озвучено как массив передаётся в функцию, по ссылке или по значению. В первом случае array.Length это, потенциально, смена контекста, а это, как известно, самая затратная операция.
Во вторых, как тут уже написали выше в комментариях — случай вложенных циклов. Если во вложенном цикле произойдёт обращение к внешнему array.Length, это, возможно тоже спровоцирует смену контекста.
Однако, всё зависит от компилятора и конкретного куска кода. Тут же в комментариях, есть примеры, что достаточно умный компилятор при относительно простом коде создаст локальную переменную автоматически (что в ваших примерах и происходит).
Пока всё ещё считаю, что лучше явно указывать компилятору что делать, инициализируя локальную переменную.
Стоит ли сохранять длину массива в локальную переменную в C#