Компилируются ли функции в собственном классе C++/CLI в MSIL или собственный машинный код x64?

Этот вопрос связан с другим моим вопросом под названием Вызов MASM PROC из C++/CLI в режиме x64 приводит к неожиданным проблемам с производительностью. Я не получил никаких комментариев и ответов, но в конце концов я сам обнаружил, что проблема вызвана переходниками функций, которые вставляются компилятором всякий раз, когда управляемая функция вызывает неуправляемую, и наоборот. Я не буду вдаваться в подробности еще раз, потому что сегодня я не хочу заострять внимание на еще одном последствии этого механизма настройки.

Чтобы дать некоторый контекст для вопроса, моя проблема заключалась в замене функции C++ для умножения 64-128-битных целых чисел без знака в неуправляемом классе C++/CLI функцией в файле MASM64 ради производительности. Замена ASM максимально проста:

AsmMul1 proc ; ?AsmMul1@@$$FYAX_K0AEA_K1@Z

; ecx  : Factor1
; edx  : Factor2
; [r8] : ProductL
; [r9] : ProductH

mov  rax, rcx            ; rax = Factor1
mul  rdx                 ; rdx:rax = Factor1 * Factor2
mov  qword ptr [r8], rax ; [r8] = ProductL
mov  qword ptr [r9], rdx ; [r9] = ProductH
ret

AsmMul1 endp

Я ожидал большого прироста производительности, заменив скомпилированную функцию с четырьмя 32-битными умножениями на 64-битные простой инструкцией CPU MUL. Большим сюрпризом стало то, что версия ASM была примерно в четыре раза медленнее (!), чем версия C++. После долгих исследований и тестов я обнаружил, что некоторые вызовы функций в C++/CLI включают преобразование, которое, очевидно, является настолько сложной вещью, что занимает гораздо больше времени, чем сама функция.

Прочитав больше об этом thunking оказалось, что всякий раз, когда вы используете параметр компилятора /clr, соглашение о вызовах всех функций незаметно изменяется на __clrcall, что означает, что они становятся управляемыми функциями. Исключениями являются функции, которые используют встроенные функции компилятора, встроенный ASM и вызовы других DLL через dllimport, и, как показали мои тесты, сюда входят функции, вызывающие внешние функции ASM.

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

Теперь, после этого длинного пролога, давайте перейдем к сути моего вопроса. Насколько я понимаю соглашение __clrcall и переключатель компилятора /clr, такая маркировка функции в неуправляемом классе C++ заставляет компилятор выдавать код MSIL. Я нашел это предложение в документации __clrcall:

Помечая функцию как __clrcall, вы указываете, что реализация функции должна быть MSIL и что собственная функция точки входа не будет создана.

Честно говоря, меня это пугает! В конце концов, я прохожу через трудности написания кода C++/CLI, чтобы получить настоящий нативный код, то есть сверхбыстрый машинный код x64. Однако это не похоже на значение по умолчанию для смешанные сборки. Пожалуйста, поправьте меня, если я ошибаюсь: если я использую значения проекта по умолчанию, заданные VC2017, моя сборка содержит MSIL, который будет скомпилирован JIT. Истинный?

Существует #pragma managed, который, по-видимому, запрещает создание MSIL в пользу собственного кода для каждой функции. Я протестировал его, и он работает, но проблема в том, что thunking снова мешает, как только собственный код вызывает управляемую функцию, и наоборот. В моем проекте C++/CLI я не нашел способа настроить преобразование и генерацию кода без снижения производительности в каком-либо месте.

Итак, что я спрашиваю себя сейчас: в чем вообще смысл использования C++/CLI? Дает ли это мне преимущества в производительности, когда все по-прежнему скомпилировано в MSIL? Может быть, лучше написать все на чистом C++ и использовать Pinvoke для вызова этих функций? Я не знаю, я как бы застрял здесь.

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


person SBS    schedule 22.03.2019    source источник
comment
Я думаю, что эта ссылка может вам помочь: stackoverflow.com/a/5698663/11241587   -  person Theodor Badea    schedule 22.03.2019
comment
Прошло некоторое время с тех пор, как я использовал Masm и никогда не использовал управляемый код. Во-первых, компилятор выполняет оптимизацию, и ваш код ручной сборки никогда не будет таким же хорошим, как оптимизатор. Какой управляемый код окружает каждый вызов, чтобы несоответствие стека не могло привести к сбою машины. Он также добавляет сегменты памяти, поэтому доступ к памяти за пределами памяти, назначенной программе, вызывает исключение и может восстанавливаться после исключений, не вызывая сбоя машины. Как только вы настроите код С++, который вызывает, я не думаю, что он защищен так же, как в С#, если вы не добавите директивы.   -  person jdweng    schedule 22.03.2019
comment
@TheodorBadea Спасибо за ссылку. Я сам считаю, что единственный возможный выход — поместить весь код, требующий производительности, в пару исходных файлов, скомпилированных без /clr, чтобы минимизировать управляемые/неуправляемые переходы. Однако это каким-то образом разрушает элегантную идею C++/CLI. Затем вы должны позаботиться о вызовах из управляемых классов, которые вызывают этот код. Например, я имею в виду управляемые свойства, которые вызывают функции получения/установки в нативных классах. Таким образом, проблема с производительностью может быть просто перенесена в другое место в проекте.   -  person SBS    schedule 22.03.2019
comment
Я ожидал большого прироста производительности, заменив скомпилированную функцию с четырьмя 32-битными умножениями на 64-битные простой инструкцией ЦП MUL. . На самом деле вы заменили функцию C++ на функцию ASM, а не на инструкцию ASM. Функции C++ могут быть встроены компиляторами C++, что устраняет все накладные расходы на вызовы функций. Ваш AsmMul1 должен быть вызван с обычными накладными расходами   -  person MSalters    schedule 22.03.2019
comment
@MSalters Накладные расходы на вызов / возврат быстрого вызова x64 для этой процедуры с четырьмя инструкциями с параметрами, передаваемыми в регистрах, очень малы по сравнению с общими усилиями, выполняемыми внутри функции C ++, которая включает четыре IMUL и множество операций добавления и сдвиги. Встраивание вряд ли может перевесить это здесь.   -  person SBS    schedule 22.03.2019