Posts .NET 환경의 컴파일 과정 - CLR, CIL, JIT, AOT
Post
Cancel

.NET 환경의 컴파일 과정 - CLR, CIL, JIT, AOT

.NET 환경의 컴파일 과정


예전의 C, C++의 경우 개별 환경이 프로그램의 실행 시간에 영향을 미치는 문제가 있었다.

하지만 Java가 나오면서 컴파일된 바이트코드는 윈도우, 리눅스, 그 어떤 실행환경이든 JVM만 실행 가능하다면 실행할 수 있다는 장점을 통해 인기를 끌었고,

마이크로소프트는 이를 이용해 기존 문제를 해결할 수 있도록 .NET 환경에 가상 머신을 만들어서

.NET 환경의 언어로 개발된 IL(Intermediate Language, 중간 언어) 코드들은 .NET Framework가 설치된 어떠한 환경에서도 실행할 수 있도록 하였다.

대표적인 예시로 VB .NET, C#이 있다.


CLR(Common Language Runtime, 공통 언어 런타임)은 이 가상머신의 구성요소 중 하나이며,

CIL(Common Intermidiate Language, 공통 중간 언어) 코드는 .NET 환경의 언어로 작성된 소스 코드를 컴파일했을 때 만들어지는 바이트코드를 의미하며, 어셈블리 코드의 일종이다.

image

.NET 환경의 언어로 개발할 때, 소스코드를 컴파일하게 되면 컴파일 타임에 해당 언어의 컴파일러에 의해 우선 바이트코드인 CIL Code를 생성한다.

그리고 CLR런타임에 JIT(Just-In-Time) 또는 AOT(Ahead-Of-Time) 컴파일 방식을 이용하여 CIL Code를 OS가 이해할 수 있는 Native Code 로 변환하게 된다.


JIT 컴파일


JIT : Just-In-Time


그렇다면 JIT 컴파일이란 무엇일까?

우선, C/C++로 작성된 프로그램은 다른 언어에 비해 빠르다고 알려져 있다.

왜냐하면 C/C++은 정적인 네이티브 코드(.exe, .dll)를 생성해 배포하기 때문이다.

반면에 Java, C#과 같은 언어들은 컴파일러가 생성한 IL 코드를 생성하여 갖고 있다가

프로그램을 실행시키면 런타임에 가상머신을 통해 동적으로 네이티브 코드를 생성하게 되는데,

이 때 가상머신에 의한 런타임 컴파일 방식을 JIT 컴파일이라고 한다.


JIT 컴파일이란 런타임에 IL 코드를 네이티브 코드로 바꾸는 컴파일 과정을 의미한다.

프로그램이 처음 로드될 때 가상머신은 IL 코드를 실행 환경에 알맞는 네이티브 코드로 컴파일한다.

그리고 다음 실행 시에는 JIT 과정 없이 해당 네이티브 코드를 실행하게 된다.

C#에서 성능 테스트를 위해 코드를 반복 수행할 때

첫 수행이 아주 느리고, 다음 수행부터 비슷하게 빠른 현상을 발견할 수 있는데, 이것은 바로 JIT 컴파일 때문이다.


JIT 컴파일 방식 덕분에, 개발자는 프로그램의 실행 환경을 고려하지 않고 개발할 수 있다는 장점이 있다.

하지만 JIT 컴파일 방식은 메모리를 많이 사용하고 속도도 떨어진다는 단점이 있다.


AOT 컴파일


AOT : Ahead-Of-Time


JIT 컴파일 방식의 느린 속도를 해결하기 위해 만들어진 컴파일 방식.

컴파일 타임에 중간 코드를 실행 환경에 적합한 Native Code로 컴파일을 모두 완료한다.

CIL Code를 C++ 컴파일러를 통해 .NET Native 이진코드로 변환하게 된다.

.NET Native는 C++와 유사하지만, C++처럼 Unmanaged는 아니다.

C++는 CRT.dll(C 런타임)을 사용하는 데 비해,

.NET NativeMRT.dll(최소 CLR 런타임)을 사용하며 여기에 GC 코드가 포함되어 있다.


개념 정리


Native Code는 .exe, .dll을 의미하나요?

  • Yes.

그런데 CIL Code.exe, .dll 내부에 포함되어 있다.

그러니까 어쨌든 .exe, .dll은 다 바이너리인데,

애초에 특정 실행 환경에 맞게 딱 정적으로 만들어진 바이너리를 Native Code라고 부르고,

.NET 환경에서 CLR 가상머신이 런타임에 JIT 컴파일을 통해 입맛대로 맛보고 즐길 수 있도록 만들어진 바이너리 내부에 있는 프로그램 코드 부분을 CIL Code라고 한다.


그래서 .exe, .dll이 뭐라구요?

한마디로 정리하면 바이너리 파일(Binary File).

특정 실행 환경에 맞게 정적으로 만들어진 바이너리이면 Native Code,

.NET 환경에서 JIT 컴파일이 가능하도록 만들어진 바이너리이면 그냥 뭐..

.NET Binary Code라고 불러야 할 것 같다.

그리고 그 내부에는 다양한 데이터들을 담고 있으며, CIL Code도 포함되어 있다.

후술하겠지만, 결국 .NET Binary CodeModule을 의미한다.


