컴파일러의 시간 복잡성


54

컴파일러의 시간 복잡성에 관심이 있습니다. 고려해야 할 많은 컴파일러, 컴파일러 옵션 및 변수가 있기 때문에 분명히 이것은 매우 복잡한 질문입니다. 특히, 나는 LLVM에 관심이 있지만 사람들이 연구를 시작할 장소 나 생각에 관심이 있습니다. 꽤 구글은 빛을 거의 가져 오지 않는 것 같습니다.

제 생각에는 지수 적이지만 실제 시간에는 거의 영향을 미치지 않는 최적화 단계가있을 것입니다. 예를 들어, 숫자를 기반으로하는 지수는 함수의 인수입니다.

내 머리 꼭대기에서 AST 트리 생성은 선형 적이라고 말할 수 있습니다. IR 생성은 계속 증가하는 테이블에서 값을 찾는 동안 트리를 단계별로 실행해야하므로 또는 입니다. 코드 생성 및 연결은 비슷한 유형의 작업입니다. 따라서 현실적으로 자라지 않는 변수의 지수를 제거하면 내 추측은 입니다.O(n2)O(nlogn)O(n2)

그래도 완전히 틀릴 수 있습니다. 누구든지 그것에 대해 생각이 있습니까?


7
"지수", "선형", 또는 이라고 주장 할 때는주의해야합니다 . 적어도 나에게, 당신이 입력을 측정하는 방법이 전혀 분명하지 않습니다 (지수는 무엇입니까? 은 무엇을 의미합니까 ?)O(n2)O(nlogn)n
Juho

2
LLVM을 말할 때 Clang을 의미합니까? LLVM은 여러 개의 다른 컴파일러 하위 프로젝트가있는 큰 프로젝트이므로 약간 모호합니다.
Nate CK

5
C #의 경우 최악의 문제에 대해 적어도 지수입니다 (C #에서 NP complete SAT 문제를 인코딩 할 수 있음). 이것은 단지 최적화가 아니라 함수의 올바른 과부하를 선택하는 데 필요합니다. C ++과 같은 언어의 경우 템플릿이 완성 되었기 때문에 결정할 수 없습니다.
코드 InChaos

2
@ 제인 나는 당신의 요점을 이해하지 못합니다. 템플릿 인스턴스화는 컴파일 중에 발생합니다. 올바른 출력을 생성하기 위해 컴파일러가 해당 문제를 해결하도록하는 방식으로 어려운 문제를 템플릿으로 인코딩 할 수 있습니다. 컴파일러를 튜링 완전한 템플릿 프로그래밍 언어의 해석기로 간주 할 수 있습니다.
코드 InChaos

3
C # 과부하 해결은 여러 과부하를 람다 식과 결합 할 때 매우 까다 롭습니다. 이를 사용하여 부울 수식을 인코딩 할 수 있습니다. 적용 가능한 과부하가 있는지 확인하려면 NP-complete 3SAT 문제가 필요합니다. 실제로 문제를 컴파일하려면 컴파일러는 실제로 해당 수식에 대한 솔루션을 찾아야합니다. Eric Lippert는 자신의 블로그 게시물 Lambda Expressions vs. Anonymous Methods, Part 5
CodeInChaos

답변:


50

귀하의 질문에 대답하기위한 가장 좋은 책은 아마도 Cooper와 Torczon, "컴파일러 엔지니어링"2003 일 것입니다. 대학 도서관에 액세스 할 수 있다면 사본을 빌릴 수 있어야합니다.

llvm 또는 gcc와 같은 프로덕션 컴파일러에서 설계자는 모든 알고리즘을 미만으로 유지하기 위해 모든 노력을 기울입니다. 여기서 은 입력 크기입니다. "최적화"단계에 대한 일부 분석의 경우 이는 실제로 최적의 코드를 생성하는 대신 휴리스틱을 사용해야한다는 것을 의미합니다.O(n2)n

