바이트 코드와 머신 코드로의 컴파일


13

기계 코드로 "완벽하게"이동하는 대신 중간 바이트 코드 (Java와 같은)를 생성하는 컴파일이 일반적으로 복잡성이 줄어듦 (따라서 시간이 덜 걸리는가)?

답변:


22

예, Java 바이트 코드로 컴파일하는 것이 머신 코드로 컴파일하는 것보다 쉽습니다. 이는 부분적으로 대상으로 지정할 형식이 하나뿐이기 때문입니다 (Mandrill이 언급했듯이 컴파일 시간이 아니라 컴파일러 복잡성을 감소시키기 만 함). 부분적으로 JVM은 실제 CPU보다 훨씬 단순하고 프로그래밍하기에 편리합니다. Java 언어와 함께 대부분의 Java 작업은 매우 간단한 방식으로 정확히 하나의 바이트 코드 작업에 매핑됩니다. 또 다른 중요한 이유는 거의 없습니다 최적화가 이루어집니다. 거의 모든 효율성 문제는 JIT 컴파일러 (또는 JVM 전체)에 맡겨져 있으므로 일반 컴파일러의 전체 중간 끝이 사라집니다. 기본적으로 AST를 한 번 살펴보고 각 노드에 대해 기성품 바이트 코드 시퀀스를 생성 할 수 있습니다. 메소드 테이블, 상수 풀 등을 생성하는 데 약간의 "관리 오버 헤드"가 있지만 LLVM의 복잡성과 비교할만한 것은 없습니다.


"... 중간 끝 ..."이라고 썼습니다. "... 중간에서 끝까지 ..."를 의미 했습니까? 아니면 "... 중간 부분 ..."?
Julian A.

6
@Julian "중간 끝" "프런트 엔드"와 "백 엔드": 의미 없음 관련과 유사하게 만들어 낸 진정한 용어입니다

7

컴파일러는 단순히 사람이 읽을 수있는 1 개의 텍스트 파일을 컴퓨터의 이진 명령어로 변환 하는 프로그램 입니다. 한 걸음 물러서서이 이론적 관점에서 질문에 대해 생각해 보면 복잡성은 거의 같습니다. 그러나보다 실용적인 수준에서는 바이트 코드 컴파일러가 더 간단합니다.

프로그램을 컴파일하려면 어떤 단계를 거쳐야합니까?

  1. 소스 코드 스캔, 파싱 및 유효성 검사
  2. 소스를 추상 구문 트리로 변환
  3. 선택 사항 : 언어 사양에서 허용하는 경우 AST 처리 및 개선 (예 : 데드 코드 제거, 재정렬 작업, 기타 최적화)
  4. 기계가 이해하는 형태로 AST 변환

둘 사이에는 두 가지의 실제 차이점 만 있습니다.

  • 일반적으로 컴파일 단위가 여러 개인 프로그램은 머신 코드로 컴파일 할 때 링크해야하며 일반적으로 바이트 코드가 아닙니다. 이 질문의 맥락에서 링크가 컴파일의 일부인지에 대해 머리카락을 나눌 수 있습니다. 그렇다면 바이트 코드 컴파일이 약간 더 간단 해집니다. 그러나 VM에서 많은 연결 문제를 처리 할 때 런타임에 연결이 복잡해집니다 (아래 참고 참조).

  • 바이트 코드 컴파일러는 VM이 ​​즉시이를 더 잘 수행 할 수 있기 때문에 최적화하지 않는 경향이 있습니다 (JIT 컴파일러는 현재 VM에 상당히 표준으로 추가되어 있습니다).

이것으로부터 바이트 코드 컴파일러는 대부분의 최적화와 모든 연결의 복잡성을 생략하여 VM 런타임에 대한 두 가지를 모두 지연시킬 수 있다고 결론을 내립니다. 바이트 코드 컴파일러는 머신 코드 컴파일러가 스스로 수행하는 VM에 많은 복잡성을 삽니다.

1 난해한 언어를 세지 마십시오


