Я напишу это одно вверх, есть довольно приличный совет программирования за ним, что должен иметь значение для любого 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 не оптимизация.
Duplicate? http://stackoverflow.com/questions/2082735/performance-of-calling-delegates-vs-methods –
@ Dan Я не думаю, что этот вопрос является дубликатом другого, который больше связан с сравнением прямых вызовов метода против делегатов. В моем случае меня интересуют выступления делегатов. –
'Я действительно не ищу микро-оптимизацию' Тогда зачем задавать вопрос о микрооптимизации? – Servy