현대 컴파일러에서 제네릭은 어떻게 구현됩니까?


15

여기서 의미하는 바는 일부 템플릿 T add(T a, T b) ...에서 생성 된 코드로 어떻게 이동합니까? 나는 이것을 달성 할 수있는 몇 가지 방법을 생각했다. 우리는 일반 함수를 AST에 저장 Function_Node한 다음 그것을 사용할 때마다 원래 함수 노드에 자체 유형의 사본으로 저장된 모든 유형의 사본을 원래 함수 노드에 저장 T한다. 사용 중입니다. 예를 들어 add<int>(5, 6)대한 일반적인 기능의 사본을 저장합니다 add및 모든 종류의 대체 T 사본에int.

따라서 다음과 같이 보일 것입니다.

struct Function_Node {
    std::string name; // etc.
    Type return_type;
    std::vector<std::pair<Type, std::string>> arguments;
    std::vector<Function_Node> copies;
};

그런 다음 이들에 대한 코드를 생성 할 수 Function_Node있으며 사본 목록 을 방문하면 모든 사본 copies.size() > 0을 호출 visitFunction합니다.

visitFunction(Function_Node& node) {
    if (node.copies.size() > 0) {
        for (auto& node : nodes.copies) {
            visitFunction(node);
        }
        // it's a generic function so we don't want
        // to emit code for this.
        return;
    }
}

이것이 잘 작동합니까? 최신 컴파일러는이 문제에 어떻게 접근합니까? 아마도이 작업을 수행하는 또 다른 방법은 사본을 AST에 주입하여 모든 의미 단계를 거치도록 할 수 있다고 생각합니다. 예를 들어 Rust의 MIR 또는 Swifts SIL과 같은 즉각적인 형태로 생성 할 수 있다고 생각했습니다.

내 코드는 Java로 작성되었으며 여기 예제는 C ++입니다. 예제에 대한 설명은 조금 덜 장황하지만 원칙은 기본적으로 동일합니다. 질문 상자에 손으로 작성 되었기 때문에 약간의 오류가있을 수 있습니다.

이 문제에 접근하는 가장 좋은 방법과 마찬가지로 최신 컴파일러를 의미합니다. 그리고 제네릭을 말할 때 유형 삭제를 사용하는 Java 제네릭과 같은 의미는 아닙니다.


C ++ (다른 프로그래밍 언어에는 제네릭이 있지만 각각 다르게 구현)에서는 기본적으로 거대한 컴파일 타임 매크로 시스템입니다. 실제 코드는 대체 유형을 사용하여 생성됩니다.
Robert Harvey

왜 지우개를 입력하지 않습니까? Java뿐만 아니라 요구 사항에 따라 나쁜 기술이 아닙니다.
Andres F.

@AndresF. 언어가 작동하는 방식을 고려하면 제대로 작동하지 않을 것이라고 생각합니다.
Jon Flow

2
어떤 종류의 제네릭을 이야기하고 있는지 분명히해야한다고 생각합니다. 예를 들어 C ++ 템플릿, C # 제네릭 및 Java 제네릭은 모두 서로 매우 다릅니다. 당신은 당신이 자바 제네릭을 의미하는 것이 아니라, 당신이 의미하는 것을 말하지는 않습니다.
svick

2
지나치게 광범위하지 않게하려면 한 언어 시스템에 중점을 두어야합니다.
Daenyth

답변:


14

현대 컴파일러에서 제네릭은 어떻게 구현됩니까?

최신 컴파일러의 작동 방식을 알고 싶다면 최신 컴파일러의 소스 코드를 읽으십시오. C # 및 Visual Basic 컴파일러를 구현하는 Roslyn 프로젝트부터 시작하겠습니다.

특히 형식 기호를 구현하는 C # 컴파일러의 코드에주의를 기울입니다.

https://github.com/dotnet/roslyn/tree/master/src/Compilers/CSharp/Portable/Symbols

