Оптимизация компилятора С#

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

 public static uint CalculateCheckSum(string str) { 
    char[] charArray = str.ToCharArray();
    uint checkSum = 0;
    foreach (char c in charArray) {
        checkSum += c;
    }
    return checkSum % 256;
 }

Я работаю с коллегой, который делает некоторые бенчмаркинга/оптимизации для приложения обработки сообщений. Выполнение 10 миллионов итераций этой функции с использованием той же вводной строки заняло около 25 секунд в Visual Studio 2012, однако, когда проект был построен с использованием опции "Оптимизировать код", включен тот же код, выполненный за 7 секунд для тех же 10 миллионов итераций.

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

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

class Program
{
    public static uint CalculateCheckSum(string str)
    {
        char[] charArray = str.ToCharArray();
        uint checkSum = 0;
        foreach (char c in charArray)
        {
            checkSum += c;
        }
        return checkSum % 256;
    }

    static void Main(string[] args)
    {
        string stringToCount = "8=FIX.4.29=15135=D49=SFS56=TOMW34=11752=20101201-03:03:03.2321=DEMO=DG00121=155=IBM54=138=10040=160=20101201-03:03:03.23244=10.059=0100=ARCA10=246";
        Stopwatch stopwatch = Stopwatch.StartNew();
        for (int i = 0; i < 10000000; i++)
        {
            CalculateCheckSum(stringToCount);
        }
        stopwatch.Stop();
        Console.WriteLine(stopwatch.Elapsed);
    }
}

Запуск отладки с отключением оптимизации. Я вижу 13 секунд, я получаю 2 секунды.

Запуск в режиме Release с оптимизацией с 3,1 секунды и на 2,3 секунды.

Ответы

Ответ 1

Чтобы посмотреть, что делает компилятор С# для вас, вам нужно посмотреть на IL. Если вы хотите увидеть, как это влияет на JIT-код, вам нужно посмотреть на собственный код, как описано Scott Chamberlain. Имейте в виду, что JIT-код будет зависеть от архитектуры процессора, версии CLR, способа запуска процесса и, возможно, других вещей.

Обычно я начинал с IL, а затем потенциально смотрел код JIT.

Сравнение IL с использованием ildasm может быть несколько сложным, так как оно включает метку для каждой команды. Вот две версии вашего метода, скомпилированные с оптимизацией (без использования оптимизации) (с использованием компилятора С# 5), с посторонними метками (и инструкциями nop), чтобы сделать их максимально удобными для сравнения:

Оптимизированное

  .method public hidebysig static uint32 
          CalculateCheckSum(string str) cil managed
  {
    // Code size       46 (0x2e)
    .maxstack  2
    .locals init (char[] V_0,
             uint32 V_1,
             char V_2,
             char[] V_3,
             int32 V_4)
    ldarg.0
    callvirt   instance char[] [mscorlib]System.String::ToCharArray()
    stloc.0
    ldc.i4.0
    stloc.1
    ldloc.0
    stloc.3
    ldc.i4.0
    stloc.s    V_4
    br.s       loopcheck
  loopstart:
    ldloc.3
    ldloc.s    V_4
    ldelem.u2
    stloc.2
    ldloc.1
    ldloc.2
    add
    stloc.1
    ldloc.s    V_4
    ldc.i4.1
    add
    stloc.s    V_4
  loopcheck:
    ldloc.s    V_4
    ldloc.3
    ldlen
    conv.i4
    blt.s      loopstart
    ldloc.1
    ldc.i4     0x100
    rem.un
    ret
  } // end of method Program::CalculateCheckSum

Неоптимизированный

  .method public hidebysig static uint32 
          CalculateCheckSum(string str) cil managed
  {
    // Code size       63 (0x3f)
    .maxstack  2
    .locals init (char[] V_0,
             uint32 V_1,
             char V_2,
             uint32 V_3,
             char[] V_4,
             int32 V_5,
             bool V_6)
    ldarg.0
    callvirt   instance char[] [mscorlib]System.String::ToCharArray()
    stloc.0
    ldc.i4.0
    stloc.1
    ldloc.0
    stloc.s    V_4
    ldc.i4.0
    stloc.s    V_5
    br.s       loopcheck

  loopstart:
    ldloc.s    V_4
    ldloc.s    V_5
    ldelem.u2
    stloc.2
    ldloc.1
    ldloc.2
    add
    stloc.1
    ldloc.s    V_5
    ldc.i4.1
    add
    stloc.s    V_5
  loopcheck:
    ldloc.s    V_5
    ldloc.s    V_4
    ldlen
    conv.i4
    clt
    stloc.s    V_6
    ldloc.s    V_6
    brtrue.s   loopstart

    ldloc.1
    ldc.i4     0x100
    rem.un
    stloc.3
    br.s       methodend

  methodend:
    ldloc.3
    ret
  }

Примечание:

  • Оптимизированная версия использует меньшее количество локальных пользователей. Это может позволить JIT более эффективно использовать регистры.
  • Оптимизированная версия использует blt.s, а не clt, а затем brtrue.s, проверяя, следует ли снова повторять цикл (это причина для одного из дополнительных локалей).
  • Неоптимизированная версия использует дополнительную локальную память для хранения возвращаемого значения перед возвратом, предположительно для облегчения отладки.
  • Неоптимизированная версия имеет безусловную ветвь перед ее возвратом.
  • Оптимизированная версия короче, но я сомневаюсь, что она достаточно короткая, чтобы быть встроенной, поэтому я подозреваю, что это неуместно.

Ответ 2

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

Скомпилируйте сборку, затем сделайте ее копию и снова скомпилируйте с оптимизацией. Затем откройте обе сборки в рефлекторе .net и сравните разницу скомпилированного IL.

Обновление: Рефлектор Dotnet доступен по адресу http://www.red-gate.com/products/dotnet-development/reflector/

Обновление 2: IlSpy кажется хорошей альтернативой с открытым исходным кодом. http://ilspy.net/

Альтернативы Open Source для отражателя?

Ответ 3

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

Сначала создайте свой оптимизированный код и запустите его без прикрепленного отладчика (JIT-компилятор сгенерирует другой код, если отладчик подключен). Запустите свой код, чтобы вы знали, что раздел был введен хотя бы один раз, чтобы JIT-компилятор имел возможность обработать его, а в Visual Studio - Debug->Attach To Process.... В новом меню выберите свое приложение.

Поместите точку останова в то место, о котором вы думаете, и дайте программе остановиться, после остановки перейдите к Debug->Windows->Dissasembly. Это покажет вам скомпилированный код, созданный JIT, и вы сможете проверить, что он делает.

(Нажмите, чтобы увеличить)введите описание изображения здесь