Какой метод может вызывать нативный API быстрее, чем другие? - PullRequest
1 голос
/ 14 мая 2011

Предположим, у меня есть три куска кода, которые могут вызывать собственный API Win следующим образом:

1) режим компиляции: /clr

#pragma unmanaged
void finc(){
::MessageBox(NULL, "Hi all", "Win32 Message Box",0);
}
#pragma managed

2) режим компиляции: /clr:safe

[DllImport("user32.dll", CharSet=CharSet::Auto)]
int MessageBox(IntPtr, String^ text, String^ caption, unsigned int type);
int main(){
MessageBox(IntPtr::Zero, "Hi all", "Win32 Message Box",0);
}

3) режим компиляции: /clr

void finc(){
::MessageBox(NULL, "Hi all", "Win32 Message Box",0);
}

Какой код может датьлучшая производительность (самая быстрая) при вызове функции Win API и why?

Ответы [ 2 ]

2 голосов
/ 15 мая 2011

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

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

Наиболее фундаментальные правила оптимизации (после профиля вначале) таковы: если подход B выполняет все ту же работу, что и подход A, он не будет быстрее.Вполне возможно провести дополнительную работу в одно и то же время, если процессор тратит время на ожидание данных, которые не находятся в кэше, но вы не можете получить те же результаты быстрее, выполняя их одинаково.

ИтакПервое, что я заметил в вашем примере, кроме того факта, что API MessageBox выполняет гораздо больше работы, чем настройка вызова функции, поэтому ваши сбережения будут незначительными, это то, что некоторые вещи ДОЛЖНЫ быть сделаны для вызова MessageBox.

  • Аргументы должны передаваться в соответствии с соглашением о вызовах.
  • Функция должна вызываться.

Это приводит к следующему выводу:

Невозможно сделать код быстрее, чем простая неуправляемая программа:

#include <windows.h>
void func( void )
{
    ::MessageBoxA(NULL, "Hi all", "Win32 Message Box", MB_OK);
}

int main( void )
{
    func();
}

После того, как оптимизатор встроит вызов, что, безусловно, произойдет, родной оптимизатор вполне хорош, этоПрограмма не будет делать ничего, кроме подготовки списка аргументов и вызова функции, верно?

Ну, это не так.Библиотека времени выполнения C ++ выполнит кучу настроек, которые не нужны для этого примера.Вы могли бы получить огромное улучшение, используя пользовательскую точку входа, но это выглядело бы так, как будто func стало быстрее, на самом деле изменения произошли из кода, который скрыт от вас компилятором.

Даже если вы исключитебиблиотека времени выполнения C ++, у вас все еще остается небольшое количество накладных расходов: загрузчик ОС разрешает импорт, что приводит к другой детали: здесь вызов выполняется с помощью указателя функции, а не непосредственного адреса.Тем не менее, современное предсказание ветвления ЦП устраняет стоимость этой косвенности.

Это действительно будет тема, кстати, управление кодом, добавленное за кулисами.

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

// compile with /clr
#include <windows.h>

void func( void )
{
    ::MessageBoxA(NULL, "Hi all", "Win32 Message Box", MB_OK);
}

int main( void )
{
    func();
}

Это сборка в смешанном режиме с использованием взаимодействия C ++.Компилятор C ++ больше не может встраивать вызов func, но теперь за JIT отвечает оптимизация.Компилятор C ++ / CLI также отключает защиту кода в этом случае, вы даже не можете загрузить сборку смешанного режима без разрешения на запуск неуправляемого кода, поэтому он не будет проверяться при каждом вызове.

ЭтоПрограмма будет работать намного медленнее, чем первая, потому что с появлением .NET инициализация библиотеки времени выполнения значительно усложнилась.Но после запуска JIT вызов функции будет идентичен первому, стоимость вызова func идентична.

Давайте рассмотрим другой случай из вопроса:

// compile with /clr
#include <windows.h>

#pragma managed(push, off)
void func( void )
{
    ::MessageBoxA(NULL, "Hi all", "Win32 Message Box", 0);
}
#pragma managed(pop)

int main( void )
{
    func();
}

