Войти в систему

Home
    - Создать дневник
    - Написать в дневник
       - Подробный режим

LJ.Rossia.org
    - Новости сайта
    - Общие настройки
    - Sitemap
    - Оплата
    - ljr-fif

Редактировать...
    - Настройки
    - Список друзей
    - Дневник
    - Картинки
    - Пароль
    - Вид дневника

Сообщества

Настроить S2

Помощь
    - Забыли пароль?
    - FAQ
    - Тех. поддержка



Пишет ringill ([info]ringill)
@ 2006-12-08 16:03:00


Previous Entry  Add to memories!  Tell a Friend!  Next Entry
[.NET] Вызов низкоуровневого кода из CLR
Речь идёт о вызове низкоуровневой функции, принадлежащей динамической библиотеке (DLL), из управляемого кода (на C#, например). Есть несколько способов это сделать:

  1. P/Invoke — наиболее распространённый способ. Прототип вызываемой функции описывается в коде на C# с  указанием директивы DllImport, и вызывается элементарно:
    [DllImport("mylib.dll")]
    static public extern int MyFunc (int param1, int param2);
    
    static void Main ()
    {
      int res = MyFunc(10, 20);
    }
    

    Это работает медленно. Я не знаю, почему. Если такие вызовы редки — ничего
    страшного, но если их миллионы (а сама вызываемая функция проста), то избыточность
    становится заметна.
    UPDATE: На современной .NET уже быстро, поэтому всё последующее можно не читать.



  2. Чтобы вызывать функцию из DLL миллионы раз, достаточно один раз получить её адрес. Процесс вызова прост:
    кладём аргументы на стек, кладём адрес функции на стек, делаем calli и ret.
    Получить адрес можно вызовом GetProcAddress, через DllImport из kernel32.dll. Медленно, но всего один раз на миллион вызовов.

    Для работы со стеком напрямую можно задействовать MSIL, скомпилировав код
    на этом языке в отдельную сборку. Это товарищ Neil Cowburn догадался.
    Вот пример: делаем файл FunctionCaller.il
    .assembly extern mscorlib {}
    .assembly FunctionCaller {}
    
    .class public FunctionCaller
    {
      .method public static int32 IntMethod_Int_Int (native int pfn, int32 param1, int32 param2) 
      { 
        .maxstack 3
        ldarg.1 // для передачи по ссылке следует использовать ldarga
        ldarg.2
        ldarg.0
        calli unmanaged stdcall int32 (int32, int32) 
        ret 
      } 
    }
    

    Компилируем его командой ilasm.exe /dll FunctionCaller.il, и FunctionCaller.dll подключаем
    в проект как Reference. А вот код на C#:
    [DllImport("kernel32.dll")]
    static extern IntPtr LoadLibrary (string file_name);
    
    [DllImport("kernel32.dll")]
    static extern IntPtr GetProcAddress (IntPtr module, string proc_name);
    
    [DllImport("kernel32.dll")]
    static extern bool FreeLibrary (IntPtr module);
    
    static void Main ()
    {
      IntPtr lib = LoadLibrary("mylib.dll");
      IntPtr myfunc_ptr = GetProcAddress(lib, "MyFunc");
      
      int res = FunctionCaller.IntMethod_Int_Int(myfunc_ptr, 10, 20);
      
      FreeLibrary(lib);
    }
    

  3. Указанный код для работы со стеком можно конструировать на этапе выполнения, пользуясь
    механизмами System.Reflection. Полноценный пример приведён в microsoft.public.dotnet.languages.csharp.
    Однако, там используется вызов MethodInfo.Invoke, который хоть и быстрее, чем P/Invoke, но тоже избыточен.

    Для преодоления его избыточности есть приём —
    создание делегата с помощью CreateDelegate. Вот как это делается:
    delegate int MyFuncInvoker (int param1, int param2);
    
    static void Main ()
    {
      IntPtr lib = LoadLibrary("mylib.dll");
      IntPtr myfunc_ptr = GetProcAddress(lib, "MyFunc");
    
      AssemblyName assembly_name = new AssemblyName("Invokable");
      AssemblyBuilder assembly_builder =
         AppDomain.CurrentDomain.DefineDynamicAssembly(assembly_name, AssemblyBuilderAccess.Run);
      ModuleBuilder module_builder = assembly_builder.DefineDynamicModule("CInvoker");
    
      // тип возвращаемого значения и типы параметров нашей функции
      Type return_type = typeof(int);
      Type[] parameter_types = new Type[] { typeof(int), typeof(int) };
    
      DynamicMethod dynamic_method = new DynamicMethod("Invoker",
         MethodAttributes.Public | MethodAttributes.Static, CallingConventions.Standard,
         return_type, parameter_types, module_builder, false);
    
      // начинаем конструировать код, аналогичный вышеприведённому на MSIL
      ILGenerator il_generator = dynamic_method.GetILGenerator();
      
      // положить параметры на стек; опять же, для передачи по ссылке следует использовать Ldarga
      il_generator.Emit(OpCodes.Ldarg, 0);
      il_generator.Emit(OpCodes.Ldarg, 1);
    
      // положить адрес функции на стек
      il_generator.Emit(OpCodes.Ldc_I4, myfunc_ptr.ToInt32());
      // (для 64-битных систем следует написать (OpCodes.Ldc_I8, myfunc_ptr.ToInt64())
    
      // сделать calli и ret
      il_generator.EmitCalli(OpCodes.Calli, CallingConvention.StdCall, return_type, parameter_types);
      il_generator.Emit(OpCodes.Ret);
      
      // код сконструировали; теперь создаём делегат
      MyFuncInvoker myfunc_invoker = (MyFuncInvoker)dynamic_method.CreateDelegate(typeof(MyFuncInvoker));
    
      int res = myfunc_invoker(10, 20);
      
      FreeLibrary(lib);
    }


Сравнительное время миллиона вызовов пустой функции
P/Invoke

~5 сек.

Код от ILGenerator, вызванный
через MethodInfo.Invoke()

~3.7 сек.

Код от ILGenerator, вызванный
через DynamicMethod.CreateDelegate()
~0.5 сек.

Вызов через отдельную сборку на MSIL

~0.5 сек.




Вызовы двумя последними способами происходят быстрее в десять раз.
Чем они отличаются между собой? Первый лучше тем, что не требует добавлять в проект отдельную сборку.


(Добавить комментарий)


[info]aamonster@lj
2008-03-18 10:51 (ссылка)
А тема про тормоза PInvoke все еще актуальна? Вот только что на Visual Studio 2005 (даже не 2008) пробовал:

В dll:
extern "C" __declspec( dllexport ) void empty()
{
}

В c#-коде:
[DllImport("test_dll.dll")]
public static extern void empty();
...
for (int i = 0; i < 100000000; i++)
{
empty();
}

- так лишнего кода почти нет (единственное что - переменная i хранится в стеке, а не в регистре) и выполняется 100 миллионов циклов за 6 секунд (Pentium D 3GHz), а никак не 5 секунд на миллион вызовов...

Да, и пример с делегатом так и не удалось правильно собрать :-( - выдает System.AccessViolationException. Поэтому сам сравнить быстродействие не смог.

(Ответить) (Ветвь дискуссии)


[info]ringill@lj
2008-03-20 17:10 (ссылка)
И я сейчас проверил в VS 2008, получил те же 6 секунд.
Значит, исправили. Молодцы.

(Ответить) (Уровень выше) (Ветвь дискуссии)


[info]aamonster@lj
2008-03-21 04:41 (ссылка)
Ну, все-таки не 100% исправили: тот же тест, но с вызовом native->native дает 0.4с на те же 10 млн вызовов. А с вызовом не через [DllImport], а через обвязку в виде unmanaged-функции на C++ - 1.2с.

Но если функция не совсем тривиальная, то 180 тактов оверхеда - это еще туда-сюда.

Кстати, выяснился забавный факт: если смотреть ассемблерный код, запустив release-версию под отладчиком Visual Studio - весь код, создающий оверхед, прячется. Чтобы посмотреть реальный код - надо воткнуть __asm int 3 и начинать отлаживать через Just-In-Time debugger.

(Ответить) (Уровень выше) (Ветвь дискуссии)


[info]ringill@lj
2008-03-21 19:02 (ссылка)
Да, оверхед остался.

На 100 миллионов вызовов:

P/Invoke -- 6.5 секунд
CreateDelegate() -- 2.2 секунды
MSIL -- 2.0 секунды

Пример могу выслать, если интересно.

(Ответить) (Уровень выше) (Ветвь дискуссии)


[info]aamonster@lj
2008-03-24 04:26 (ссылка)
Интересно. На аамонстер-псина-мыл-ру.
А то вариант с делегатом из первоначального поста мне так и не удалось скомпилить/запустить.

(Ответить) (Уровень выше) (Ветвь дискуссии)


[info]ringill@lj
2008-03-24 07:13 (ссылка)
Выложил, так сказать, в публичный доступ:

http://ringill.googlepages.com/clrtest.zip
Рассчитано на сборку в конфигурации Release.

А в приведённом коде у меня была ошибка: вместо

il_generator.Emit(OpCodes.Ldarg, 1);
il_generator.Emit(OpCodes.Ldarg, 2);

надо

il_generator.Emit(OpCodes.Ldarg, 0);
il_generator.Emit(OpCodes.Ldarg, 1);


Прошу прощения.

(Ответить) (Уровень выше)


(Анонимно)
2012-03-21 22:02 (ссылка)
ссылка добавляется,но при запуске:
System.IO.FileNotFoundException не обработано
Message=Невозможно загрузить файл или сборку "FunctCaller, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" или один из зависимых от них компонентов. Не удается найти указанный файл.
Source=CalliBasic
FileName=FunctCaller, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
FusionLog==== Информация о состоянии предварительной привязки ===
Журнал: User = Вальдек-ПК\Вальдек
Журнал: DisplayName = FunctCaller, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
(Fully-specified)
Журнал: Appbase = file:///G:/Флешка рабочая/Проекты/CalliBasic/CalliBasic/bin/Debug/
Журнал: Initial PrivatePath = NULL
Вызов сборки: CalliBasic, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null.
===
Журнал: данная привязка начинается в контексте загрузки default.
Журнал: файл конфигурации приложения не найден.
Журнал: используется файл конфигурации главного узла:
Журнал: используется файл конфигурации компьютера из C:\Windows\Microsoft.NET\Framework\v4.0.30319\config\machine.config.
Журнал: политика в данный момент не применяется к ссылке (личная, пользовательская, частичная привязка сборки или привязка по местоположению).
Журнал: попытка загрузки нового URL file:///G:/Флешка рабочая/Проекты/CalliBasic/CalliBasic/bin/Debug/FunctCaller.DLL.
Журнал: попытка загрузки нового URL file:///G:/Флешка рабочая/Проекты/CalliBasic/CalliBasic/bin/Debug/FunctCaller/FunctCaller.DLL.
Журнал: попытка загрузки нового URL file:///G:/Флешка рабочая/Проекты/CalliBasic/CalliBasic/bin/Debug/FunctCaller.EXE.
Журнал: попытка загрузки нового URL file:///G:/Флешка рабочая/Проекты/CalliBasic/CalliBasic/bin/Debug/FunctCaller/FunctCaller.EXE.

StackTrace:
в WindowsApplication1.Form1.Form1_MouseDown(Object sender, MouseEventArgs e)
в System.Windows.Forms.Control.OnMouseDown(MouseEventArgs e)
в System.Windows.Forms.Control.WmMouseDown(Message& m, MouseButtons button, Int32 clicks)
в System.Windows.Forms.Control.WndProc(Message& m)
в System.Windows.Forms.ScrollableControl.WndProc(Message& m)
в System.Windows.Forms.Form.WndProc(Message& m)
в System.Windows.Forms.Control.ControlNativeWindow.OnMessage(Message& m)
в System.Windows.Forms.Control.ControlNativeWindow.WndProc(Message& m)
в System.Windows.Forms.NativeWindow.DebuggableCallback(IntPtr hWnd, Int32 msg, IntPtr wparam, IntPtr lparam)
в System.Windows.Forms.UnsafeNativeMethods.DispatchMessageW(MSG& msg)
в System.Windows.Forms.Application.ComponentManager.System.Windows.Forms.UnsafeNativeMethods.IMsoComponentManager.FPushMessageLoop(IntPtr dwComponentID, Int32 reason, Int32 pvLoopData)
в System.Windows.Forms.Application.ThreadContext.RunMessageLoopInner(Int32 reason, ApplicationContext context)
в System.Windows.Forms.Application.ThreadContext.RunMessageLoop(Int32 reason, ApplicationContext context)
в Microsoft.VisualBasic.ApplicationServices.WindowsFormsApplicationBase.OnRun()
в Microsoft.VisualBasic.ApplicationServices.WindowsFormsApplicationBase.DoApplicationModel()
в Microsoft.VisualBasic.ApplicationServices.WindowsFormsApplicationBase.Run(String[] commandLine)
в WindowsApplication1.My.MyApplication.Main(String[] Args) в 17d14f5c-a337-4978-8281-53493378c1071.vb:строка 81
в System.AppDomain._nExecuteAssembly(RuntimeAssembly assembly, String[] args)
в System.AppDomain.ExecuteAssembly(String assemblyFile, Evidence assemblySecurity, String[] args)
в Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly()
в System.Threading.ThreadHelper.ThreadStart_Context(Object state)
в System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean ignoreSyncCtx)
в System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
в System.Threading.ThreadHelper.ThreadStart()
InnerException:

(Ответить) (Ветвь дискуссии)


[info]ringill
2012-03-21 22:20 (ссылка)
В комментариях выше есть ссылка на работающий проект, а также отмечено, что P/Invoke сильно ускорили, и данные, приведённые в посте, неактуальны.

(Ответить) (Уровень выше)