컴파일러
1. Compiler
컴파일이란 어떤 언어의 코드를 다른 언어로 바꿔주는 과정. 대표적인 예는 C++ 코드를 기계어로 바꿔주는 것이다. 사전적 의미는 (여러 출처에서 자료를 따와) 엮다, 편집[편찬]하다라는 뜻으로 소스코드와 기타 라이브러리 등을 하나로 엮어서 결과물을 만들어 낸다고 이해하면 될 듯.
컴파일러를 엄밀히 말하자면, 어떤 프로그래밍 언어로 쓰여진 소스 파일을 다른 언어로 바꾸어주는 번역기 인 셈이다. 어떤 언어 A를 B로 바꾸면 그게 컴파일러다. Scheme을 C언어로 번역한다든지, 심지어 기계어를 C언어로 번역하더라도(!) 컴파일러라고 칭할 수 있다. 하지만 대개의 경우 고수준 언어를 기계어로 번역하는 프로그램을 일컫는다. 이 두 개념을 구분하고자 할 경우 전자를 트랜스컴파일러(transcompiler)나 소스간 컴파일러(source-to-source compiler), 후자를 디컴파일러(decompiler)라고 많이 부른다. 어셈블리어를 기계어로 번역하는 프로그램 역시 어셈블리어의 특수성 때문에 따로 어셈블러라 한다.
초기엔 프로그램을 작성하기 위해서는 컴퓨터 위에서 바로 돌아가는 기계어를 통하여 프로그래밍을 했다. 그러나 이런 과정은 생산성, 기기 간 호환성[1] , 디버깅 등 모든 면에서 효율적이지 않다. 따라서 컴퓨터공학이 발전하면서 많은 부분 추상화된 고수준 언어를 작성하고 이를 번역기를 통해 기계어로 번역하기 시작했는데, 이 번역기가 바로 컴파일러이다. 현재 많은 프로그램은 컴파일러를 통하여 전체를 기계어로 번역하여 실행하므로 프로그램 개발에 필수적인 툴 중에 하나다.
한 언어의 컴파일러를 자신의 언어로 재작성하는 것을 부트스트래핑이라고 한다. 재밌는 것은 컴파일러도 결국 하나의 언어로 짜여진 프로그램이라는 점. 따라서 바이너리 포맷의 파일을 쓸 수 있고 트리 자료구조를 생성할 수 있는 언어는 부트스트래핑(bootstrapping)이 가능하다. [2] 범용 목적 프로그래밍 언어는 당연히 이 조건을 만족하므로 부트스트래핑이 가능하다. [3] gcc라는 C 컴파일러는 컴파일에 C++을 사용한다. 컴파일러의 최초 버전만 다른 언어용의 컴파일러 또는 어셈블러의 도움을 받아 만들고 나면[4] 컴파일러가 자기 자신의 언어로 짜여질 수 있다. 일례로, GHC라는 Haskell 컴파일러는 최초에 Lazy ML이라는 다른 언어로 작성되었다가, 자기 자신의 언어인 Haskell로 재작성 되었다. 이런 식으로 거슬러 올라가다 보면, 최초의 어셈블러는 기계어로 만들어졌고 최초의 고급 언어는 어셈블러로 만들어졌음을 알 수 있다.
원칙적으로 컴파일러는 프로그램을 기계어로 바꾸기만 할 뿐 이를 바로 실행이 가능하게 하지는 않는다. 여러 소스 파일에서 나온 결과물을 합치고 라이브러리도 포함시키는 등 별도의 작업을 거쳐야 실행이 가능해지는데 이를 수행하는 프로그램이 링커이다. 하지만 보통은 그냥 뭉뚱그려 컴파일러라 부르는 경우가 많다. 또한 요즘은 그냥 프로그램 하나만 돌리면 컴파일과 링킹을 한 번에 끝낼 수 있게 되어 있다. 물론 내부적으로는 컴파일러와 링커가 따로 있어서 이를 이용하는 경우가 많지만.
컴파일을 하는 대신, 소스를 한꺼번에 번역하지 않고 명령 하나하나를 실행할 때마다 해석하여 계산하는 방법도 있는데 이 해석기를 인터프리터라 해서 따로 분류한다. 컴파일러가 번역기라면 인터프리터는 통역기인셈.
인터프리터 언어에 비해 컴파일 언어의 단점은 수정이 용이하지 않다는 점이다. 수정 사항이 발생하면 다시 컴파일을 해야 되는데, 작은 프로그램일 경우에는 문제가 되지 않지만 컴파일이 몇 시간씩 걸리는 덩치 큰 프로그램에서는 문제가 된다. 특히 수정 사항이 빈번하게 발생할 경우에는 큰 문제가 된다. 이 때문에 수정 사항이 빈번하게 발생할 것 같은 부분은 인터프리터를 쓰는 방법으로 따로 빼 두는 기법을 많이 사용한다. (인터프리터는 속도는 느리지만 수정이 간단하다는 장점이 있다.)
컴파일러는 다음과 같은 세 분류로 나뉜다.
- 원시 코드를 바로 기계어로 변환하는 정적 컴파일(Static Compilation)
- 바이트코드 등의 중간 코드를 기계어로 변환하는 AOT 컴파일(Ahead-Of-Time Compilation)
- 실행시 최초 한 번에 한해 컴파일을 거치는 JIT 컴파일(Just-In-Time Compilation)
대졸 프로그래머가 할 수 있는 가장 어려운 프로그래밍 중 하나가 컴파일러, OS, 게임 엔진 제작이다.[5] 컴파일러는 대학교 컴퓨터공학과 커리큘럼의 가장 마지막(4학년)에 있는 과목이다.[6] 컴파일러를 제작하려면 하드웨어와 소프트웨어 전부를 이해하고 있어야 하고 자료구조와 알고리즘에 대한 심도 깊은 이해와 함께 전산수학에도 능해야 한다. 컴파일러를 제작할 수 있을 정도의 실력이라면 국내 탑티어 대기업에 들어갈 정도의 실력이 된다. 거기서 더 나간 스킬트리로는 프로그램 최적화가 있는데 이건 컴파일러의 보조 스킬.
기계어를 다시 프로그래밍 언어로 바꾸는 디컴파일이라는 용어도 있다. 사실 컴파일 과정에서 소스 코드의 일부 정보를 지워버리기 때문에 완전한 디컴파일은 불가능하다. 대체로 Java는 디컴파일시 손실되는 정보가 적은 편이고 C++의 경우 컴파일 프로필이 '릴리즈' 모드일 경우 상당수의 정보가 손실된다. 간혹 프로필을 '디버그'로 둔 채 그대로 릴리즈해버린 경우가 있는데 이 경우에는 디컴파일시 꽤 많은 정보가 보존되어 나온다. 하지만 대개 지역변수 이름 같이 지역성이 명백하고 기계한테 필요없는 정보는 날아가는 편이다.
1.1. 크로스 컴파일러
다른 CPU나 다른 운영체제에서 작동하는 실행코드를 만들어주는 컴파일러. 예를 들자면 ARM에서 구동시킬 실행파일을 x86-64 리눅스 환경에서 컴파일 및 링크하는 것을 말한다. 보통 임베디드 시스템에서 작동시킬 실행파일을 만들 때 사용한다. 주된 이유는 속도와 생산성 때문. 컴파일 과정은 CPU 작업을 상당히 많이 요구하며, 임베디드 시스템은 CPU 성능이 떨어지고 메모리가 협소하기 때문에 고성능 컴퓨터에서 편리하게 코드를 편집하고 컴파일 작업을 진행하는 것이 훨씬 더 빠르고 생산성이 좋다. 486~586 수준의 임베디드 시스템 내에서 직접 컴파일하는 것도 가능하지만, 고성능 x86-64 머신을 사용하는 게 20배 이상 빠르다. 물론 슈퍼컴퓨터용 바이너리를 8086에 DOS 같은 구닥다리에서 컴파일해 만들어도 크로스 컴파일이다.
안드로이드(운영체제)나 iPhone에서 작동하는 앱을 만들기 위해 윈도우나 iMac에서 개발도구로 실행파일을 만들어내는 것은 모두 크로스 컴파일에 속한다.
1.2. intrinsic
C 등의 고급 언어에서 CPU의 인스트럭션 하나로 해결할 수 있는 복잡한 기능(?)을 사용할 경우 특정 함수를 호출하면 컴파일러가 함수를 호출하는 명령을 넣어 주는 것이 아니라 그냥 그 자리에 어셈블리를 사용한 것과 동일하게 기계어 코드를 직접 넣어주는 함수(처럼 생긴 예약어)가 있는데 이것을 intrinsic, 혹은 built-in function이라고 한다(인라인 함수와는 다르다). Compare-And-Exchange[7] 의 예를 들어 a값이 m인 경우 n으로 바꾸는 코드는 x86 CPU에서 cmpxchg 인스트럭션 하나로 해결이 가능하다. 이러한 명령은 컴파일러가 지원하는 인트린식을 사용하면 된다. RISC에서 pseudoinstruction[8] 을 사용하는 것과 반대의 개념으로 볼 수 있다.
컴파일러별로, 그리고 아키텍처별로 지원하는 인트린식이 다르니 사용을 원하면 컴파일러 매뉴얼을 참조하면 된다.
int a, b;
...
b = CAS(&a, 10, 20);
....
- C 코드
int CAS(int* pos, int oldval, int newval)
{
int oldpos = *pos;
if(*pos == oldval)
*pos = newval;
return oldpos;
}
- 어셈블리 코드(x86-64)
CAS:
movl %edx, %eax
lock
cmpxchgl %esi, (%rdi)
ret
- 어셈블리 코드(x86 32비트) (built-in function 을 사용하지 않은 경우)
CAS:
mov ecx, dword ptr[esp + 4]
mov eax, dword ptr[ecx]
push eax;
mov ebx, dword ptr[esp + 12];
cmp ebx, eax;
jne endpoint;
mov dword ptr[ecx], dword ptr[esp + 16];
endpoint:
pop eax;
ret 16;
- 어셈블리 코드(x86) (built-in function 을 사용한 경우)
CAS:
mov ecx, dword ptr[esp + 4]
mov eax, dword ptr[ecx]
mov ebx, dword ptr[esp + 8];
mov edx, dword ptr[esp + 12];
cmpxchg ebx, edx;
mov dword ptr[ecx], esp;
ret 16;
- VC++ intrinsic
b = _InterlockedCompareExchange(&a, 20, 10);
- gcc built-in function
b = __sync_val_compare_and_swap(&a, 10, 20);
또다른 예제로 popcount를 들 수 있다. 정수형의 값에서 1로 세트된 비트를 세는 것이다.printf("%d %d\n", popcount(0x0000FFFF), popcount(0x00000001));
16 1
- C 코드
int popcount(unsigned int a)
{
int i, cnt = 0;
for(i = 0; i < sizeof(unsigned int) * 8; i++)
{
/* i번째 비트가 1이면 cnt 증가 */
if(((a >> i) & 1) == 1) cnt++;
/* cnt += (a>>i) & 1; ^^; */
}
return cnt;
}
- gcc built-in function
int popcount(unsigned int a)
{
return __builtin_popcount(a);
}
인텔의 x86 CPU는 삼각함수나 로그 등의 실수연산을 CPU가 바로 지원해주기 때문에 x86용 컴파일러는 대부분의 수학 함수를 인트린식으로 지원한다.
1.3. 목록
1.4. 관련 사이트
- Godbolt Compiler Explorer: 온라인으로 각종 언어의 컴파일러를 사용해 코드를 컴파일해볼 수 있는 사이트이다.
1.5. 관련 문서
2. 아사미야 키아의 만화
전뇌(컴퓨터)세계에서 날아온 컴파일러, 어셈블러, 인터프리터 등의 루틴이라는 존재들의 여성들을 중심으로 한 코미디 만화. 연애 노선도, 개그 노선도, 스토리성도 애매한 작품이었다. 속편으로는 연애 노선이 약간 늘어난 어셈블러OX 가 있다. 주인공인 컴파일러는 작가가 그린 또 다른 작품인 사일런트 뵈비우스 0에 잠시 등장하기도 했고 현재 연재 중인 히메가미 가젯에도 등장한다.[9]
뮤직 클립 영상과 OVA도 만들어졌다. 음의 장과 양의 장, 그리고 컴파일러 파스타 총 3종.
음의 장은 개그 요소가 전혀 없는 진지한 일상, 양의 장은 개그&연애요소라는 상반된 분위기가 특징. 파스타는 본편 처럼 정신 없는 분위기.
[1] 예를 들어서 ARM 아키텍쳐 대상으로 작성된 프로그램은 x86 아키텍쳐에서 안돌아가는 식으로. [2] 트리 자료구조를 사용하지 않는다면 어셈블러에 해당하며, 고수준 언어를 컴파일 할 수는 없다.[3] 그렇지 않은 일부 특수 목적 언어는 영원히 다른 언어의 도움을 받아야 한다.[4] 또는 다른 아키텍처상에서 구동하는 크로스컴파일러로[5] 웹 브라우저 엔진도 2010년대 오면서 OS급의 복잡성을 자랑하게 되었지만 엔진이 대부분 오픈 소스인 고로 그냥 끌리는 거 하나 퍼서 쓰면 된다(...).[6] 일부 학교는 3학년 때 편성하기도 한다. # [7] Compare-And-Swap이라고도 하며 줄여서 CAE 또는 CAS라고 부른다. 이는 멀티 쓰레딩이나 멀티 프로세싱에서 같은 메모리를 동시에 수정할때 동기화 문제를 해결하기 위해 현대의 대부분의 CPU에서 지원하는 원자적인(atomic) 명령어이다.[8] 어셈블리 명령어는 있지만 실제 바이너리를 까보면 어러 명령어의 집합으로 나오는 경우[9] 히메가미 가젯 5편 말미에 컴파일러가 등장한다.