Microsoft MVP성태의 닷넷 이야기
.NET Framework: 517. calli IL 호출이 DllImport 호출보다 빠를까요? [링크 복사], [링크+제목 복사]
조회: 6514
글쓴 사람
홈페이지
첨부 파일

calli IL 호출이 DllImport 호출보다 빠를까요?

네이티브 함수를 닷넷에서 직접 호출할 수 있는 IL 코드가 바로 calli인데요.

OpCodes.Calli Field
; https://msdn.microsoft.com/en-us/library/system.reflection.emit.opcodes.calli(v=vs.110).aspx

예전에 OpenCover 코드 커버리지 도구를 살펴보면서 calli에 대한 동작 방식을 설명한 적도 있습니다. ^^

OpenCover 코드 커버리지 도구의 동작방식을 통해 살펴보는 Calli IL 코드 사용법
; https://www.sysnet.pe.kr/2/0/2882

근데... 이것이 일반적인 delegate 호출보다 성능이 더 좋다고 합니다.

Why is Calli Faster Than a Delegate Call?
; http://stackoverflow.com/questions/5893024/why-is-calli-faster-than-a-delegate-call

덧글에 보면 "The EmitCalli is faster because it is a raw byte code call"라고 하는데, 그럼 혹시 "Managed to Unmanaged"간의 전환이 좀더 가벼울 수도 있지 않을까요?




그래서, 가정을 해봤습니다. .NET에서 native 코드 호출 방법이 대표적으로 DllImport가 있는데요. 그것과 비교해 보면 어떨까요? 예제를 간단히 하기 위해 GetCurrentThreadId Win32 API를 대상으로 다음의 3가지 방법에 대해 각각 성능 테스트를 해봤습니다.

  1. C++ DLL 프로젝트에서 GetCurrentThreadId Win32 API의 주소를 반환해 calli로 호출하는 예제
  2. BCL에서 제공되는 AppDomain.GetCurrentThreadId를 호출하는 예제
  3. DllImport로 직접 kernel32.dll의 GetCurrentThreadId를 호출하는 예제

소스 코드는 대충 다음과 같습니다.

// ============== C++ DLL ==============

WIN32PROJECT1_API __int64 __stdcall fnWin32Project1()
{
    return (__int64)&::GetCurrentThreadId;
}


// ============== C# DLL ==============

using System;
using System.Reflection.Emit;
using System.Runtime.InteropServices;

namespace ClassLibrary1
{
    public class Class1
    {
        [DllImport("Win32Project1.dll", EntryPoint = "fnWin32Project1")]
        static extern long GetThisThreadId32();

        static GetThisThreadIdDelegate _GetThisThreadIdMethod = null;
        static object _lockGetThisThraedIdMethod = new object();
        delegate int GetThisThreadIdDelegate();

        public static int GetThisThreadId()
        {
            long result = 0;

            try
            {
                if (_GetThisThreadIdMethod == null)
                {
                    lock (_lockGetThisThraedIdMethod)
                    {
                        if (_GetThisThreadIdMethod == null)
                        {
                            if (IntPtr.Size == 4)
                            {
                                result = GetThisThreadId32();
                            }

                            var type = typeof(Class1);
                            DynamicMethod dynamicMethod = new DynamicMethod("", typeof(int), Type.EmptyTypes, type, true);

                            var iLGenerator = dynamicMethod.GetILGenerator();

                            if (IntPtr.Size == 4)
                            {
                                iLGenerator.Emit(OpCodes.Ldc_I4, (int)result);
                            }

                            iLGenerator.EmitCalli(OpCodes.Calli, CallingConvention.StdCall, typeof(int), Type.EmptyTypes);
                            iLGenerator.Emit(OpCodes.Ret);

                            GetThisThreadIdDelegate tempDelegate = dynamicMethod.CreateDelegate(typeof(GetThisThreadIdDelegate)) as GetThisThreadIdDelegate;
                            _GetThisThreadIdMethod = tempDelegate;
                        }
                    }
                }

                return _GetThisThreadIdMethod();
            }
            catch
            {
            }

            return 0;
        }
    }
}

// ============== C# EXE ==============

using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Security;

namespace ConsoleApplication1
{

    class Program
    {
        [DllImport("kernel32.dll")]
        static extern int GetCurrentThreadId();

        static void Main(string[] args)
        {
            TestPerf1(1);     // JIT 컴파일 기회를 주기 위해
            Console.WriteLine();
            TestPerf1(10000); // 실제 성능 측정
        }

