내 언어를 C 코드로 먼저 컴파일하는 것이 언제 합리적입니까?


34

자체 프로그래밍 언어를 설계 할 때 gcc와 같은 기존 컴파일러를 사용하여 기계 코드로 끝날 수 있도록 소스 코드를 가져 와서 C 또는 C ++ 코드로 변환하는 변환기를 작성하는 것이 언제 합리적입니까? 이 접근법을 사용하는 프로젝트가 있습니까?



4
C를 살펴보면 C #과 Java도 중간 언어로 컴파일된다는 것을 알 수 있습니다. 다른 사람이 이미 수행 한 많은 작업을 다시 수행해야하는 번거 로움을 덜 수 있습니다.
Casey

1
@emodendroket 그러나 C # 및 Java는 일반적으로 IL로 설계되고 C # / Java를 위해 특별히 설계된 IL로 컴파일되므로 CIL 및 JVM 바이트 코드는 C보다 훨씬 현명하고 편리합니다. 중급 언어 사용 여부가 아니라 중급 언어 사용 여부에 관한 것입니다.

1
C 코드를 생성하는 몇 가지 무료 소프트웨어 구현을 살펴보십시오. 그리고 언어 구현이 자유 소프트웨어가되기를 바랍니다.
Basile Starynkevitch

2
다음은 @RobertHarvey의 의견에서 업데이트 된 링크입니다 : yosefk.com/blog/c-as-an-intermediate-language.html .
Christian Dean

답변:


52

C 코드로 번역하는 것은 매우 잘 알려진 습관입니다. 클래스가있는 원래 C와 초기 C ++ 구현 ( Cfront 라고 함 )이 성공적으로 수행했습니다. Lisp 또는 Scheme의 몇 가지 구현, 예를 들어 Chicken Scheme , Scheme48 , Bigloo가 있습니다. 어떤 사람들은 Prolog를 C로 번역했습니다 . 그리고 Mozart의 일부 버전도 마찬가지였습니다 (그리고 Ocaml 바이트 코드를 C 로 컴파일하려고 시도했습니다 ). J.Pitrat의 인공 지능 CAIA 시스템 도 부트 스트랩되어 모든 C 코드를 생성합니다. Vala 는 GTK 관련 코드를 위해 C로도 변환합니다. Queinnec의 저술 Lisp In Small Pieces C 로의 번역에 관한 장이 있습니다.

C로 변환 할 때 발생하는 문제 중 하나는 꼬리 재귀 호출 입니다. C 표준은 (는 "인수 점프"에, 즉 C 컴파일러가 제대로 번역되어 있지 보증을하지 않고 의 경우에도 호출 스택을 먹고) 일부 의 경우, GCC (또는 연타의 / LLVM)의 최신 버전은 그 최적화를 할 .

또 다른 문제는 가비지 수집 입니다. 여러 구현에서는 Boehm 보수 가비지 수집기 ( C 친화적 인 ...)를 사용합니다. 악의적 일 수있는 코드를 가비지 수집 (예 : SBCL과 같은 여러 Lisp 구현과 같이)하려는 경우 ( dlclosePosix에서 원합니다 ).

또 다른 문제는 일류 연속call / cc 처리하는 것 입니다. 그러나 영리한 트릭이 가능합니다 (치킨 계획 내부를보십시오). 콜 스택에 액세스하려면 많은 트릭이 필요할 수 있습니다 (그러나 GNU backtrace 등 참조 ). 연속체 (즉, 스택 또는 스레드)의 직교 지속성 은 C에서 어렵다.

예외 처리는 종종 longjmp 등을 영리하게 호출하는 문제입니다 .

적절한 #line지시문 을 생성 (방출 된 C 코드로) 할 수 있습니다. 이것은 지루하고 많은 작업이 필요합니다 (예 : 더 쉽게 gdb디버그 가능한 코드를 생성하는 것이 좋습니다).