또한 변환 규칙에 대한 코드를보고 싶을 수도 있습니다. 제네릭 형식의 대수적 조작과 관련된 내용이 많이 있습니다.

https://github.com/dotnet/roslyn/tree/master/src/Compilers/CSharp/Portable/Binder/Semantics/Conversions

나는 후자를 읽기 쉽도록 노력했다.

이것을 달성하는 몇 가지 방법을 생각했습니다. 일반 함수를 AST에 Function_Node로 저장 한 다음 사용할 때마다 원래 함수 노드에 모든 유형 T로 대체 된 자체 사본을 저장합니다. 사용되고 있습니다.

제네릭이 아닌 템플릿 을 설명하고 있습니다. C # 및 Visual Basic은 형식 시스템에 실제 제네릭이 있습니다.

간단히, 그들은 이렇게 작동합니다.

  • 컴파일 타임에 형식적으로 형식을 구성하는 규칙을 설정하는 것으로 시작합니다. 예를 들면 다음과 같습니다. intis a type, type parameter Tis a type, any typeX 에 대해 배열 유형 X[]도 유형 입니다.

  • 제네릭 규칙에는 대체가 포함됩니다. 예를 들어 class C with one type parameter유형이 아닙니다. 유형을 만들기위한 패턴입니다. class C with one type parameter called T, under substitution with int for T 이다 일종.

  • 형식 간 관계를 설명하는 규칙 (할당시 호환성, 식 형식 결정 방법 등)은 컴파일러에서 설계 및 구현됩니다.

  • 메타 데이터 시스템에서 일반 유형을 지원하는 바이트 코드 언어가 설계되고 구현됩니다.

  • 런타임시 JIT 컴파일러는 바이트 코드를 기계 코드로 변환합니다. 일반 전문화가 주어지면 적절한 기계 코드를 구성해야합니다.

예를 들어 C #에서는 말할 때

class C<T> { public void X(T t) { Console.WriteLine(t); } }
...
var c = new C<int>(); 
c.X(123);

그런 다음 컴파일러는에서 C<int>인수 int가 유효한 대체인지 확인하고 T그에 따라 메타 데이터 및 바이트 코드를 생성합니다. 런타임시 지터는 a C<int>가 처음으로 생성되고 있음을 감지 하고 적절한 머신 코드를 동적으로 생성합니다.


9

제네릭 (또는 파라 메트릭 다형성)의 대부분의 구현은 유형 삭제를 사용합니다. 이렇게하면 일반 코드를 컴파일하는 문제가 크게 단순화되지만 박스 형식에만 적용됩니다. 각 인수는 사실상 불투명 포인터이므로 인수에 대한 작업을 수행하려면 VTable 또는 이와 유사한 디스패치 메커니즘이 필요합니다. 자바에서 :

<T extends Addable> T add(T a, T b) { … }

컴파일하고 형식을 확인하고 다음과 같은 방식으로 호출 할 수 있습니다.

Addable add(Addable a, Addable b) { … }

제외시켰다 제네릭이 전화 사이트에서 훨씬 더 많은 정보 유형 검사를 제공하고 있다고. 이 추가 정보는 특히 제네릭 형식이 유추 될 때 형식 변수 로 처리 할 수 ​​있습니다 . 타입 검사 동안, 각각의 제네릭 타입은 변수로 대체 될 수 있습니다 $T1.

$T1 add($T1 a, $T1 b)

그런 다음 구체적인 변수로 대체 될 수있을 때까지 유형 변수가 더 많은 사실로 업데이트됩니다. 타입 검사 알고리즘은 이러한 타입 변수가 아직 완전한 타입으로 해석되지 않더라도 수용 할 수있는 방식으로 작성되어야합니다. Java 자체에서는 보통 함수 호출 유형을 알기 전에 인수 유형을 알기 때문에 일반적으로 쉽게 수행 할 수 있습니다. 주목할만한 예외는 함수 인수와 같은 람다 식이며 이러한 유형 변수를 사용해야합니다.