Нам все еще нужно загрузить среду выполнения .NET, нам все еще нужно JIT, нам все еще нужно установить аргументы и вызвать MessageBox.Так что не может быть быстрее , чем в приведенных выше примерах.Поскольку реализация func не является управляемым кодом, JIT не может встроить его.Но теперь это возможно для компилятора C ++, поскольку func больше не является управляемой функцией, которую нужно оставлять в покое.Если оптимизатор указывает func, мы имеем производительность, идентичную предыдущему случаю, в противном случае у нас есть один дополнительный вызов функции.На самом деле не о чем беспокоиться.

Следующий случай:

// compile with /clr:pure
#include <windows.h>

void func( void )
{
    ::MessageBoxA(NULL, "Hi all", "Win32 Message Box", MB_OK);
}

int main( void )
{
    func();
}

Теперь у нас больше нет сборки в смешанном режиме, и у нас больше нет взаимодействия с C ++.Вместо этого компилятор сгенерирует соответствующую сигнатуру p / invoke (почему вы пишете сигнатуры p / invoke вручную?).В результате импорт разрешает не загрузчик ОС, а среда выполнения .NET.И теперь этот адрес импорта может быть доступен для JIT, который теоретически может кодировать исправленный адрес непосредственно в инструкцию вызова и избежать указателя на функцию, хотя мы уже знаем, что предсказания ветвлений делают эту разницу спорные.Но p / invoke предназначен для безопасности, а не для скорости, так что вы действительно получаете некоторую дополнительную проверку стека вокруг вызова, чтобы убедиться, что подпись p / invoke была правильной.

Итог: эта версия определенно медленнее, но не настолько, чтобы быть значимым.

Давайте сделаем одно изменение TINY:

// compile with /clr:safe
#include <windows.h>

void func( void )
{
    ::MessageBoxA(NULL, "Hi all", "Win32 Message Box", MB_OK);
}

int main( void )
{
    func();
}

Хорошо, запросил сборку для проверки.Все функции p / invoke такие же, как /clr:pure, но этот код больше не ограничен для работы в доверенной среде.Поэтому проверки доступа к коду не могут быть отключены.Безопасность проверяет ярлык, когда среда полностью доверяет, и в этом примере приложение не включило частичное доверие, поэтому оно не будет иметь большого значения.В реальном сценарии, когда сборка C ++ / CLI загружается в более крупное приложение .NET, результаты проверок безопасности могут быть ОГРОМНЫМИ, поскольку для каждой из них требуется обход стека.Один из способов смягчить это - сделать утверждения CAS как можно ближе к стеку вызовов в любое время, когда многие вызовы p / invoke будут выполняться последовательно, но выполнение с частичным доверием приведет к замедлению ваших вызовов к собственному коду.

// compile with /clr:safe
[DllImport("user32.dll", CharSet=CharSet::Auto)]
int MessageBox(System::IntPtr, System::String^ text, System::String^ caption, unsigned type);

int main( void )
{
    MessageBox(System::IntPtr::Zero, "Hi all", "Win32 Message Box", 0);
}

Это пример № 2 из вопроса.Это ужасно медленный по сравнению, хотя все еще вероятно недостаточно, чтобы заметить помимо стоимости MessageBox самой.Причина в том, что предоставленная пользователем подпись p / invoke плохо субоптимальна по сравнению с той, которую компилятор мог сгенерировать автоматически, используя файл заголовка.Не только отсутствует поддержка GetLastError, но тип параметра изменен с SByte* на System::String^.Теперь p / invoke предстоит проделать большую работу: выделить пространство, которое не может быть перемещено сборщиком мусора, и скопировать туда содержимое строки.Что еще хуже, CharSet.Auto означает ANSI.System::String - это Юникод.Поэтому P / Invoke необходимо не только копировать содержимое строки, но и выполнять преобразование Unicode -> ANSI.

Опять же, для MessageBox издержки незначительны по сравнению с задачей.Но для других функций, таких как glVertex2i и GetClassName, дополнительная работа, такая как копирование аргументов, преобразование Unicode <-> ANSI и обход стека CAS, может привести к снижению производительности.

1 голос
/ 14 мая 2011

Просто протестируйте его с помощью профилирования.Вызовите метод 100 000 раз, рассчитайте время и распечатайте номера для каждого теста.

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