MELT Lisp 다운 도메인 특정 언어는 (사용자 정의하거나 확장하는 GCC를 ) (지금 ++ 실제로 가난한 C까지) C로 변환됩니다. 자체 생성 가비지 수집기가 있습니다. ( Qish 또는 Ravenbrook MPS에 관심이있을 수 있습니다 ). 실제로, 세대 별 GC는 수작업으로 작성된 C 코드보다 기계로 생성 된 C 코드에서 더 쉽습니다 (쓰기 장벽 및 GC 기계에 맞게 C 코드 생성기를 조정하기 때문에).

나는 진정한 C ++ 코드로 번역하는 언어 구현을 모른다 . 즉, 많은 "STL 템플릿을 사용하고 RAII 관용구를 존중하는 C ++ 코드를 생성하는"컴파일 타임 가비지 수집 "기술을 사용한다 . (알고 있다면 알려주세요).

오늘날 재미있는 것은 (현재 Linux 데스크톱에서) C 컴파일러는 C로 변환 된 대화식 최상위 읽기-판독-인쇄-루프 를 구현하기에 충분히 빠를 수 있다는 것입니다. 모든 사용자에서 C 코드 (수백 줄)를 방출합니다 상호 작용할 fork때 공유 객체로 컴파일 한 다음 dlopen. (MELT는 모든 것을 준비하고 있으며 일반적으로 충분히 빠릅니다). 이 모든 것이 수십 분의 1 초가 걸리고 최종 사용자가 수용 할 수 있습니다.

가능하면 C ++ 컴파일이 느리기 때문에 C ++가 아닌 C로 변환하는 것이 좋습니다.

언어를 구현하는 경우 libjit , GNU lightning , asmjit 또는 LLVM 또는 GCCJIT 와 같은 일부 JIT 라이브러리를 C 코드를 내보내는 대신 고려할 수도 있습니다 . C로 번역하고 싶다면 때때로 tinycc를 사용할 수 있습니다 . 생성 된 C 코드 (메모리에서도)를 매우 빠르게 컴파일하여 머신 코드 를 느리게 합니다. 그러나 일반적으로 GCC 와 같은 실제 C 컴파일러가 수행 하는 최적화를 활용하려고합니다.

C 언어로 번역하는 경우 생성 된 C 코드 의 전체 AST 를 메모리에 먼저 빌드해야합니다 (이렇게하면 모든 선언, 모든 정의 및 함수 코드를 먼저 생성하는 것이 더 쉬워집니다). 이런 식으로 일부 최적화 / 정규화를 수행 할 수 있습니다. 또한 여러 GCC 확장 (예 : 계산 된 gotos)에 관심이있을 수 있습니다 . C 컴파일러를 최적화하는 것은 매우 큰 C 함수에 실제로 불만족하기 때문에 거대한 C 함수 (예 : 수십만 줄의 생성 된 C)를 생성 하지 않으려 할 것입니다 (실제로 실험적으로gcc -O큰 함수의 컴파일 시간은 함수 코드 크기의 제곱에 비례합니다). 따라서 생성 된 C 함수의 크기를 각각 수천 줄로 제한하십시오.

공지 사항이 모두 연타 (통해 LLVM 과) GCC (를 통해 libgccjit C & C는 ++ 컴파일러이 컴파일러에 적합 일부 내부 표현을 방출 할 수있는 방법을 제공하지만, 힘이 (여부) 열심히 C (또는 C ++) 코드를 방출보다 수 있도록하고) 각 컴파일러마다 다릅니다.

C로 번역 될 언어를 디자인하는 경우, 언어와 C의 혼합을 생성하는 몇 가지 트릭 (또는 구성)이 필요할 수 있습니다. DSL2011 논문 MELT : GCC 컴파일러에 포함 된 번역 된 도메인 특정 언어가 유용한 힌트를 제공해야합니다.


"치킨 제도"를 언급하고 있습니까?
Robert Harvey

1
예. URL을 제공했습니다.
Basile Starynkevitch

Java 또는 다른 것과 같은 가상 시스템을 바이트 코드를 C로 컴파일 한 다음 gcc를 사용하여 JIT 컴파일하는 것이 상대적으로 실용적입니까? 아니면 바이트 코드에서 어셈블리로 바로 가야합니까?
Panzercrisis