렉서는 유한 상태 기계이므로, (문자) 입력의 크기 및 스트림 생성 파서로 전달되는 토큰.O(n)O(n)

많은 언어의 많은 컴파일러에서 파서는 LALR (1)이므로 입력 토큰 수 에서 시간 에 토큰 스트림을 처리합니다 . 구문 분석 중에는 일반적으로 기호 테이블을 추적해야하지만 많은 언어의 경우 해시 테이블 스택 ( "사전")으로 처리 할 수 ​​있습니다. 각 사전 액세스는 이지만 때로는 기호를 찾기 위해 스택을 걸어야 할 수도 있습니다. 스택의 깊이는 이며 여기서 는 범위의 중첩 깊이입니다. (따라서 C와 같은 언어에서는 몇 개의 중괄호 레이어가 들어 있습니까?)O(n)O(1)O(s)s

그런 다음 구문 분석 트리는 일반적으로 제어 플로우 그래프로 "평 평화"됩니다. 제어 흐름 그래프의 노드는 3 주소 명령 (RISC 어셈블리 언어와 유사) 일 수 있으며 제어 흐름 그래프의 크기는 일반적으로 구문 분석 트리의 크기에 선형입니다.

그런 다음 일련의 중복 제거 단계가 일반적으로 적용됩니다 (공통 하위 표현 제거, 루프 불변 코드 모션, 상수 전파 등). 결과에 대해 최적의 결과는 거의 없지만 실제로는 "최적화"라고합니다. 실제 목표는 컴파일러에서 설정 한 시간 및 공간 제약 내에서 가능한 한 많이 코드를 개선하는 것입니다. 각 중복 제거 단계는 일반적으로 제어 흐름 그래프에 대한 몇 가지 사실에 대한 증거가 필요합니다. 이러한 증명은 일반적으로 데이터 흐름 분석을 사용하여 수행됩니다 . 대부분의 데이터 흐름 분석은 가 루프 중첩 깊이이고 흐름 그래프를 통과하는 데 시간 가 걸리는 흐름 그래프 를 통해 패스로 수렴되도록 설계되었습니다.O(d)dO(n)여기서 은 3- 주소 명령의 수입니다.n

보다 정교한 최적화를 위해보다 정교한 분석을 수행 할 수 있습니다. 이 시점에서 트레이드 오프가 시작됩니다. 분석 알고리즘이 보다 훨씬 적게 걸리기를 원합니다.O(n2)전체 프로그램의 흐름 그래프의 크기에 시간이 걸리지 만 이는 증명하기에 비용이 많이 드는 정보 (및 변환을 개선하는 프로그램)없이 수행해야 함을 의미합니다. 이에 대한 전형적인 예는 별칭 분석입니다. 여기서 일부 메모리 쓰기 쌍의 경우 두 쓰기가 동일한 메모리 위치를 대상으로 할 수 없음을 증명하려고합니다. (한 명령을 다른 명령 위로 이동할 수 있는지 확인하기 위해 별칭 분석을 수행 할 수 있습니다.) 별칭에 대한 정확한 정보를 얻으려면 프로그램을 통해 가능한 모든 제어 경로를 분석해야 할 수 있습니다. 이는 분기 수의 지수입니다 프로그램에서 (따라서 제어 흐름 그래프의 노드 수에 지수 적으로)

다음으로 레지스터 할당에 들어갑니다. 레지스터 할당은 그래프 색 문제 로 표현 될 수 있으며 , 최소한의 색으로 그래프를 채색하는 것은 NP-Hard로 알려져 있습니다. 따라서 대부분의 컴파일러는 합리적인 시간 범위 내에서 가능한 한 최대한 레지스터 유출 수를 줄이기 위해 레지스터 유출과 결합 된 탐욕적 휴리스틱을 사용합니다.

