제네릭 메서드는 어디에 저장됩니까?
.ΝΕΤ에서 제네릭에 대한 몇 가지 정보를 읽었고 한 가지 흥미로운 점을 발견했습니다.
예를 들어, 제네릭 클래스가있는 경우 :
class Foo<T>
{
public static int Counter;
}
Console.WriteLine(++Foo<int>.Counter); //1
Console.WriteLine(++Foo<string>.Counter); //1
두 클래스 Foo<int>
와 Foo<string>
런타임에 다릅니다. 그러나 제네릭이 아닌 클래스가 제네릭 메서드를 갖는 경우는 어떻습니까?
class Foo
{
public void Bar<T>()
{
}
}
Foo
클래스가 하나 뿐인 것은 분명합니다 . 그러나 방법은 Bar
어떻습니까? 모든 제네릭 클래스 및 메서드는 사용 된 매개 변수를 사용하여 런타임에 닫힙니다. 클래스 에이 메서드에 대한 정보가 메모리에 저장되는 위치와 Foo
구현이 많이 있다는 의미 Bar
입니까?
C ++ 템플릿과 달리 .NET 제네릭은 컴파일 타임이 아닌 런타임에 평가됩니다. 의미 적으로 다른 유형 매개 변수로 제네릭 클래스를 인스턴스화하면 두 개의 다른 클래스 인 것처럼 동작하지만 내부적으로 컴파일 된 IL (중간 언어) 코드에는 클래스가 하나뿐입니다.
일반 유형
동일한 제네릭 유형의 다른 인스턴스 간의 차이점은 Reflection : 을 사용할 때 분명해 typeof(YourClass<int>)
집니다 typeof(YourClass<string>)
. 이를 생성 된 제네릭 유형 이라고 합니다 . 또한이 존재 typeof(YourClass<>)
나타내는 제네릭 형식 정의를 . 다음은 Reflection을 통해 제네릭을 처리하는 데 대한 몇 가지 추가 팁 입니다.
생성 된 제네릭 클래스 를 인스턴스화 할 때 런타임은 즉시 특수 클래스를 생성합니다. 값 유형과 참조 유형에서 작동하는 방식에는 미묘한 차이가 있습니다.
- 컴파일러는 어셈블리에 단일 제네릭 형식 만 생성합니다.
- 런타임은 함께 사용하는 각 값 유형에 대해 별도의 일반 클래스 버전을 만듭니다.
- 런타임은 제네릭 클래스의 각 유형 매개 변수에 대해 별도의 정적 필드 세트를 할당합니다.
- 참조 유형의 크기가 동일하기 때문에 런타임은 참조 유형과 함께 처음 사용할 때 생성 한 특수 버전을 재사용 할 수 있습니다.
일반적인 방법
의 경우 일반적인 방법 , 원칙은 동일합니다.
- 컴파일러는 일반 메소드 정의 인 하나의 일반 메소드 만 생성 합니다 .
- 런타임에서 메서드의 각기 다른 전문화는 동일한 클래스의 다른 메서드로 취급됩니다.
먼저 두 가지를 명확히하겠습니다. 다음은 일반적인 메서드 정의입니다.
T M<T>(T x)
{
return x;
}
이것은 일반 유형 정의입니다.
class C<T>
{
}
무엇인지 물어 보면 M
a를 취하고 T
를 반환하는 제네릭 메서드라고 말할 수 있습니다 T
. 그것은 절대적으로 맞습니다. 그러나 저는 그것에 대해 다른 생각을 제안합니다. 여기에 두 세트의 매개 변수가 있습니다. 하나는 유형 T
이고 다른 하나는 객체 x
입니다. 이들을 결합하면이 방법이 총 두 개의 매개 변수를 취한다는 것을 알 수 있습니다.
커링의 개념 은 두 개의 매개 변수를 사용하는 함수가 하나의 매개 변수를 사용하고 다른 매개 변수를 사용하는 다른 함수를 반환하는 함수로 변환 될 수 있음을 알려줍니다 (반대의 경우도 마찬가지). 예를 들어, 다음은 두 개의 정수를 받아 그 합계를 생성하는 함수입니다.
Func<int, int, int> uncurry = (x, y) => x + y;
int sum = uncurry(1, 3);
그리고 여기에 동등한 형태가 있습니다. 여기서 우리는 하나의 정수를 취하고 또 다른 정수를 취하고 앞서 언급 한 정수들의 합을 반환하는 함수를 생성하는 함수를 가지고 있습니다 :
Func<int, Func<int, int>> curry = x => y => x + y;
int sum = curry(1)(3);
우리는 두 개의 정수를 취하는 하나의 함수에서 정수를 받아서 함수를 생성 하는 함수를 가지게되었습니다 . 분명히이 두 가지는 C #에서 말 그대로 동일한 것은 아니지만 동일한 정보를 전달하면 결국 동일한 최종 결과를 얻을 수 있기 때문에 동일한 것을 말하는 두 가지 다른 방법입니다.
커링을 사용하면 함수에 대해 더 쉽게 추론 할 수 있으며 (두 개보다 하나의 매개 변수에 대해 추론하는 것이 더 쉽습니다) 결론이 여전히 여러 매개 변수와 관련이 있음을 알 수 있습니다.
추상적 인 수준에서 이것이 여기서 일어나는 일임을 잠시 생각해보십시오. M
유형을 취하고 T
일반 메서드를 반환하는 "수퍼 함수" 라고 가정 해 보겠습니다 . 반환 된 메서드는 T
값을 가져와 값을 반환 T
합니다.
예를 들어, M
인수를 사용하여 수퍼 함수 를 호출하면 to int
에서 일반 메소드를 얻 int
습니다 int
.
Func<int, int> e = M<int>;
우리가 인수하는 일반 메소드를 호출한다면 5
, 우리는 도착 5
우리가 예상대로 다시 :
int v = e(5);
따라서 다음 식을 고려하십시오.
int v = M<int>(5);
왜 이것이 두 개의 개별 호출로 간주 될 수 있는지 아십니까? 인수가에서 전달되기 때문에 수퍼 함수에 대한 호출을 인식 할 수 있습니다 <>
. 그런 다음 반환 된 메서드에 대한 호출이 이어지고 인수가 ()
. 이전 예와 유사합니다.
curry(1)(3);
마찬가지로 제네릭 유형 정의는 유형을 취하고 다른 유형을 반환하는 수퍼 함수이기도합니다. 예를 들어, 정수 목록 인 유형을 반환 하는 인수가 List<int>
있는 수퍼 함수 List
에 대한 호출 int
입니다.
이제 C # 컴파일러가 일반 메서드를 만나면이를 일반 메서드로 컴파일합니다. 가능한 다른 인수에 대해 다른 정의를 만들려고 시도하지 않습니다. 그래서 이건:
int Square(int x) => x * x;
있는 그대로 컴파일됩니다. 다음과 같이 컴파일되지 않습니다.
int Square__0() => 0;
int Square__1() => 1;
int Square__2() => 4;
// and so on
즉, C # 컴파일러는이 메서드에 대해 가능한 모든 인수를 평가하여 최종 실행 파일에 포함하지 않고 메서드를 매개 변수화 된 형식으로 남겨두고 결과가 런타임에 평가 될 것이라고 신뢰합니다.
마찬가지로 C # 컴파일러가 슈퍼 함수 (일반 메서드 또는 형식 정의)를 충족하면이를 슈퍼 함수로 컴파일합니다. 가능한 다른 인수에 대해 다른 정의를 만들려고 시도하지 않습니다. 그래서 이건:
T M<T>(T x) => x;
있는 그대로 컴파일됩니다. 다음과 같이 컴파일되지 않습니다.
int M(int x) => x;
int[] M(int[] x) => x;
int[][] M(int[][] x) => x;
// and so on
float M(float x) => x;
float[] M(float[] x) => x;
float[][] M(float[][] x) => x;
// and so on
다시 말하지만, C # 컴파일러는이 수퍼 함수가 호출 될 때 런타임에 평가되고 해당 평가에 의해 일반 메서드 또는 유형이 생성 될 것이라고 신뢰합니다.
이것이 C #이 런타임의 일부로 JIT 컴파일러를 사용하여 이점을 얻는 이유 중 하나입니다. 수퍼 함수가 평가되면 컴파일 타임에 없었던 새로운 메서드 또는 유형이 생성됩니다! 우리는 프로세스를 호출 구체화을 . 결과적으로 런타임은 그 결과를 기억하므로 다시 만들 필요가 없습니다. 그 부분을 메모 라고 합니다.
런타임의 일부로 JIT 컴파일러가 필요하지 않은 C ++와 비교하십시오. C ++ 컴파일러는 실제로 컴파일 타임에 수퍼 함수 ( "템플릿"이라고 함)를 평가해야합니다. 슈퍼 함수의 인수는 컴파일 타임에 평가할 수 있는 항목으로 제한되기 때문에 가능한 옵션 입니다.
따라서 귀하의 질문에 답하려면 :
class Foo
{
public void Bar()
{
}
}
Foo
일반 유형이며 그중 하나만 있습니다. Bar
내부에 일반적인 방법이 Foo
있으며 그중 하나만 있습니다.
class Foo<T>
{
public void Bar()
{
}
}
Foo<T>
런타임에 유형을 생성하는 슈퍼 함수입니다. 이러한 결과 유형 각각에는 이름이 지정된 고유 한 일반 메서드가 있으며 Bar
각 유형에 대해 하나만 있습니다.
class Foo
{
public void Bar<T>()
{
}
}
Foo
일반 유형이고 그 중 하나만 있습니다. Bar<T>
런타임에 일반 메서드를 생성하는 슈퍼 함수입니다. 그런 다음 각각의 결과 메서드는 일반 유형의 일부로 간주됩니다 Foo
.
class Foo<Τ1>
{
public void Bar<T2>()
{
}
}
Foo<T1>
is a super-function that creates types at runtime. Each one of those resulting types has its own a super-function named Bar<T2>
that creates regular methods at runtime (at a later time). Each one of those resulting methods is considered part of the type that created the corresponding super-function.
The above is the conceptual explanation. Beyond it, certain optimizations can be implemented to reduce the number of distinct implementations in memory -- e.g. two constructed methods can share a single machine-code implementation under certain circumstances. See Luaan's answer about why the CLR can do this and when it actually does it.
In IL itself, there's just one "copy" of the code, just like in C#. Generics are fully supported by IL, and the C# compiler doesn't need to do any tricks. You will find that each reification of a generic type (e.g. List<int>
) has a separate type, but they still keep a reference to the original open generic type (e.g. List<>
); however, at the same time, as per contract, they must behave as if there were separate methods or types for each closed generic. So the simplest solution is indeed to have each closed generic method be a separate method.
Now for the implementation details :) In practice, this is rarely necessary, and can be expensive. So what actually happens is that if a single method can handle multiple type arguments, it will. This means that all reference types can use the same method (the type safety is already determined at compile-time, so there's no need to have it again in runtime), and with a little trickery with static fields, you can use the same "type" as well. For example:
class Foo<T>
{
private static int Counter;
public static int DoCount() => Counter++;
public static bool IsOk() => true;
}
Foo<string>.DoCount(); // 0
Foo<string>.DoCount(); // 1
Foo<object>.DoCount(); // 0
There's only one assembly "method" for IsOk
, and it can be used by both Foo<string>
and Foo<object>
(which of course also means that calls to that method can be the same). But their static fields are still separate, as required by the CLI specification, which also means that DoCount
must refer to two separate fields for Foo<string>
and Foo<object>
. And yet, when I do the disassembly (on my computer, mind you - these are implementation details and may vary quite a bit; also, it takes a bit of effort to prevent the inlining of DoCount
), there's only one DoCount
method. How? The "reference" to Counter
is indirect:
000007FE940D048E mov rcx, 7FE93FC5C18h ; Foo<string>
000007FE940D0498 call 000007FE940D00C8 ; Foo<>.DoCount()
000007FE940D049D mov rcx, 7FE93FC5C18h ; Foo<string>
000007FE940D04A7 call 000007FE940D00C8 ; Foo<>.DoCount()
000007FE940D04AC mov rcx, 7FE93FC5D28h ; Foo<object>
000007FE940D04B6 call 000007FE940D00C8 ; Foo<>.DoCount()
And the DoCount
method looks something like this (excluding the prolog and "I don't want to inline this method" filler):
000007FE940D0514 mov rcx,rsi ; RCX was stored in RSI in the prolog
000007FE940D0517 call 000007FEF3BC9050 ; Load Foo<actual> address
000007FE940D051C mov edx,dword ptr [rax+8] ; EDX = Foo<actual>.Counter
000007FE940D051F lea ecx,[rdx+1] ; ECX = RDX + 1
000007FE940D0522 mov dword ptr [rax+8],ecx ; Foo<actual>.Counter = ECX
000007FE940D0525 mov eax,edx
000007FE940D0527 add rsp,30h
000007FE940D052B pop rsi
000007FE940D052C ret
So the code basically "injected" the Foo<string>
/Foo<object>
dependency, so while the calls are different, the method being called is actually the same - just with a bit more indirection. Of course, for our original method (() => Counter++
), this will not be a call at all, and will not have the extra indirection - it will just inline in the callsite.
It's a bit trickier for value types. Fields of reference types are always the same size - the size of the reference. On the other hand, fields of value types may have different sizes e.g. int
vs. long
or decimal
. Indexing an array of integers requires different assembly than indexing an array of decimal
s. And since structs can be generic too, the size of the struct may depend on the size of the type arguments:
struct Container<T>
{
public T Value;
}
default(Container<double>); // Can be as small as 8 bytes
default(Container<decimal>); // Can never be smaller than 16 bytes
If we add value types to our earlier example
Foo<int>.DoCount();
Foo<double>.DoCount();
Foo<int>.DoCount();
We get this code:
000007FE940D04BB call 000007FE940D00F0 ; Foo<int>.DoCount()
000007FE940D04C0 call 000007FE940D0118 ; Foo<double>.DoCount()
000007FE940D04C5 call 000007FE940D00F0 ; Foo<int>.DoCount()
As you can see, while we don't get the extra indirection for the static fields unlike with the reference types, each method is actually entirely separate. The code in the method is shorter (and faster), but cannot be reused (this is for Foo<int>.DoCount()
:
000007FE940D058B mov eax,dword ptr [000007FE93FC60D0h] ; Foo<int>.Counter
000007FE940D0594 lea edx,[rax+1]
000007FE940D0597 mov dword ptr [7FE93FC60D0h],edx
Just a plain static field access as if the type wasn't generic at all - as if we just defined class FooOfInt
and class FooOfDouble
.
Most of the time, this isn't really important for you. Well-designed generics usually more than pay for their costs, and you can't just make a flat statement about the performance of generics. Using a List<int>
will almost always be a better idea than using ArrayList
of ints - you pay the extra memory cost of having multiple List<>
methods, but unless you have many different value-type List<>
s with no items, the savings will likely well outweigh the cost in both memory and time. If you only have one reification of a given generic type (or all the reifications are closed on reference types), you usually aren't going to pay extra - there may be a bit of extra indirection if inlining isn't possible.
There's a few guidelines to using generics efficiently. The most relevant here is to only keep the actually generic parts generic. As soon as the containing type is generic, everything inside may also be generic - so if you have 100 kiB of static fields in a generic type, every reification will need to duplicate that. This may be what you want, but it might be a mistake. The usual aproach is to put the non-generic parts in a non-generic static class. The same applies to nested classes - class Foo<T> { class Bar { } }
means that Bar
is also a generic class (it "inherits" the type argument of its containing class).
On my computer, even if I keep the DoCount
method free of anything generic (replace Counter++
with just 42
), the code is still the same - the compilers don't try to eliminate unnecessary "genericity". If you need to use a lot of different reifications of one generic type, this can add up quickly - so do consider keeping those methods apart; putting them in a non-generic base class or a static extension method might be worthwhile. But as always with performance - profile. It probably isn't an issue.
ReferenceURL : https://stackoverflow.com/questions/41461595/where-are-generic-methods-stored
'IT Share you' 카테고리의 다른 글
Haskell의 DataKinds 확장은 무엇입니까? (0) | 2021.01.07 |
---|---|
Asp.Net Identity DataBase 첫 번째 접근 방식 사용 (0) | 2021.01.07 |
이 코드가 Hamcrest의 hasItems 컴파일을 사용하지 않는 이유는 무엇입니까? (0) | 2021.01.07 |
UIScrollView-스크롤 막대 표시 (0) | 2021.01.07 |
수십억 웃음 XML DoS 공격은 어떻게 작동합니까? (0) | 2021.01.07 |