어셈블리? 어셈블리어? 어셈블러? 바이너리?

이것도 용어가 참 헷갈리는데, 정리해보면


어셈블리어(Assembly Language)

  • 기계어와 고급 언어 사이의 수준을 가지며, 기계어와 일대일 대응이 되는 저급 언어
  • opcode + operand로 이루어져 있다.

어셈블리(Assembly)

  • 또는 어셈블리 코드. 어셈블리어로 작성된 코드를 의미한다.

어셈블러(Assembler)

  • 어셈블리 코드를 기계어 코드로 번역하는 도구

디스어셈블러(Disassembler)

  • 기계어 코드를 어셈블리 코드로 번역하는 도구

바이너리(Binary)

  • 또는 바이너리 코드. 기계어로 작성된 코드를 의미한다.


CIL Code는 ‘어셈블리’라고 부르던데요?

  • .NET에서의 어셈블리는 단순히 어셈블리어로 작성된 어셈블리 코드를 의미하지 않는다.
  • .NET의 어셈블리는 “버전 관리되고 배포 되는 프로그램의 단위”를 의미한다.
  • 어쨌든 결국 .NET의 어셈블리는 .exe, .dll 또는 이들의 집합을 의미하며, 단일 파일이 하나의 어셈블리일 수도 있고, 여러 개의 파일이 모여 하나의 어셈블리를 이룰 수도 있다.
  • 그러니까 .NET에서 Assembly라고 부르는 녀석은 ‘Binary File의 집합’인 셈이다.


.NET Assembly 구조

image

.NET Assembly

  • 1개 이상의 Module로 구성된다.
  • 모듈 중 하나는 반드시 다른 모듈 목록을 관리하는 Manifest 데이터를 담고 있어야 한다.

Module

  • 컴파일 완료된 .dll, .exe 바이너리 파일을 의미한다.
  • 어셈블리가 단일 모듈로 이루어진 경우, 바이너리 파일이 어셈블리라고 할 수 있다.
  • Manifest를 담고 있는 경우 Primary Module(주모듈), 아닌 경우 Secondary Module(부모듈)이라고 한다.

Manifest

  • 연결된 다른 모듈들의 정보를 갖는 메타데이터.
  • 어셈블리 내의 모든 모듈의 참조를 담고 있다.

Type Metadata

  • 어셈블리 내에서 사용되는 모든 타입에 대한 구체적인 정보를 담고 있다.
  • 이 요소 덕분에 리플렉션이 가능하다.

CIL Code

  • 각 언어의 소스 코드가 중간 언어 코드를 거쳐 기계어로 컴파일된 실제 프로그램 내용물.


결국 CIL의 정체는?

  • .NET 고유의 객체 지향 어셈블리 언어.

  • .NET 환경에서 인간이 이해할 수 있는 가장 낮은 수준의 프로그래밍 언어.


CIL 코드 예시 :

1
2
3
4
5
6
7
8
9
10
11
// Hello World Program
.assembly Hello {}
.assembly extern mscorlib {}
.method static void Main()
{
    .entrypoint
    .maxstack 1
    ldstr "Hello, world!"
    call void [mscorlib]System.Console::WriteLine(string)
    ret
}


그럼 CIL로 만들어진 파일의 확장자는?

  • .il


  • Ilasm.exe(IL 어셈블러)를 통해 .il -> .dll or .exe 생성

  • Ildasm.exe(IL 디스어셈블러)를 통해 .dll or .exe -> .il 생성


그러니까 도대체 .NET 환경의 가상머신은 누구인가요?

CLR이 가상머신인가 하니 이건 가상 머신의 일부 구성요소라고 하고,

.NET이 가상머신 역할을 하는거에요! 라고 모호하게 설명하기도 하고,

가상머신이란 녀석은 도대체 누구인가..

결국 CLR이 맞다.


그럼 .NET Framework의 정체는 무엇인가요?

= FCL(Framework Class Library) + CLR

FCL.NET Framework를 대상으로 하는 모든 언어가 사용할 수 있는 공용 클래스 라이브러리,

CLR은 공통 언어 런타임 클래스이며, 보안, 메모리, 스레드 관리, JIT 컴파일을 수행하는 가상 머신이다.


C언어가 (상대적으로) 빠른 이유

  • 컴파일을 완료하면 정적인(특정 환경에서 실행 가능한) 네이티브 코드를 생성한다.
  • 이를 실행하면 JIT 컴파일 과정 거칠 것 없이 바로 실행되므로 빠르다.


Java와 C#이 (상대적으로) 느린 이유

  • 컴파일을 완료하면 IL(중간 언어) 코드인 .class, CIL 코드를 생성하고, 이를 바이너리에 포함시킨다.
  • 각각 JVMCLR이 런타임에 JIT 컴파일을 통해 Native Code를 생성하는 과정을 거쳐야 하므로 느리다.


컴파일 과정 간략 정리


(c) : Compile Time
(r) : Runtime

  • Source Code -> 컴파일(c) -> CIL Code -> JIT 컴파일(r) -> Native Code

  • Source Code -> 컴파일(c) -> CIL Code -> AOT 컴파일(c) -> .NET Native Code


References


This post is licensed under CC BY 4.0 by the author.