마지막으로 코드 생성에 들어갑니다. 코드 생성은 일반적으로 기본 블록 이 단일 입력 및 단일 종료를 갖는 선형 연결된 제어 흐름 그래프 노드 세트 인 시점에서 최대 기본 블록으로 수행됩니다 . 이것은 당신이 다루려고하는 그래프가 기본 블록에있는 3- 어드레스 명령어 세트의 의존성 그래프이고 사용 가능한 기계를 나타내는 그래프 세트로 다루려고하는 경우 문제를 다루는 그래프로 재구성 될 수 있습니다. 명령. 이 문제는 가장 큰 기본 블록의 크기 (원칙적으로 전체 프로그램의 크기와 동일한 순서 일 수 있음)의 크기에 기하 급수적이므로 가능한 커버링의 작은 하위 집합 만있는 휴리스틱으로 다시 수행됩니다. 검사했다.


4
셋째! 또한, 컴파일러가 해결하려고하는 많은 문제 (예 : 레지스터 할당)는 NP-hard이지만 다른 것들은 공식적으로 결정할 수 없습니다. 예를 들어, 호출 p () 다음에 호출 q ()가 있다고 가정하십시오. p가 순수한 함수이면 p ()가 무한 반복되지 않는 한 호출을 안전하게 재정렬 할 수 있습니다. 이를 증명하려면 정지 문제를 해결해야합니다. NP-hard 문제와 마찬가지로 컴파일러 작성자는 가능한 한 솔루션을 근사화하는 데 거의 또는 적은 노력을 기울일 수 있습니다.
가명

4
아, 한가지 더 : 오늘날 사용되는 일부 유형 시스템이 이론상 매우 복잡합니다. Hindley-Milner 형식 유추는 DEXPTIME 완료로 알려져 있으며 ML 유사 언어는이를 올바르게 구현해야합니다. 그러나 a) 실제 프로그램에서는 병리학 적 사례가 발생하지 않으며 b) 실제 프로그래머는 더 나은 오류 메시지를 얻기 만하면 형식 주석을 작성하는 경향이 있기 때문에 실행 시간은 실제로 선형입니다.
가명

1
큰 대답은 누락 된 것으로 보이는 유일한 설명은 간단한 용어로 설명 된 간단한 부분입니다. 프로그램 컴파일은 O (n)에서 수행 할 수 있습니다. 현대의 컴파일러와 마찬가지로 컴파일하기 전에 프로그램을 최적화하는 것은 사실상 무제한의 작업입니다. 실제로 걸리는 시간은 작업의 본질적인 한계에 의해 좌우되는 것이 아니라, 사람들이 기다리는 데 지치기 전에 어느 시점에서 컴파일러를 완료해야하는 실질적인 필요성에 의해 결정됩니다. 항상 타협입니다.
aaaaaaaaaaaa

@Pseudonym, 컴파일러가 정지 문제 (또는 매우 심한 NP 어려운 문제)를 여러 번 해결해야한다는 사실은 표준이 정의되지 않은 동작이 발생하지 않는다고 가정 할 때 컴파일러 작성자에게 여유를주는 이유 중 하나입니다 (무한 루프와 같은 ).
vonbrand

15

실제로 C ++, Lisp 및 D와 같은 일부 언어는 컴파일 타임에 튜링이 완료되므로 일반적으로 컴파일 할 수 없습니다. C ++의 경우 이는 재귀 템플릿 인스턴스화 때문입니다. Lisp 및 D의 경우 컴파일 타임에 거의 모든 코드를 실행할 수 있으므로 원하는 경우 컴파일러를 무한 루프에 넣을 수 있습니다.


3
Haskell (확장자 포함) 및 Scala 유형 시스템도 튜링이 완료되어 유형 확인에 시간이 오래 걸릴 수 있습니다. 스칼라는 이제 튜링 완료 매크로가 맨 위에 있습니다.
Jörg W Mittag 12

5

C # 컴파일러에 대한 실제 경험을 통해 특정 프로그램의 경우 출력 바이너리의 크기가 입력 소스의 크기와 관련하여 기하 급수적으로 증가한다고 말할 수 있습니다 (실제로 C # 사양에 필요하며 줄일 수 없음). 적어도 지수이어야합니다.

