2017-02-08 1 views
2

Когда я выполняю следующий код в режиме деблокирования на моей машине, выполнение делегата с ненулевой целью всегда немного быстрее, чем когда делегат имеет нулевую цель (я ожидал, что это будет быть эквивалентным или медленнее)..net делегат без цели медленнее, чем с целевым

Я действительно не ищу микро-оптимизацию, но мне было интересно, почему это так?

static void Main(string[] args) 
{ 
    // Warmup code 

    long durationWithTarget = 
     MeasureDuration(() => new DelegatePerformanceTester(withTarget: true).Run()); 

    Console.WriteLine($"With target: {durationWithTarget}"); 

    long durationWithoutTarget = 
     MeasureDuration(() => new DelegatePerformanceTester(withTarget: false).Run()); 

    Console.WriteLine($"Without target: {durationWithoutTarget}"); 
} 

/// <summary> 
/// Measures the duration of an action. 
/// </summary> 
/// <param name="action">Action which duration has to be measured.</param> 
/// <returns>The duration in milliseconds.</returns> 
private static long MeasureDuration(Action action) 
{ 
    Stopwatch stopwatch = Stopwatch.StartNew(); 

    action(); 

    return stopwatch.ElapsedMilliseconds; 
} 

class DelegatePerformanceTester 
{ 
    public DelegatePerformanceTester(bool withTarget) 
    { 
     if (withTarget) 
     { 
      _func = AddNotStatic; 
     } 
     else 
     { 
      _func = AddStatic; 
     } 
    } 
    private readonly Func<double, double, double> _func; 

    private double AddNotStatic(double x, double y) => x + y; 
    private static double AddStatic(double x, double y) => x + y; 

    public void Run() 
    { 
     const int loops = 1000000000; 
     for (int i = 0; i < loops; i++) 
     { 
      double funcResult = _func.Invoke(1d, 2d); 
     } 
    } 
} 
+0

Duplicate? http://stackoverflow.com/questions/2082735/performance-of-calling-delegates-vs-methods –

+0

@ Dan Я не думаю, что этот вопрос является дубликатом другого, который больше связан с сравнением прямых вызовов метода против делегатов. В моем случае меня интересуют выступления делегатов. –

+1

'Я действительно не ищу микро-оптимизацию' Тогда зачем задавать вопрос о микрооптимизации? – Servy

ответ

8

Я напишу это одно вверх, есть довольно приличный совет программирования за ним, что должен иметь значение для любого C# программиста, который заботится о написании быстрого кода. В целом я предостерегаю об использовании микро-тестов, различия в 15% или менее не являются в общем статистически значимыми из-за непредсказуемости скорости выполнения кода на современном ядре ЦП. Хороший подход к уменьшению вероятности измерения чего-то, чего нет, заключается в повторении теста не менее 10 раз для удаления эффектов кеширования и замены теста, чтобы можно было исключить эффекты выравнивания кода.

Но то, что вы видели, реально, делегаты, вызывающие статический метод, на самом деле медленнее. Эффект довольно мал в коде x86, но он значительно хуже в коде x64, не забудьте поработать с Project> Properties> Build tab> Предпочитайте 32-битные и целевые настройки платформы, чтобы попробовать оба.

Зная, почему это медленнее, необходимо посмотреть на машинный код, генерируемый джиттером. В случае делегатов этот код очень хорошо скрыт. Вы не увидите этого, когда посмотрите на код с помощью Debug> Windows> Disassembly. И вы не можете даже пропустить код, управляемый отладчик был написан, чтобы скрыть его и полностью отказывается его показывать. Я должен описать технику, чтобы вернуть «визуальный» обратно в Visual Studio.

Мне нужно немного поговорить о «заглушках». Штук - это небольшая скобка машинного кода, который CLR динамически создает в дополнение к коду, который генерирует дрожание. Штыри используются для реализации интерфейсов, они обеспечивают гибкость в том, что порядок методов в таблице методов для класса не должен соответствовать порядку методов интерфейса. И они важны для делегатов, тема этого вопроса. Заготовки также имеют значение для компиляции точно в момент, исходный код в заглушке указывает на точку входа в дрожание, чтобы получить метод, скомпилированный при его вызове. После этого заглушка заменяется, теперь вызывается метод дрожащей цели. Это заглушка делает медленный вызов статического метода, заглушка для цели статического метода более сложна, чем заглушка для метода экземпляра.


Чтобы увидеть заглушки, вы должны прервать отладчик, чтобы заставить его показать свой код. Требуется некоторая настройка: сначала выберите «Сервис»> «Параметры»> «Отладка»> «Общие». Отмените флажок «Только мой код», снимите флажок «Подавить оптимизацию JIT». Если вы используете VS2015, то отметьте «Использовать режим управляемой совместимости», отладчик VS2015 очень глючит и серьезно относится к этому способу отладки, этот параметр обеспечивает обходной путь, заставляя использовать управляемый отладчик VS2010. Переключитесь на конфигурацию Release. Затем «Проект»> «Свойства»> «Отладка» отметьте флажок «Включить отладку собственного кода». И Project> Properties> Build, untick флажок «Предпочитаю 32-бит» и «Платформа целевой» должен быть AnyCPU.

