Как я могу узнать достаточно о CLR, чтобы сделать обоснованные предположения о проблемах производительности? - PullRequest
10 голосов
/ 05 сентября 2010

Да, я , с помощью профилировщика (ANTS).Но на микроуровне он не может сказать вам, как решить вашу проблему.И я сейчас на стадии микрооптимизации.Например, я профилировал это:

for (int x = 0; x < Width; x++)
{
    for (int y = 0; y < Height; y++)
    {
        packedCells.Add(Data[x, y].HasCar);
        packedCells.Add(Data[x, y].RoadState);
        packedCells.Add(Data[x, y].Population);
    }
}

ANTS показал, что линия y-петли отнимает много времени.Я думал, что это потому, что он должен постоянно вызывать получателя высоты.Поэтому я создал локальный int height = Height; перед циклами и сделал проверку внутреннего цикла на y < height.Это фактически ухудшило производительность!ANTS теперь сказал мне, что x-loop-line была проблемой.А?Это должно быть незначительным, это внешний цикл!

В конце концов у меня появилось откровение - возможно, с помощью свойства для привязки к внешнему циклу и локального для привязанного к внутреннему циклу перехода CLR часто междукэш "localals" и кеш "this-pointer" (я привык думать в терминах кеша процессора).Так что я сделал локальный для Width, и это исправило его.

Оттуда было ясно, что я должен сделать локальный и для Data - даже если Data даже не был свойством (это было поле).И действительно, это принесло мне больше производительности.

Удивительно, но переупорядочение циклов x и y (для улучшения использования кэша) не имело никакого значения, даже если массив огромен (3000x3000).

Теперь я хочу узнать почему то, что я сделал, улучшило производительность. Какую книгу вы предлагаете мне прочитать?

Ответы [ 7 ]

10 голосов
/ 05 сентября 2010

CLR via C # от Jeffrey Richter.

Это такая замечательная книга, что кто-то украл ее в моей библиотеке вместе с C # в глубину .

4 голосов
/ 05 сентября 2010

CLR здесь вообще не задействован, все это должно быть переведено в прямой машинный код без вызовов в CLR.JIT-компилятор отвечает за генерацию этого машинного кода, он имеет оптимизатор, который пытается создать наиболее эффективный код.У него есть ограничения, он не может тратить на это большое количество времени.

Одна из важных вещей, которую он делает, - это выяснить, какие локальные переменные должны храниться в регистрах ЦП.Это то, что изменилось, когда вы поместили свойство Height в локальную переменную.Возможно, он решил сохранить эту переменную в регистре.Но теперь есть еще один доступный для хранения другой переменной.Как и переменная x или y, которая критична для скорости.Да, это замедлит его.

У вас плохая диагностика внешнего цикла.Это может быть вызвано тем, что оптимизатор JIT реорганизовал код цикла, что затрудняет отображение профилировщиком отображения машинного кода обратно в соответствующий оператор C #.

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

В любом случае, единственный способ понять это - взглянуть на сгенерированный машинный код.Есть много достойных книг по ассемблерному коду x86, хотя их может быть немного трудно найти в наши дни.Ваша отправная точка - Debug + Windows + Disassembly.

Имейте в виду, однако, что даже машинный код не очень хороший предсказатель того, насколько эффективный код будет выполняться.Современные ядра ЦП чрезвычайно сложны, и машинный код больше не отражает того, что на самом деле происходит внутри ядра.Единственный проверенный и верный способ - это то, что вы уже делали: метод проб и ошибок.

3 голосов
/ 05 сентября 2010

Альбин - нет. Честно говоря, я не думал, что работа вне профилировщика изменит разницу в производительности, поэтому я не стал беспокоиться. Вы думаете, я должен был? Это было проблемой для вас раньше? (Хотя я компилирую с оптимизацией)

Запуск под отладчиком изменяет производительность: когда он запускается под отладчиком, компилятор «точно в срок» автоматически отключает оптимизации (чтобы упростить отладку)!

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

2 голосов
/ 05 сентября 2010

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

Зная это, вы можете захотеть провести тест MyCell Data[][] вместо MyCell Data[,]

1 голос
/ 06 сентября 2010

Пункт 1) Образованные догадки - не способ настройки производительности.В этом случае я могу догадаться примерно так же, как и большинство других, но угадать - неправильный способ сделать это.

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

Я много настраиваю производительность, и вот что я делаю.Я переключаюсь между двумя действиями:

  • Общее измерение времени.Это не требует специальных инструментов.Я не пытаюсь измерить отдельные подпрограммы.

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

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

(Извините, если я не могу написать «узкое место» без кавычек, потому что я не думаю, что когда-либо обнаруживал проблему с производительностью, которая напоминала горлышко бутылкиСкорее, все они просто делали то, что на самом деле не нужно было делать.)

1 голос
/ 05 сентября 2010

Хм, я не думаю, что регистрация в цикле - настоящая проблема.1. Я бы попытался избежать доступа к массиву Data три раза за внутренний цикл.2. Я бы также рекомендовал переосмыслить три утверждения Add: вы, очевидно, трижды обращаетесь к коллекции, чтобы добавить тривиальные данные.Сделайте это только один доступ на одну итерацию и добавьте тип данных, содержащий три записи:

for (int y = 0; ... {
 tTemp = Data[x, y];
 packedCells.Add(new {
  tTemp.HasCar, tTemp.RoadState, tTemp.Population 
 });
}

Другой взгляд показывает, что вы в основном векторизуете матрицу, копируя ее в массив (или некоторую другую последовательную коллекцию)... Это вообще необходимо?Почему бы вам просто не определить специализированный индексатор, который имитирует этот линейный доступ?Более того, если вам нужно только перечислить записи (в этом примере вы не используете произвольный доступ), почему бы вам не использовать адекватное выражение LINQ?

0 голосов
/ 05 сентября 2010

Поскольку комментарий может быть пропущен, я повторяюсь: оптимизировать код довольно громоздко, что само по себе излишне. Вам вообще не нужно явно линеаризовать вашу матрицу, см. Комментарий выше: Определите линеаризующий адаптер, который реализует IEnumerable<MyCell> и подайте его в форматтер.

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

[Serializable()]
class CellMatrix {
 Cell [] mCells;

 public int Rows { get; }
 public int Columns { get; }

 public Cell this (int i, int j) {
  get {
   return mCells[i + Rows * j];    
  }   
  // setter...
 }

 // constructor taking rows/cols...
}

Подобные вещи должны сериализоваться так же быстро, как это делает нативный Array.

Ура, Пол

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...