C #의 일반적인 과부하 해결 작업은 NP-hard로 알려져 있으며 실제 구현의 복잡성은 적어도 지수입니다.

C # 소스에서 XML 문서 주석을 처리하려면 컴파일 타임에 임의의 XPath 1.0 표현식, 즉 지수 AFAIK도 평가해야합니다.


C # 바이너리가 어떻게 그렇게 날려 줍니까? 나에게 언어 버그처럼 들리지만 ...
vonbrand

1
메타 데이터에서 일반 유형이 인코딩되는 방식입니다. class X<A,B,C,D,E> { class Y : X<Y,Y,Y,Y,Y> { Y.Y.Y.Y.Y.Y.Y.Y.Y y; } }
Vladimir Reshetnikov

-2

일련의 오픈 소스 프로젝트와 같은 현실적인 코드 기반으로이를 측정하십시오. 결과를 (codeSize, finishTime)으로 플로팅하면 해당 그래프를 플로팅 할 수 있습니다. 데이터 f (x) = y가 O (n) 인 경우 g = f (x) / x를 플로팅하면 데이터가 커지기 시작한 후에 직선이 표시됩니다.

f (x) / x, f (x) / lg (x), f (x) / (x * lg (x)), f (x) / (x * x) 등을 플롯합니다. 그래프가 다이빙합니다. 제로로 설정하거나 바운드없이 늘리거나 평평하게합니다. 이 아이디어는 빈 데이터베이스에서 시작하여 삽입 시간을 측정하는 등의 상황에 유용합니다 (예 : 장기간에 걸쳐 '성능 누출'찾기).


1
실행 시간의 경험적 측정은 계산 복잡성을 확립하지 않습니다. 첫째, 계산 복잡성은 가장 일반적으로 최악의 실행 시간으로 표현됩니다. 둘째, 어떤 종류의 평균 사례를 측정하고 싶더라도 입력이 그런 의미에서 "평균"이되어야합니다.
David Richerby

예상치에 불과합니다. 그러나 실제 데이터가 많은 간단한 경험적 테스트 (git repos를 커밋 할 때마다)는 신중한 모델을 능가 할 수 있습니다. 어쨌든 함수가 실제로 O (n ^ 3)이고 f (n) / (n n n) 을 플로팅 하면 경사가 대략 0 인 노이즈 라인이 나타납니다. O (n ^ 3) / (n * n) 만 플로팅하면 선형으로 상승하는 것을 볼 수 있습니다. 라인을 과대 평가하고 빠르게 0으로 다이빙하는 것을 보는 것이 정말 분명합니다.
Rob

1
예를 들어, quicksort는 대부분의 입력 데이터 에서 시간에 실행 되지만 일부 구현에는 최악의 경우 (일반적으로 이미 정렬 된 입력 실행 시간이 있습니다. 그러나 실행 시간을 플롯 하면 케이스보다 케이스에 훨씬 더 많이 될 수 있습니다. Θ(nlogn)Θ(n2)Θ(nlogn)Θ(n2)
David Richerby

침입자가 잘못된 입력을 제공하여 실시간으로 중요한 입력 구문 분석을 수행하여 서비스 거부를 걱정할 경우 알아야 할 사항입니다. 컴파일 시간을 측정하는 실제 함수는 매우 시끄러울 것이고, 우리가 관심을 갖는 경우는 실제 코드 리포지토리에있을 것입니다.
Rob

1
아닙니다. 질문은 문제의 시간 복잡성에 대해 묻습니다. 이는 일반적으로 최악의 실행 시간으로 해석되며 이는 리포지토리의 코드에서 실행 시간이 아닙니다. 제안한 테스트는 컴파일러가 주어진 코드 조각을 가져 오는 데 걸리는 시간을 합리적으로 처리합니다. 이는 유용하고 유용한 것입니다. 그러나 그들은 문제의 계산 복잡성에 대해 거의 아무것도 말하지 않습니다.
David Richerby
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.