1
@Panzercrisis 대부분의 JIT 컴파일러는 함수를 바꾸고 기존 코드를 점프 / 트랩 도어로 패치하는 것과 같은 기능을 지원하기 위해 머신 코드 백엔드가 필요합니다. 그 외에도 gcc는 JIT 컴파일 및 기타 사용 사례에 적합하지 않습니다. 그래도 libgccjit를 확인하십시오 : gcc.gnu.org/ml/gcc-patches/2013-10/msg00228.htmlgcc.gnu.org/wiki/JIT

1
훌륭한 오리엔테이션 소재. 감사!
capr

7

전체 머신 코드를 생성하는 시간이 C 컴파일러를 사용하여 "IL"을 머신 코드로 컴파일하는 중간 단계를 수행해야하는 불편 함을 능가하는 것이 합리적입니다.

일반적으로 도메인 별 언어는 이러한 방식으로 작성되며, 실행 가능한 파일 또는 dll로 컴파일되는 프로세스를 정의하거나 설명하는 데 매우 높은 수준의 시스템이 사용됩니다. 작업 / 양호한 어셈블리를 생성하는 데 걸리는 시간은 C를 생성하는 것보다 훨씬 길며 C는 성능을 위해 어셈블리 코드와 매우 유사하므로 C를 생성하고 C 컴파일러 작성자의 기술을 재사용하는 것이 좋습니다. gcc 또는 llvm을 작성하는 사람들은 최적화 된 기계어 코드를 만드는 데 많은 시간을 들였으므로 모든 노력을 다시 시도하는 것은 어려울 것입니다.

IIRC가 언어 중립적 인 LLVM의 컴파일러 백엔드를 재사용하는 것이 더 받아 들일 수 있으므로 C 코드 대신 LLVM 명령어를 생성합니다.


라이브러리가 그것을 고려해야 할 꽤 매력적인 이유 인 것 같습니다.
Casey

당신의 "IL"이라고 말할 때, 당신은 무엇을 말하는가? 추상 구문 트리?
Robert Harvey

@RobertHarvey 아니오, 나는 C 코드를 의미합니다. OP의 경우, 이것은 자신의 고급 언어와 기계어 코드의 중간 언어입니다. 나는 많은 사람들이 사용하는 IL이 아닌 IL이라는 아이디어를 인용하고 인용했다. (예 : Microsoft의 .NET IL)
gbjbaanb

2

머신 코드를 생성하기 위해 컴파일러를 작성하는 것은 C를 생성하는 컴파일러를 작성하는 것 (어쩌면 더 쉬울 수 있음)을 작성하는 것보다 훨씬 어렵지 않지만 머신 코드를 생성하는 컴파일러는 특정 플랫폼에서 실행 가능한 프로그램 만 생성 할 수 있습니다. 그것은 쓰여졌다; 대조적으로 C 코드를 생성하는 컴파일러는 생성 된 코드가 지원하도록 설계된 C의 방언을 사용하는 모든 플랫폼에 대한 프로그램을 생성 할 수 있습니다. 많은 경우 C 표준으로 보장되지 않는 동작을 사용하지 않고 완전히 이식 가능하고 원하는대로 동작하는 C 코드를 작성할 수 있지만 플랫폼 보장 동작에 의존하는 코드는 훨씬 빠르게 실행될 수 있습니다. 그렇지 않은 코드보다 보장하는 플랫폼에서.

예를 들어, 언어가 빅 엔디안 방식으로 해석되어 UInt32임의로 정렬 된 4 개의 연속 바이트 를 생성하는 기능을 지원한다고 가정합니다 UInt8[]. 일부 컴파일러에서는 다음과 같이 코드를 작성할 수 있습니다.

uint32_t dat = *(__packed uint32_t*)p;
return (dat >> 24) | (dat >> 8) | ((uint32_t)dat << 8) | ((uint32_t)dat << 24));

컴파일러가 워드로드 작업을 수행 한 다음 워드 단위의 바이트 단위 명령을 생성하도록합니다. 그러나 일부 컴파일러는 __packed 수정자를 지원하지 않으며 부재시 작동하지 않는 코드를 생성합니다.

또는 코드를 다음과 같이 작성할 수 있습니다.