        private static void TestPerf1(int count)
        {
            Stopwatch st = new Stopwatch();
            st.Start();
            for (int i = 0; i < count; i ++)
            {
                ClassLibrary1.Class1.GetThisThreadId(); // 1. C++ DLL 프로젝트에서 GetCurrentThreadId Win32 API의 주소를 반환해 calli로 호출
            }
            st.Stop();
            if (count != 1) Console.WriteLine("Calli: " + st.ElapsedTicks);

            st = new Stopwatch();
            st.Start();
            for (int i = 0; i < count; i++)
            {
                AppDomain.GetCurrentThreadId(); // 2. BCL에서 제공되는 AppDomain.GetCurrentThreadId를 호출
            }
            st.Stop();
            if (count != 1) Console.WriteLine("BCL: " + st.ElapsedTicks);

            st = new Stopwatch();
            st.Start();
            for (int i = 0; i < count; i++)
            {
                GetCurrentThreadId(); // 3. DllImport로 직접 kernel32.dll의 GetCurrentThreadId를 호출
            }
            st.Stop();
            if (count != 1) Console.WriteLine("DllImport: " + st.ElapsedTicks);
        }
    }
}

결과는 아쉽게도 Calli가 코드가 복잡한 거에 비하면 수확이 마이너스입니다.

// x86 Release 빌드로 테스트 (낮을수록 좋음)
Calli: 456
BCL: 408
DllImport: 394

오히려 DllImport가 가장 빠른 성능을 보여줍니다. 물론, ElapsedTicks 단위로 10,000번 호출한 성능을 잰 것이기 때문에 저 정도의 차이는 실제 업무에서 거의 표시도 안날 수준이니 어떤 것을 선택해도 무방합니다. 단지, calli는 구현 코드가 복잡하기 때문에 가독성 측면에서 선택해야 할 아무런 이점이 없습니다.




혹시나 싶어서, GetCurrentThreadId처럼 단일 호출로 끝나는 것이 아니고 다중으로 네이티브 코드가 호출되는 형식이라면 어떨지... 궁금해졌습니다. ^^ 그러니까, C++에서는 다음과 같이 코딩하지만,

WIN32PROJECT1_API __int64 __stdcall GetThreadCpuTime(int tid)
{
    __int64 result = 0;
    {
        __int64 creationTime, exitTime, kernelTime, userTime;
        HANDLE hThread = OpenThread(THREAD_QUERY_INFORMATION, false, (DWORD)tid);

        if (hThread != NULL)
        {
            if (GetThreadTimes(hThread, (FILETIME *)&creationTime, (FILETIME *)&exitTime, (FILETIME *)&kernelTime, (FILETIME *)&userTime) == TRUE)
            {
                result = (kernelTime + userTime) / 10000;
            }

            CloseHandle(hThread);
        }
    }

    return result;
}

C#에서는 개별마다 DllImport로 정의한 메서드를 호출해야 하는 경우입니다.

[DllImport("kernel32.dll")]
public static extern IntPtr OpenThread(ThreadAccess desiredAccess, bool inheritHandle, uint threadId);