3
최적화를 무시하는 것은 바보입니다. 이러한 "선택적 단계"는 대부분의 컴파일러의 코드 기반, 복잡성 및 컴파일 시간을 상당 부분 구성합니다.

실제로는 맞습니다. 나는 학업을 엉망으로 만들었고 대답을 업데이트했습니다.

실제로 최적화를 금지하는 언어 사양이 있습니까? 일부 언어는 어려움을 겪지 만 처음부터 시작할 수는 없다는 것을 알고 있습니다.
Davidmh 2016 년

@Davidmh 나는 그들을 금지 하는 사양을 모른다 . 내 이해는 대부분 컴파일러가 허용되지만 자세하게 설명하지는 않는다는 것입니다. 많은 최적화는 일반적으로 CPU, OS 및 대상 아키텍처의 세부 사항에 의존하기 때문에 각 구현은 다릅니다. 이러한 이유로 바이트 코드 컴파일러는 기본 아키텍처를 알고있는 VM에이를 최적화하고 대신 펀칭 할 가능성이 적습니다.

4

컴파일은 항상 Java에서 일반 가상 머신 코드이기 때문에 컴파일러 디자인을 단순화한다고 말합니다. 즉, 코드를 한 번만 컴파일하면 각 시스템에서 컴파일하지 않고 모든 플랫폼에서 실행됩니다. 표준화 된 머신과 같은 가상 머신을 고려할 수 있으므로 컴파일 시간이 더 짧은 지 확실하지 않습니다.

다른 한편으로, 각 머신은 자바 바이트 머신 (Java Virtual Machine)을로드해야하므로 "바이트 코드"(Java 코드 컴파일의 결과 인 가상 머신 코드)를 해석하고 실제 머신 코드로 변환하여 실행할 수 있습니다. .

Imo 이것은 큰 프로그램에는 좋지만 작은 프로그램에는 좋지 않습니다 (가상 머신은 메모리 낭비이기 때문에).


내가 참조. 따라서 바이트 코드를 표준 시스템 (예 : JVM)에 매핑하는 복잡성이 소스 코드를 물리적 시스템에 매핑하는 복잡성과 일치한다고 생각하여 바이트 코드가 컴파일 시간을 단축시킬 것이라고 생각할 이유가 없습니까?
Julian A.

그것은 내가 말한 것이 아닙니다. Java 코드를 바이트 코드 (Virtual Machine Assembler)에 매핑하면 소스 코드 (Java)를 실제 기계 코드에 매핑하는 것과 일치한다고 말했습니다.
Mandrill

3

컴파일의 복잡성은 소스 언어와 대상 언어 사이의 의미 적 차이와이 차이를 메우면서 적용하려는 최적화 수준에 크게 좌우됩니다.

예를 들어 Java 소스 코드를 JVM 바이트 코드로 컴파일하는 것은 비교적 간단합니다. Java의 핵심 서브 세트가 JVM 바이트 코드의 서브 세트에 거의 직접적으로 맵핑되기 때문입니다. 몇 가지 차이점이 있습니다 .Java에는 루프가 있지만 아니오 GOTO, JVM에는 GOTO루프 가 없지만 Java에는 제네릭이 있으며 JVM은 그렇지 않지만 쉽게 처리 할 수 ​​있습니다 (루프에서 조건부 점프로의 변환은 사소하고 유형 소거는 약간 적습니다) 여전히 관리 가능합니다). 다른 차이점이 있지만 덜 심각합니다.