return dat[3] | ((uint16_t)dat[2] << 8) | ((uint32_t)dat[1] << 16) | ((uint32_t)dat[0] << 24);

이러한 코드는 CHAR_BITS8이 아닌 소스 플랫폼 의 소스 데이터의 각 8 진수가 별개의 배열 요소로 가정된다고 가정 하더라도 모든 플랫폼에서 작동해야 하지만 이러한 코드는 이식성이없는 것만 큼 빠르게 실행되지 않을 수 있습니다. 전자를 지원하는 플랫폼의 버전.

이식성은 종종 코드가 타입 캐스트 및 유사한 구조에서 매우 자유로울 것을 요구합니다. 예를 들어, 두 개의 32 비트 부호없는 정수를 곱하고 결과의 하위 32 비트를 생성하려는 코드는 이식성을 위해 다음과 같이 작성해야합니다.

uint32_t result = 1u*x*y;

그것 없이는 1uINT_BITS가 33에서 64까지 인 시스템의 컴파일러는 x와 y의 곱이 2,147,483,647보다 크면 합법적으로 원하는 것을 수행 할 수 있으며 일부 컴파일러는 그러한 기회를 이용하기 쉽습니다.


1

위의 몇 가지 훌륭한 답변이 있지만, 한 의견에서 "왜 처음에 자신 ​​만의 프로그래밍 언어를 만들고 싶습니까?"라는 질문에 대답했습니다. "주로 학습 목적이 될 것입니다." 다른 각도에서 대답하겠습니다.

어휘, 구문 및 언어에 대한 학습에 더 관심이있는 경우 소스 코드를 가져 와서 C 또는 C ++ 코드로 변환하는 변환기를 작성하여 gcc와 같은 기존 컴파일러를 사용하여 기계 코드로 끝낼 수 있습니다. 코드 생성 및 최적화에 대해 배우는 것보다 의미 분석!

자신의 머신 코드 생성기를 작성하는 것은 C 코드로 컴파일하여 주로 관심이없는 경우 피할 수있는 매우 중요한 작업입니다!

그러나 어셈블리 프로그램에 참여하여 코드를 가장 낮은 수준에서 최적화해야하는 문제에 매료되면 학습 경험을 위해 직접 코드 생성기를 작성하십시오!


-7

Windows를 사용하는 경우 사용하는 운영 체제에 따라 코드를 중간 언어로 변환하여 기계 코드로 컴파일하는 데 시간이 걸리지 않는 Microsoft IL (중급 언어)이 있습니다. 또는 Linux를 사용하는 경우 별도의 컴파일러가 있습니다.

당신의 질문으로 되돌아가는 것은 당신이 당신의 언어를 디자인 할 때 기계가 높은 수준의 언어를 알지 못하기 때문에 별도의 컴파일러 나 인터프리터를 가져야합니다. 기계에 유용하도록 코드를 기계 코드로 컴파일해야합니다.


2
Your code should be compiled into machine code to make it useful for machine-컴파일러가 c 코드를 출력으로 생성 한 경우 c 코드를 ac 컴파일러에 넣어 기계 코드를 생성 할 수 있습니다.
Robert Harvey

예. 기계는 c 언어가 아니기 때문에
Tayyab Gulsher Vohra

2
권리. 따라서 문제는 "기계 언어 나 바이트 코드를 직접 방출하는 대신 c를 방출하고 ac 컴파일러를 사용하는 것이 언제 합리적입니까?"였습니다.
Robert Harvey

실제로 그는 "C 또는 C ++ 코드로 변환 할 것을 요구하는"프로그래밍 언어를 설계하도록 요구하고있다. 그래서 당신이 왜 자신의 프로그래밍 언어를 디자인하고 있다면 왜 당신이 c 컴파일러 또는 c ++를 사용 해야하는지 설명하고 있습니다. 당신이 충분히 똑똑하다면 당신은 당신 자신을 디자인해야합니다
Tayyab Gulsher Vohra

8
나는 당신이 그 질문을 이해하지 못한다고 생각합니다. 참조 yosefk.com/blog/c-as-an-intermediate-language.html
로버트 하비에게
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.