[DllImport("kernel32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool CloseHandle(IntPtr hObject);

[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool GetThreadTimes(IntPtr hThread, out long lpCreationTime,
    out long lpExitTime, out long lpKernelTime, out long lpUserTime);

private static long GetThreadCpuTime3(int tid)
{
    long reult = 0;
    {
        long creationTime, exitTime, kernelTime, userTime;
        IntPtr hThread = OpenThread(ThreadAccess.QUERY_INFORMATION, false, (uint)tid);

        try
        {
            if (hThread != IntPtr.Zero)
            {
                if (GetThreadTimes(hThread, out creationTime, out exitTime, out kernelTime, out userTime) == true)
                {
                    reult = (kernelTime + userTime) / 10000;
                }
            }
        }
        finally
        {
            if (hThread != IntPtr.Zero)
            {
                CloseHandle(hThread);
            }
        }
    }

    return reult;
}

테스트 해보니, 역시나 DllImport와 비교해 거의 성능 차이가 나지 않습니다.

// x86 Release 빌드로 테스트 (낮을수록 좋음)
Calli: 46660
3번의 DllImport: 48590

(첨부한 파일은 위의 코드 테스트를 포함합니다.)



결론은, delegate 호출의 성능을 개선할 때만 calli 호출을 쓰시고 그 외에는 그냥 DllImport를 쓰시는 것이 좋습니다. 또는, 특수한 사례로 네이티브 코드를 제공할 때 symbol export가 안되도록 숨기고 싶은 경우에 한해 calli를 쓰는 것도 좋겠고. ^^




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]





[최초 등록일: ]
[최종 수정일: 6/18/2015 ]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer@outlook.com

비밀번호

댓글 쓴 사람
 



2020-02-27 10시25분
{
    // public delegate void VoidIntPtrDelegate(IntPtr objAddress);

    var type = typeof(Program);
    DynamicMethod dynamicMethod = new DynamicMethod("", typeof(void), new Type[] { typeof(IntPtr) }, type, true);

    var iLGenerator = dynamicMethod.GetILGenerator();

    iLGenerator.Emit(OpCodes.Ldarg_0);

    if (IntPtr.Size == 4)
    {
        iLGenerator.Emit(OpCodes.Ldc_I4, ...32비트 코드 주소...);
    }
    else
    {
        iLGenerator.Emit(OpCodes.Ldc_I8, ...645비트 코드 주소...);
    }

    iLGenerator.EmitCalli(OpCodes.Calli, CallingConvention.StdCall, typeof(void), new Type[] { typeof(IntPtr) });
    iLGenerator.Emit(OpCodes.Ret);

    VoidIntPtrDelegate tempDelegate = dynamicMethod.CreateDelegate(typeof(VoidIntPtrDelegate)) as VoidIntPtrDelegate;
}
정성태

[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
12203정성태3/18/202064오류 유형: 612. warning: 'C:\ProgramData/Git/config' has a dubious owner: '...'.
12202정성태3/18/202087개발 환경 구성: 486. .NET Framework 프로젝트를 위한 GitLab CI/CD Runner 구성
12201정성태3/18/202032오류 유형: 611. git-credential-manager.exe: Using credentials for username "Personal Access Token".
12200정성태3/18/2020122VS.NET IDE: 145. NuGet + Github 라이브러리 디버깅 관련 옵션 3가지 - "Enable Just My Code" / "Enable Source Link support" / "Suppress JIT optimization on module load (Managed only)"
12199정성태3/17/202035오류 유형: 610. C# - CodeDomProvider 사용 시 Unhandled Exception: System.IO.DirectoryNotFoundException: Could not find a part of the path '...\f2_6uod0.tmp'.
12198정성태3/17/202031오류 유형: 609. SQL 서버 접속 시 "Cannot open user default database. Login failed."
12197정성태3/17/2020120VS.NET IDE: 144. .NET Core 콘솔 응용 프로그램을 배포(publish) 시 docker image 자동 생성 - 두 번째 이야기
12196정성태3/17/202057오류 유형: 608. The ServicedComponent being invoked is not correctly configured (Use regsvcs to re-register).
12195정성태3/17/2020169.NET Framework: 902. C# - 프로세스의 모든 핸들을 열람 - 세 번째 이야기
12194정성태3/16/202040오류 유형: 607. PostgreSQL - Npgsql.NpgsqlException: sorry, too many clients already
12193정성태3/16/202073개발 환경 구성: 485. docker - SAP Adaptive Server Enterprise 컨테이너 실행
12192정성태3/14/202086개발 환경 구성: 484. docker - Sybase Anywhere 16 컨테이너 실행
12191정성태3/14/2020125개발 환경 구성: 483. docker - OracleXE 컨테이너 실행
12190정성태3/14/202044오류 유형: 606. Docker Desktop 업그레이드 시 "The process cannot access the file 'C:\Program Files\Docker\Docker\resources\dockerd.exe' because it is being used by another process."
12189정성태3/13/202085개발 환경 구성: 482. Facebook OAuth 처리 시 상태 정보 전달 방법과 "유효한 OAuth 리디렉션 URI" 설정 규칙
12188정성태3/14/202047Windows: 167. 부팅 시점에 실행되는 chkdsk 결과를 확인하는 방법
12187정성태3/12/202034오류 유형: 605. NtpClient was unable to set a manual peer to use as a time source because of duplicate error on '...'.
12186정성태3/12/202053오류 유형: 604. The SysVol Permissions for one or more GPOs on this domain controller and not in sync with the permissions for the GPOs on the Baseline domain controller.
12185정성태3/11/202052오류 유형: 603. The browser service was unable to retrieve a list of servers from the browser master...
12184정성태3/11/202054오류 유형: 602. Automatic certificate enrollment for local system failed (0x800706ba) The RPC server is unavailable.
12183정성태3/12/202052오류 유형: 601. Warning: DsGetDcName returned information for \\[...], when we were trying to reach [...].
12182정성태3/11/2020117.NET Framework: 901. C# Windows Forms - Vista/7 이후의 Progress Bar 업데이트가 느린 문제파일 다운로드1
12181정성태3/11/2020144기타: 76. 재현 가능한 최소한의 예제 프로젝트란? - 두 번째 예제파일 다운로드1
12180정성태3/10/202046오류 유형: 600. "Docker Desktop for Windows" - EXPOSE 포트가 LISTENING 되지 않는 문제
12179정성태3/10/202069개발 환경 구성: 481. docker - PostgreSQL 컨테이너 실행
[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...