(특히 전에 JVM 바이트 코드에 루비 소스 코드를 컴파일하는 것은 훨씬 더 복잡 invokedynamic하고는 MethodHandlesJVM을 사양의 제 3 판에 더 정확하게 자바 7에 도입, 또는했다). Ruby에서는 런타임에 메소드를 바꿀 수 있습니다. JVM에서 런타임시 대체 할 수있는 가장 작은 코드 단위는 클래스이므로 Ruby 메소드는 JVM 메소드가 아닌 JVM 클래스로 컴파일되어야합니다. Ruby 메소드 디스패치가 JVM 메소드 디스패치와 일치하지 않으며 이전 invokedynamic에는 자체 메소드 디스패치 메커니즘을 JVM에 삽입 할 방법이 없었습니다. 루비에는 연속체와 코 루틴이 있지만 JVM에는이를 구현할 기능이 없습니다. (JVM의GOTO JVM이 가지고있는 유일한 제어 흐름 기본 요소로서, 연속성을 구현하기에 충분히 강력한 예외는 예외이며 코 루틴 스레드를 구현하는 데 매우 강력하지만 코 루틴의 전체 목적은 매우 가볍습니다.

Ruby 소스 코드를 Rubinius 바이트 코드 또는 YARV 바이트 코드로 컴파일하는 OTOH는 루비의 컴파일 대상으로 명시 적으로 설계 되었기 때문에 사소한 문제입니다. .

마찬가지로 x86 기본 코드를 JVM 바이트 코드로 컴파일하는 것은 간단하지 않으며, 의미 상 큰 차이가 있습니다.

Haskell은 또 다른 좋은 예입니다. Haskell에는 기본 x86 기계 코드를 생성하는 고성능의 산업용 강력한 프로덕션 지원 컴파일러가 몇 가지 있지만 현재까지 JVM 또는 CLI 용으로 작동하는 컴파일러는 없습니다. 간격이 너무 커서 브리지하기가 매우 복잡합니다. 따라서 이것은 원시 머신 코드로의 컴파일이 JVM 또는 CIL 바이트 코드로 컴파일하는 것보다 실제로 복잡한 예입니다. 원시 머신 코드에는 GOTO메소드 호출이나 예외와 같은 상위 레벨 기본 요소를 사용하는 것보다 원하는 것을 수행하기가 더 "강제적"일 수있는 훨씬 낮은 레벨의 기본 요소 ( , 포인터 등) 가 있기 때문입니다 .

따라서 대상 언어가 높을수록 컴파일러의 복잡성을 줄이기 위해 소스 언어의 의미와 더 밀접하게 일치해야한다고 말할 수 있습니다.


0

실제로 오늘날 대부분의 JVM은 JIT 컴파일을 수행하는 매우 복잡한 소프트웨어 입니다 (따라서 바이트 코드는 JVM에 의해 기계 코드로 동적으로 변환됩니다).

따라서 Java 소스 코드 (또는 Clojure 소스 코드)에서 JVM 바이트 코드로의 컴파일이 실제로는 간단하지만 JVM 자체는 기계 코드로 복잡한 변환을 수행합니다.

JVM 내에서이 JIT 변환이 동적이기 때문에 JVM이 바이트 코드의 가장 관련성있는 부분에 집중할 수 있습니다. 실제로, 대부분의 JVM은 JVM 바이트 코드의 가장 핫한 부분 (예 : 가장 많이 호출 된 메소드 또는 가장 많이 실행 된 기본 블록)을 최적화합니다.

JVM + Java와 바이트 코드 컴파일러 의 결합 된 복잡성이 사전 컴파일러의 복잡성보다 훨씬 적다는 것은 확실하지 않습니다 .

(같은 대부분의 전통적인 컴파일러도 있음을주의 GCC 또는 연타 / LLVM은 ) 내부 표현 (로 (C ++, 또는 에이다, ... 또는) 소스 코드를 입력 C를 변환하는 Gimple GCC에 대한 LLVM 매우 유사 연타에 대한) 일부 바이트 코드. 그런 다음 내부 표현 (먼저 그것을 최적화하는 것, 즉 대부분의 GCC 최적화 패스는 Gimple을 입력으로 사용하고 Gimple을 출력으로 생성하고 나중에 어셈블러 또는 기계 코드를 방출 함)을 객체 코드로 변환합니다.

최신 GCC (특히 libgccjit ) 및 LLVM 인프라를 갖춘 BTW를 사용하여 다른 언어 (또는 자신의 언어)를 내부 Gimple 또는 LLVM 표현으로 컴파일 한 다음 중간 및 백-의 다양한 최적화 기능을 활용할 수 있습니다. 이 컴파일러의 끝 부분.

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