본문 바로가기
책/Effective C#

Effective C# - 제네릭타입, 닫힌 제네릭 타입, 열린 제네릭 타입, JLT Compiler, IL

by 코딩하는 돼징 2023. 12. 5.
반응형

제네릭 타입

하나 이상의 타입 매개변수를 가지는 클래스 또는 메서드이다. 

class Piggy<T>

닫힌 제네릭 타입

구체적인 타입이 할당되지 않은 상태에서 사용된다.

Piggy<int> codePiggy = new Piggy<int>();

열린 제네릭 타입

일부의 타입 매개변수만 구체적인 타입이 정해서 사용된다.

Piggy<T> codePiggy = new Piggy<T>();

머신 코드 공유

참조 타입

데이터가 실제로 저장된 메모리 주소를 가리키는 참조를 사용한다. 객체의 인스턴스는 힙메모리에 저장되고 변수는 그 객체를 가리키는 참조를 갖는다.

MyPiggy piggy1 = new MyPiggy();
MyPiggy piggy2 = piggy1;

제네릭 타입의 타입 매개변수로 참조 타입이 전달되면,컴파일러는 해당 제네릭 타입에 대한 일반적인 코드를 생성하게 된다. 이 코드는 특정 참조 타입에 종속되지 않기 때문에 여러 참조 타입에 대해 공유된다. 따라서 List<string>, List<Stream>, List<MyClassType>와 같은 제네릭 타입을 사용할 때 동일한 머신 코드가 생성되어 공유다.

List<string> stringList = new List<string>();
List<Stream> OpenFiles = new List<Stream>();
List<MyClassType> anotherList = new List<MyClassType>();

위의 코드에서 List<T>는 T에 대한 일반적인 구조를 가지므로 string, Stream, MyClassType에 대해 각각 생성된 코드가 서로 다르지 않다. 즉 List<T>의 메서드들은 타입 매개변수 T가 참조 타입인 경우에는 동일한 코드를 사용다.


값 타입

데이터가 실제로 저장된 값을 직접 가지고 있다. 기본 데이터 형식, 구조체 등이 값 타입에 속한다. 실제 값이 저장되므로 서로 독립적이다.

int piggy1 = 10;
int piggy2 = piggy1;

타입 매개변수로 값 타입이 전달된 경우, 다른 규칙이 적용된다. JLT 컴파일러는 서로 다른 값 타입에 대해서는 각각 다른 머신 코드를 생성다. 따라서 아래와 같은 코드는 서로 다른 머신 코드를 사용하게 된다.

List<double> doubleList = new List<double>();
List<int> markers = new List<int>();
List<MyStruct> values = new List<MyStruct>();

List<double>, List<int>, List<MyStruct>는 각각 double, int, MyStruct에 대한 코드를 생성하여 서로 다른 머신 코드를 사용다. 이는 값 타입의 특성 때문에 각각의 값 타입이 다른 메모리 구조를 가지기 때문이다.


JLT(JLT Compiler)

JLT는 Just-In-Time의 약자로 프로그램이 실행되는 시점에 기계어로 번역하는 컴파일 기술이다. 이는 IL코드를 읽어 들여 특정 시점에 필요한 부분만을 선택적으로 컴파일하여 기계어로 변환한다. 이로써 응용 프로그램의 시작 시간을 단축하고 최적화된 코드를 생성할 수 있다.


IL(Intermediate Language)

.NET언어로 작성된 소스 코드는 C#같은 고수준 언어로 작성되어 컴파일 되면 IL로 변환된다. IL은 중간 언어로서 .NET의 어떤 언어로도 다시 컴파일될 수 있는 중립적인 형태의 코드이다. .NET에서 실행되는 어셈브리는 IL로 컴파일된 코드이며 CLR(Common Language Runtime)에서 해당 코드를 실행한다.


.NET런타임이 제네릭 정의를 JLT컴파일 할 때 타입 매개변수에 값 타입이 지정되면 두 단계를 거치게 된다. 

01 닫힌 제네릭 타입을 표현하기 위한 새로운 IL클래스를 생성

첫 번째 단계는 제네릭 정의에 있는 타입 매개변수(T 등)를 실제 값 타입으로 대체하여 닫힌 제네릭 타입을 생성한다. 예를 들어, List<T>에서 T를 int로 대체하여 List<int>와 같은 형태로 새로운 IL 클래스를 생성다.

02머신 코드를 생성

이처럼 두 단계가 필요한 이유는 어셈블리가 로드되는 시점에 JLT컴파일러가 클래스의 머신 코드를 생성하는 것이아니라 로드된 타입 내의 특정 메서드가 최초로 호출되는 시점에 호출된 메서드만을 JLT컴파일하기 때문이다. JLT컴파일이 수행되고 나면 메서드 내의 IL코드가 앞서 컴파일된 머신 코드로 대체된다.


이처럼 타입별로 서로 다른 코드가 생성된다는 것은 런타임에 메모리의 풋프린트가 커진다는 사실을 의미한다. 타입 매개변수로 값 타입을 취하는 닫힌 제네릭 타입은 개별적인 IL코드를 가진다. 타입 매개변수로 지정한 값 타입의 유형에 따라 서로 다른 머신 코드를 생성할 수 밖에 없기 때문이다. 하지만 제네릭 타입의 타입 매개변수로 값 타입을 지정하면 박싱과 언박싱을 피할 수 있고 결국 코드와 데이터의 크기가 줄어드는 장점이 있다. 그리고 컴파일러가 타입 매개변수에 대한 타입 안전성을 보장해주므로 런타임에 타입의 유형을 매번확인할 필요가 없다. 따라서 코드의 크기가 줄어들고 성능이 개선된다.

 

또한 제네릭 클래스 대신 제네릭 메서드를 사용하면 실제로 사용되는 메서드만 인스턴스화되므로 추가되는 IL코드의 양이 많지 않다. 제네릭이 아닌 일반 클래스 내에 정의된 제네릭 메서드는 사전에 JLT컴파일되지 않는다.

 

 

 

 

본 게시글은 Effective C#을 읽고 정리하였습니다.

 

 

 

반응형

댓글