IL 시작 투어 (1) – 객체 만들기

19441 단어 대상
1. 객체를 작성하는 방법
입문의 첫 편으로 가장 간단한 창설 대상부터 시작하자.
먼저 c#에서 어떤 유형의 대상을 어떻게 만드는지 돌이켜 보면 가장 기초적인 것은 바로 new 키워드를 사용하는 것이다. 예를 들어 new object()이다. 물론 이 방식도 단점이 있다. 바로 번역할 때 유형을 명확하게 알아야 한다는 것이다.
만약 컴파일할 때 유형을 모르지만 실행할 때 구체적인 유형을 알면 어떻게 도와줍니까?
첫 번째 사진은 당연히 Activator를 사용합니다.CreateInstance 메서드(예:
static void Main(string[] args)

{

    Type type = typeof(object);

    Console.WriteLine(Create(type));

}



static object Create(Type type)

{

    return Activator.CreateInstance(type);

}


또는 범용 재부팅을 사용합니다.
static void Main(string[] args)

{

    Console.WriteLine(Create<object>());

}



static T Create<T>()

{

    return Activator.CreateInstance<T>();

}


물론, 이곳의 type이나 T는 기본 구조 함수가 있어야 합니다. 그렇지 않으면 실행 중 오류가 발생할 수 있습니다.
두 번째 방법은 다음과 같은 일반 구속 new()를 사용합니다.
static void Main(string[] args)

{

    Console.WriteLine(Create<object>());

}



static T Create<T>() where T : new()

{

    return new T();

}


T가 기본 구조 함수를 갖추지 못하면 컴파일할 때 오류가 발생합니다.
물론 세 번째 방법인 반사도 있다. 예를 들어 다음과 같다.
static void Main(string[] args)

{

    Console.WriteLine(Create(typeof(object)));

}



static object Create(Type type)

{

    var ctor = type.GetConstructor(Type.EmptyTypes);

    return ctor.Invoke(new object[0]);

}


2. 성능 비교
물론 이 세 가지 방식의 성능은 각기 다르다. 세 번째 반사 방식은 최악의 성능을 가진다. 일반적인 new 제약이 T가 인용 유형일 때 컴파일러는 new T()를Activator로 자동으로 변환한다.CreateInstance의 호출은 T가 값 유형인 경우 효율적이고 높습니다.Activator.CreateInstance의 두 가지 재부팅의 효율은 호출 횟수와 비범용판의 캐시 명중률에 달려 있다. 호출 횟수가 충분할 때 대량으로 명중된 상황에서 비범용판은 상대적으로 높은 효율을 가진다. 즉, 비범용판은 캐시가 명중되지 않은 상황에서 비교적 높은 대가를 필요로 하지만 캐시가 명중된 상황에서 상대적으로 적은 대가를 받을 수 있다. 테스트 코드는 다음과 같다.
Type t = typeof(object);

var sw = new Stopwatch();

int count = 1; // 10, 100, 1000

sw.Start();

for (int i = 0; i < count; i++)

    Activator.CreateInstance(t);

Console.WriteLine("Method:CreateInstance, Count:{0}, Tick:{1}",

    count.ToString(), sw.ElapsedTicks.ToString());

sw.Reset();

sw.Start();

for (int i = 0; i < count; i++)

    Activator.CreateInstance<object>();

Console.WriteLine("Method:CreateInstance<T>, Count:{0}, Tick:{1}",

    count.ToString(), sw.ElapsedTicks.ToString());


범용 버전이 아닌 캐시를 피하기 위해 매번 다시 실행해야 합니다. 실행 결과는 다음과 같습니다.
Method:CreateInstance, Count:1, Tick:1399Method:CreateInstance, Count:1, Tick:42
Method:CreateInstance, Count:10, Tick:3009Method:CreateInstance, Count:10, Tick:112
Method:CreateInstance, Count:100, Tick:3343Method:CreateInstance, Count:100, Tick:820
Method:CreateInstance, Count:1000, Tick:4092Method:CreateInstance, Count:1000, Tick:7989
범형판의 대가가 상당히 안정적이라는 것을 쉽게 알 수 있다. 첫 번째를 제외하고 매번 호출되는 대가가 약 8-10개의 Tick이고 범형판이 아닌 대가의 변화가 크다. 100번 호출된 대가 중 10번 호출된 대가가 90%(사실상 전 2회)를 차지했지만 이후의 대가가 현저히 떨어졌지만 직접 new object()에 비해 대가가 비교적 크다.
3. IL 소개
IL 로 돌아가 Emit 을 사용하면 성능을 더 향상시킬 수 있습니까?그럼 우선 Emit으로 대상을 만드는 방법을 써야 하는데 어떻게 쓰지 않을까요?
좋아, 이제 도구가 등장할 때야. 우선 c#컴파일러야. 예를 들어 다음과 같은 방법을 써.
static object MyCreateInstance()

{

    return new object();

}


그리고 컴파일링(본문의 모든 컴파일링은Release 방식을 사용)을 하면 dll 또는exe 파일을 얻을 수 있다. 그리고reflector가 등장할 차례이다. 이 파일을 열고 클래스와 방법을 찾아 역컴파일된 언어를 IL로 선택하면 다음과 같은 내용을 얻을 수 있다.
.method private hidebysig static object MyCreateInstance() cil managed

{

    .maxstack 8

    L_0000: newobj instance void [mscorlib]System.Object::.ctor()

    L_0005: ret 

}


아니면 손댈 수가 없는 것 같아?알겠습니다. 여기. 에서 리플렉터 플러그인을 다운로드할 수 있습니다. 예를 들어 ReflectionEmitLanguage 플러그인을 불러오면 리플렉터가 여러 언어를 선택할 수 있습니다. 리플렉션.Emit, 언어를 선택하면 다음을 볼 수 있습니다.
public MethodBuilder BuildMethodMyCreateInstance(TypeBuilder type)

{

    // Declaring method builder

    // Method attributes

    System.Reflection.MethodAttributes methodAttributes =

          System.Reflection.MethodAttributes.Private

        | System.Reflection.MethodAttributes.HideBySig

        | System.Reflection.MethodAttributes.Static;

    MethodBuilder method = type.DefineMethod("MyCreateInstance", methodAttributes);

    // Preparing Reflection instances

    ConstructorInfo ctor1 = typeof(Object).GetConstructor(

        BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,

        null,

        new Type[]{

    },

        null

        );

    // Setting return type

    method.SetReturnType(typeof(Object));

    // Adding parameters

    ILGenerator gen = method.GetILGenerator();

    // Writing body

    gen.Emit(OpCodes.Newobj, ctor1);

    gen.Emit(OpCodes.Ret);

    // finished

    return method;

}

하지만 이것은 유형을 생성하는 방법입니다. 지금 필요한 것은 단지 하나의 방법을 생성하는 것입니다. 조금만 개조해 봅시다.
static Func<object> BuildMethodMyCreateInstance()

{

    DynamicMethod dm = new DynamicMethod(string.Empty, typeof(object), Type.EmptyTypes);

    var gen = dm.GetILGenerator();

    gen.Emit(OpCodes.Newobj, typeof(object).GetConstructor(Type.EmptyTypes));

    gen.Emit(OpCodes.Ret);

    return (Func<object>)dm.CreateDelegate(typeof(Func<object>));

}


이렇게 해서 아까 MyCreateInstance 방법의 내용을 Func로 포장하여 정상적으로 작동할 수 있는지 확인합니다.
Func<object> func = BuildMethodMyCreateInstance();

Console.WriteLine(func());


실행 결과를 보려면 다음과 같이 하십시오.
System.Object
작업은 괜찮았지만 문제가 생겼다. 우리가 원하는 것은 Object만 만들 수 있는 간단한 방법이 아니라 전송할 수 있는 유형과 이 유형의 대상을 만들 수 있는 방법이 필요하다. 그래서 더욱 개선해야 한다.
static Func<object> BuildMethodMyCreateInstance(Type type)

{

    DynamicMethod dm = new DynamicMethod(string.Empty, typeof(object), Type.EmptyTypes);

    var gen = dm.GetILGenerator();

    gen.Emit(OpCodes.Newobj, type.GetConstructor(Type.EmptyTypes));

    gen.Emit(OpCodes.Ret);

    return (Func<object>)dm.CreateDelegate(typeof(Func<object>));

}


매개 변수를 추가하여 조금 고쳤으니 테스트를 봅시다.
Func<object> func = BuildMethodMyCreateInstance(typeof(object));

Console.WriteLine(func());


출력:
System.Object
좋아, 이제 우리 한층 더 테스트해 보자.
4. 성능 비교 (2)
테스트 방법을 수정하여 Emit과 직접 초기 바인딩을 포함하는 성능 테스트를 수행합니다.
Type t = typeof(object);

var sw = new Stopwatch();

int count = 1; // 10, 100, 1000

sw.Reset();

sw.Start();

for (int i = 0; i < count; i++)

    Activator.CreateInstance(t);

Console.WriteLine("Method:CreateInstance, Count:{0}, Tick:{1}",

    count.ToString(), sw.ElapsedTicks.ToString());

sw.Reset();

sw.Start();

for (int i = 0; i < count; i++)

    Activator.CreateInstance<object>();

Console.WriteLine("Method:CreateInstance<T>, Count:{0}, Tick:{1}",

    count.ToString(), sw.ElapsedTicks.ToString());

sw.Reset();

sw.Start();

Func<object> func = BuildMethodMyCreateInstance(t);

for (int i = 0; i < count; i++)

    func();

Console.WriteLine("Method:MyCreateInstance, Count:{0}, Tick:{1}",

    count.ToString(), sw.ElapsedTicks.ToString());

sw.Reset();

sw.Start();

for (int i = 0; i < count; i++)

    new object();

Console.WriteLine("Method:new object(), Count:{0}, Tick:{1}",

    count.ToString(), sw.ElapsedTicks.ToString());


테스트 결과를 살펴보겠습니다.
Method:CreateInstance, Count:1, Tick:1449Method:CreateInstance, Count:1, Tick:56Method:MyCreateInstance, Count:1, Tick:11288Method:new object(), Count:1, Tick:10
와, 단 한 번의 성능으로 말하자면CreateInstance를 순순히 사용하십시오. (또는 일반적인 new () 제약을 사용하십시오.)
Method:CreateInstance, Count:10, Tick:3046Method:CreateInstance, Count:10, Tick:113Method:MyCreateInstance, Count:10, Tick:10802Method:new object(), Count:10, Tick:32
10번의 Emit 결과는 여전히 차이가 많지 않다(Emit의 시간이 줄어드는 것은 오차에 속한다)
Method:CreateInstance, Count:100, Tick:3211Method:CreateInstance, Count:100, Tick:811Method:MyCreateInstance, Count:100, Tick:9442Method:new object(), Count:100, Tick:13
100번의 Emit 결과는 여전히 안정적이어서 거의 증가하지 않았다
Method:CreateInstance, Count:1000, Tick:3964Method:CreateInstance, Count:1000, Tick:8031Method:MyCreateInstance, Count:1000, Tick:10982Method:new object(), Count:1000, Tick:90
1000번의 Emit 결과는 여전히 안정적이다(오차가 소모된 것보다 더 많다).하지만Create Instance보다 못하다.그러면 연장전을 한 번 하자.
Method:CreateInstance, Count:10000, Tick:13396Method:CreateInstance, Count:10000, Tick:81018Method:MyCreateInstance, Count:10000, Tick:10706Method:new object(), Count:10000, Tick:564
10000번의 실행이 드디어CreateInstance를 넘어섰습니다.
Method:CreateInstance, Count:100000, Tick:102373Method:CreateInstance, Count:100000, Tick:768419Method:MyCreateInstance, Count:100000, Tick:15689Method:new object(), Count:100000, Tick:4778
100000회 실행된 상황에서 Emit의 장점은 완전히 드러났다. 성능은CreateInstance의 6배가 넘고CreateInstance의 50배에 가깝다.또한 첫 번째 Emit의 대가를 빼면 new object()에 가까운 대가를 받게 됩니다.(15689-약 10000의 첫 대가, 받은 이후 약 5000여 Tick, 직접 new object() 5000번의 Tick과 매우 가깝다)
따라서 대상을 대량으로 만들어야 한다면 가능한 한 Emit 또는 조기 귀속 방식을 사용해야 한다.몇 번만 만들면CreateInstance를 선택하거나 조기 귀속을 선택해야 합니다.
5.승급
진짜 좋은 줄 알았어요?앞서 언급한 IL에 대해 특별한 처리 값 유형이 필요하다는 것을 기억하십니까?이 테스트 용례를 봅시다.
처리되지 않은 예외: System.ArgumentNullException: 값은 비워 둘 수 없습니다.매개변수 이름: con은 System에 있습니다.Reflection.Emit.DynamicILGenerator.Emit(OpCode opcode, ConstructorInfo con)
Constructor Info가 실례를 가져오지 않았기 때문에 오류가 발생했습니다. 왜?값 유형에는 일반적으로 기본 구조 함수가 없기 때문이다. (거의 모든 고급 언어는 값 유형의 기본 구조 함수를 쓸 수 없지만 IL은 막지 않는다.)
그럼 평소에:
static void X()

{

    int x = new int();

    Console.WriteLine(x);

}


어떻게 된 거예요?Reflector를 계속 사용해 보십시오.
.method private hidebysig static void X() cil managed

{

    .maxstack 1

    .locals init (

        [0] int32 x)

    L_0000: ldc.i4.0 

    L_0001: stloc.0 

    L_0002: ldloc.0 

    L_0003: call void [mscorlib]System.Console::WriteLine(int32)

    L_0008: ret 

}


c#컴파일러가 직접
int x = new int();

번역하다
int x = 0;


그럼 다른 값으로 바꾸면 어때요?예를 들면 다음과 같습니다.
static void X()

{

    int? x = null;

    Console.WriteLine(x);

}


c#컴파일러는 다음과 같이 번역됩니다.
.method private hidebysig static void X() cil managed

{

    .maxstack 1

    .locals init (

        [0] valuetype [mscorlib]System.Nullable`1<int32> x)

    L_0000: ldloca.s x

    L_0002: initobj [mscorlib]System.Nullable`1<int32>

    L_0008: ldloc.0 

    L_0009: box [mscorlib]System.Nullable`1<int32>

    L_000e: call void [mscorlib]System.Console::WriteLine(object)

    L_0013: ret 

}

인트 처리하는 거 보이시나요?x = null;c# 컴파일러는 다음을 사용합니다.
ldloca.s x

initobj [mscorlib]System.Nullable`1<int32>


이 두 마디,ldloca.s는ldloca의 짧은 형식입니다.ldloca는 Load Local variable Address라는 뜻으로 x변수를 불러오는 주소입니다.그 다음에 initobj라는 조작부호를 사용했다. 이것이 바로.net의 고급 언어는 모두 값 형식을 정의할 필요가 없는 이유입니다. initobj는 CLR에서 하나의 값 형식을 초기화하는 조작부호입니다.
따라서 이전의 방법을 한층 더 개선하고 값 유형을 지원할 수 있다. 물론 원래의 방법을 이용하여 상응하는 도움을 받아 아래의 개량판을 얻을 수 있다.
static Func<object> BuildMethodMyCreateInstance(Type type)

{

    DynamicMethod dm = new DynamicMethod(string.Empty, typeof(object), Type.EmptyTypes);

    var gen = dm.GetILGenerator();

    if (type.IsValueType)

    {

        gen.DeclareLocal(type);

        gen.Emit(OpCodes.Ldloca_S, 0);

        gen.Emit(OpCodes.Initobj, type);

        gen.Emit(OpCodes.Ldloc_0);

        gen.Emit(OpCodes.Box, type);

    }

    else

    {

        gen.Emit(OpCodes.Newobj, type.GetConstructor(Type.EmptyTypes));

    }

    gen.Emit(OpCodes.Ret);

    return (Func<object>)dm.CreateDelegate(typeof(Func<object>));

}


현재 이 방법은 값 형식과 인용 형식의 상황을 동시에 처리할 수 있다.

좋은 웹페이지 즐겨찾기