Skip to content

태그: 컴파일러

총 16개의 글이 있습니다.
객체 표현과 GC
컴파일러
객체 표현은 언어의 값을 메모리에 어떤 모양으로 담을지를 정하는 설계이고, 값의 수명을 누가 어떻게 끝낼지가 그 설계에 딸려 나온다. 값의 종류가 닫혀 있는지 열려 있는지가 두 선택을 함께 결정한다. 정적 타입 언어는 정수를 기계 워드 하나로 담는다. 컴파일 시점에 이게 정수라는 걸 알기 때문에 타입 정보를 실행 중에 들고 다닐 필요가 없다. 동적 타입 언어는 그럴 수 없다. 값이 어디로 흘러가든 자기가 무엇인지 스스로 알고 있어야 한다. CPython은 모든 값을 힙 객체로 만들고 `PyObject *` 하나로 가리킨다. 아래는 main(3.16.0a0) 기준이고 측정은 3.14.6에서 했다. 모든 값이 객체다 객체의 머리는 참조 카운트와 타입 포인터다. c struct _object {
컴파일러 생성기
컴파일러
컴파일러 생성기는 명세를 입력으로 받아 컴파일러의 한 단계를 코드로 만들어 내는 도구다. 사람은 "무엇을 인식할 것인가"만 적고, "어떻게 인식할 것인가"에 해당하는 표와 루프는 기계가 채운다. 이런 도구가 성립하려면 그 단계가 기계적으로 유도 가능한 형태여야 한다. 정규표현에서 DFA를 만드는 절차가 있고 문법에서 파싱표를 만드는 절차가 있기 때문에 렉서와 파서에 생성기가 있는 것이지, 컴파일러의 모든 단계에 있는 것이 아니다. 명세와 산출물 주요 생성기의 입출력을 실제로 재 보면 규모의 차이가 드러난다. flex와 bison은 직접 돌린 결과이고, 나머지는 CPython main(3.16.0a0) 저장소의 파일이다. | 생
펌핑 보조정리
컴파일러
펌핑 보조정리는 어떤 언어가 정규언어가 아님을 보이는 도구다. 유한한 상태로는 무한한 구별을 할 수 없다는 사실을, 문자열을 늘려 보는 절차로 바꿔 놓은 것이다. DFA(Deterministic Finite Automaton, 결정적 유한 오토마타)의 상태는 말 그대로 유한하다. 그러므로 충분히 긴 문자열을 읽는 동안에는 같은 상태를 두 번 지날 수밖에 없다. 상태가 `p`개면 `p`개보다 긴 문자열을 읽는 사이에 반드시 하나가 반복된다. q₀ ──x──▶ q ──y──▶ q ──z──▶ (종료상태) └────────┘ 같은 상태로 돌아온다 `y`가 상태를 제자리로 되돌리므로, 기계는 `y`를 몇 번 돌았는지 알지 못한다. `xz`, `xyz`, `
Lexer와 DFA
컴파일러
렉서(어휘분석기, 스캐너)는 원시 프로그램을 문자 단위로 읽어 토큰이라는 문법 단위로 자르는 단계이다. 토큰의 형태는 정규표현으로 기술되고 정규표현이 나타내는 언어는 유한 오토마타가 인식하므로, 렉서의 실체는 DFA 하나를 돌리는 일이다. 구현이 갈리는 지점은 그 DFA를 사람이 짜느냐 생성하느냐다. 일반적인 프로그래밍 언어가 쓰는 토큰은 크게 다섯 종류가 있다. 식별자(`sum`, `a`, `b`), 상수(`1`, `3.14`, `"abc"`), 예약어(`if`, `while`, `return`), 연산자(`+`, `-`, `*`, `==`), 구분자(`(`, `[`, `;`, `,`)다. 전부 정규표현으로 적을 수 있다. 중첩 괄호처럼 세는 능력이 필요한 것은 여기 없다. 그래서 렉서는 스택 없이 상
NFA와 DFA
컴파일러
유한 오토마타는 유한한 개수의 상태만으로 문자열을 인식하는 기계이고, 정규표현이 나타내는 언어와 정확히 같은 범위를 인식한다. DFA와 NFA는 그 두 가지 형태로, 표현력은 같지만 실행 비용과 만들기 쉬운 정도가 다르다. 렉서 생성기가 정규표현을 받아 스캐너를 뱉는 과정이 결국 이 둘 사이의 변환이다. 정규표현 → ε-NFA → DFA → 최소화된 DFA 만들기 쉽다 실행이 싸다 표가 작다 유한 오토마타 형식적으로는 다섯 개의 구성원소로 정의한다. M = (Q, Σ, δ, q₀, F) Q : 상태들의 유한집합 Σ : 입력기호들의 유한집합 δ : 상태전이함수 q₀ : 시작상태, q₀ ∈ Q F : 종료상태들의 집합, F ⊆ Q `
오토마타 계층
컴파일러
오토마타 계층은 형식언어를 표현력에 따라 층으로 나누고, 각 층을 인식하는 기계를 대응시킨 것이다. 문법에 제약을 얼마나 두느냐가 층을 정하고, 그 층을 인식하려면 기계에 무엇이 필요한지가 따라 나온다. 컴파일러의 단계가 이 계층 위에 놓인다. 렉서와 파서가 나뉘는 이유도 여기서 나온다. 형식언어와 촘스키 계층 글자 `a`와 `b`만 쓸 수 있다고 하자. 둘을 아무렇게나 늘어놓으면 나오는 경우의 수는 끝이 없을 것이다. ε, a, b, ab, ba, aab, bba, abab, aaabb, ... 쓸 수 있는 기호의 집합 `T = {a, b}`를 알파벳이라 하고, 그 기호를 0개 이상 나열해 만들 수 있는 모든 문자열의 집합을 `T*`라고 한다면, 위의 나열이 `T*`이다. `ε`은
인라인 캐싱과 특수화
컴파일러
인라인 캐싱은 어떤 연산이 지난번에 무엇이었는지를 그 명령어 자리에 기록해 두고 다음번에 재사용하는 기법이고, 특수화는 그 기록을 근거로 명령어 자체를 전용 명령으로 바꿔치기하는 것이다. 동적 타입 언어의 비용은 연산 자체가 아니라 매번 무엇을 해야 하는지 다시 알아내는 데 있다. python def add(a, b): return a + b `a + b` 한 줄을 실행하려면 `a`의 타입을 보고, 그 타입의 덧셈 구현을 찾고, 없으면 `b`의 타입에서 역방향 연산을 찾고, 정수라면 크기를 확인해 알맞은 경로로 간다. `a`와 `b`가 언제나 작은 정수인 함수에서도 이 과정을 매 호출마다 반복한다. 정적 타입 언어라면 컴파일 시점에 `add` 기계어 명령 하나로 끝났을 일이다. 출
코드 최적화
컴파일러
코드 최적화는 프로그램의 의미를 바꾸지 않으면서 계산 횟수를 줄이거나 실행시간을 짧게, 기억용량을 적게 만드는 변환이다. 코드 최적화를 하기 위해선 두 가지 조건이 요구된다. 중간 결과에 이름이 있어야 한다. `T2 := C + D`처럼 결과에 이름이 붙어 있어야 그 줄을 다른 곳으로 옮길 수 있다. 스택 위 익명의 자리에 있으면 옮길 수 없다. 3-주소 코드가 최적화용 중간언어인 이유다. 제어 흐름이 그래프여야 한다. 분기 없이 순차 실행되는 최대 구간을 기본 블록이라 하고, 블록을 노드로 흐름을 간선으로 만든 것이 흐름 그래프라 한다. 최적화를 하기 위해선 이 값이 여기까지 살아 있는가, 이 코드에 도달할 수 있는가? 같은 질문에 답할 수 있어야 하고, 그래프 구조에선
중간언어
컴파일러
중간언어는 원시언어와 목적코드 사이에 두는 표현으로, 컴파일러를 앞단과 뒷단으로 나누는 경계다. 앞단은 소스를 중간언어로 바꾸고 뒷단은 중간언어를 기계어로 바꾼다. 왜 한 단계를 더 두는지는 언어 `m`개를 기계 `n`종에 올리는 상황을 생각하면 분명하다. 직접 번역하면 번역기가 `m × n`개 필요하지만, 중간언어를 거치면 앞단 `m`개와 뒷단 `n`개, 합쳐서 `m + n`개로 끝난다. 이것 말고도 얻는 것이 있다. 컴파일러를 기능적으로 독립적인 모듈로 나눌 수 있다 원시 프로그램의 이식성이 올라간다 고급언어와 저급 목적코드 사이의 의미적 간극을 이어 준다 기계와 독립적인 최적화를 할 수 있다. 기계코드로 바뀐 뒤에 하는 것보다 훨씬 효율적이다 중간코드를 인터프리터로 직접 실행할 수 있다 대가도 있