훨씬 나중에 최적화 프로그램 특정 인수 집합에 대해 특수 코드를 생성 할 수 있으며 , 이는 사실상 일종의 인라인이됩니다.

일반 함수가 형식에 대한 작업을 수행하지 않고 다른 함수에만 전달하는 경우 일반 형식 인수에 대한 VTable을 피할 수 있습니다. 예를 들어 Haskell 함수 call :: (a -> b) -> a -> b; call f x = f xx인수 를 상자에 넣을 필요가 없습니다 . 그러나 크기를 모른 채 값을 통과 할 수있는 호출 규칙이 필요하므로 기본적으로 포인터로 제한됩니다.


C ++은 이런 점에서 대부분의 언어와는 매우 다릅니다. 템플릿 클래스 또는 함수 (여기서는 템플릿 함수에 대해서만 설명하겠습니다) 자체로는 호출 할 수 없습니다. 대신 템플릿은 실제 함수를 반환하는 컴파일 타임 메타 함수로 이해해야합니다. 잠시 동안 템플릿 인수 유추를 무시하면 일반적인 접근 방식은 다음 단계로 요약됩니다.

  1. 제공된 템플리트 인수에 템플리트를 적용하십시오. 예는 전화 template<class T> T add(T a, T b) { … }로하는 것은 add<int>(1, 2)우리에게 실제 기능을 줄 것이다 int __add__T_int(int a, int b)(이름 엉망으로 접근 방식을 사용 또는 무엇이든).

  2. 해당 함수에 대한 코드가 현재 컴파일 단위에서 이미 생성 된 경우 계속하십시오. 그렇지 않으면 함수 int __add__T_int(int a, int b) { … }가 소스 코드에 작성된 것처럼 코드를 생성하십시오 . 여기에는 모든 템플리트 인수가 해당 값으로 대체됩니다. 아마도 AST → AST 변환 일 것입니다. 그런 다음 생성 된 AST에서 유형 검사를 수행하십시오.

  3. 소스 코드처럼 호출을 컴파일하십시오 __add__T_int(1, 2).

C ++ 템플릿은 여기에서는 설명하고 싶지 않은 과부하 해결 메커니즘과 복잡한 상호 작용을합니다. 또한이 코드 생성을 통해 템플릿 방식을 가상으로 만들 수 없습니다. 유형 삭제 기반 접근 방식에는 이러한 실질적인 제한이 없습니다.


이것이 컴파일러 및 / 또는 언어에 어떤 의미가 있습니까? 제공하려는 제네릭의 종류에 대해 신중하게 생각해야합니다. 박스형 유형을 지원하는 경우 유형 유추가없는 유형 삭제가 가장 간단한 방법입니다. 템플릿 전문화는 상당히 단순 해 보이지만 일반적으로 템플릿이 정의 사이트가 아닌 호출 사이트에서 인스턴스화되므로 이름 변경 및 여러 컴파일 단위의 경우 상당한 출력 복제가 필요합니다.

당신이 보여준 접근법은 본질적으로 C ++와 같은 템플릿 접근법입니다. 그러나 특수 / 인스턴스화 된 템플릿을 기본 템플릿의 "버전"으로 저장합니다. 이것은 오해의 소지가 있습니다. 그것들은 개념적으로 동일하지 않으며 함수의 다른 인스턴스화는 완전히 다른 유형을 가질 수 있습니다. 함수 오버로드를 허용하면 장기적으로 복잡해집니다. 대신, 이름을 공유하는 가능한 모든 기능과 템플릿을 포함하는 과부하 세트에 대한 개념이 필요합니다. 오버로드 해결을 제외하고는 서로 다른 인스턴스화 된 템플릿이 완전히 분리 된 것으로 간주 할 수 있습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.