Установите точку останова в методе Run(), будьте осторожны, чтобы точки останова были не очень точны в оптимизированном коде. Лучше всего настроить заголовок метода. Как только он ударит, используйте Debug> Windows> Disassembly, чтобы увидеть машинный код, сгенерированный дрожанием.Вызов делегата Invoke выглядит это на ядре Haswell, может не соответствовать тому, что вы видите, если у вас есть старый процессор, который не поддерживает AVX еще:

   funcResult += _func.Invoke(1d, 2d); 
0000001a mov   rax,qword ptr [rsi+8]    ; rax = _func    
0000001e mov   rcx,qword ptr [rax+8]    ; rcx = _func._methodBase (?) 
00000022 vmovsd  xmm2,qword ptr [0000000000000070h] ; arg3 = 2d 
0000002b vmovsd  xmm1,qword ptr [0000000000000078h] ; arg2 = 1d 
00000034 call  qword ptr [rax+18h]     ; call stub 

Метод 64-разрядный вызов передает первые 4 аргумента в регистрах любые дополнительные аргументы передаются через стек (не здесь). Здесь используются регистры XMM, поскольку аргументы являются плавающей точкой. На данный момент джиттер еще не знает, является ли метод статическим или экземпляром, который не может быть обнаружен до тех пор, пока этот код не выполнится. Это задача заглушки, чтобы скрыть разницу. Предполагается, что это будет метод экземпляра, поэтому я аннотировал arg2 и arg3.

Установите точку останова в инструкции CALL, второй раз, когда она попадает (так что после того, как заглушка больше не указывает на дрожание), вы можете взглянуть на нее. Это нужно сделать вручную, используйте Debug> Windows> Registers и скопируйте значение регистра RAX. Отладка> Windows> Память> Память1 и вставьте значение, поставьте «0x» перед ним и добавьте 0x18. Щелкните это окно правой кнопкой мыши и выберите «8-байтовое целое число», скопируйте первое отображаемое значение. Это адрес кода заглушки.

Теперь трюк, на данный момент управляемый механизм отладки все еще используется и не позволит вам посмотреть на код заглушки. Вы должны принудительно переключить переключатель режима, чтобы управляемый механизм отладки контролировался. Используйте Debug> Windows> Call Stack и дважды щелкните вызов метода внизу, например RtlUserThreadStart. Заставляет отладчик переключать двигатели. Теперь вы можете пойти и можете вставить адрес в поле «Адрес», поставить «0x» перед ним. Out выдает код заглушки:

00007FFCE66D0100 jmp   00007FFCE66D0E40 

Очень простой, прямой переход к методу назначения делегата. Это будет быстрый код. Джиттер правильно угадал метод экземпляра, и объект-делегат уже предоставил аргумент this в регистре RCX, поэтому ничего особенного не нужно делать.

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

000001FE559F0850 mov   rax,rsp     ; ? 
000001FE559F0853 mov   r11,rcx     ; r11 = _func (?) 
000001FE559F0856 movaps  xmm0,xmm1    ; shuffle arg3 into right register 
000001FE559F0859 movaps  xmm1,xmm2    ; shuffle arg2 into right register 
000001FE559F085C mov   r10,qword ptr [r11+20h] ; r10 = _func.Method 
000001FE559F0860 add   r11,20h     ; ? 
000001FE559F0864 jmp   r10      ; jump to _func.Method 

код немного шаткий и не оптимальна, Microsoft, вероятно, может сделать лучшую работу здесь, и я не 100% уверен, что я аннотированный правильно. Я предполагаю, что ненужная команда mov rax, rsp применима только для заглушек для методов с более чем 4 аргументами. Не знаю, зачем нужна инструкция добавления. Наиболее важной деталью, которая имеет значение, является перемещение регистра XMM, она должна перетасовывать их, потому что статический метод не имеет аргумента this. Именно это требование перестановки делает код более медленным.

Вы можете сделать то же самое упражнение с x86 джиттером, статический метод заглушка теперь выглядит следующим образом:

04F905B4 mov   eax,ecx 
04F905B6 add   eax,10h 
04F905B9 jmp   dword ptr [eax]  ; jump to _func.Method 

Намного проще, чем 64-битной заглушка, поэтому 32-битный код не страдает от замедление почти столько же. Одна из причин, по которой это так сильно отличается, заключается в том, что 32-битный код пропускает плавающие точки в стеке FPU, и их не нужно перетасовывать. Это не обязательно будет быстрее, если вы используете интегральные или объектные аргументы.


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

+0

Многие, большое спасибо за это яснее. Теперь имеет смысл, почему существует разница между ними. И чтобы доказать, что вы меня не заставили спать: «прямо к цели делегата». Наверное, вы имеете в виду «прямой переход к методу делегата»? еще раз спасибо –

Смежные вопросы