"JVM은 테일 콜 최적화를 지원하지 않으므로 많은 폭발 스택을 예측합니다."
(1) 테일 콜 최적화를 이해하지 못하거나 (2) JVM을 이해하지 못하거나 (3) 둘 다라고 말하는 사람은 누구나
Wikipedia 의 tail-call 정의부터 시작하겠습니다 ( Wikipedia 가 마음에 들지 않으면 여기 대안이 있습니다 ) :
컴퓨터 과학에서 테일 콜은 마지막 동작으로 다른 프로 시저에서 발생하는 서브 루틴 호출입니다. 반환 값을 생성 한 다음 호출 절차에 의해 즉시 반환 될 수 있습니다.
아래 코드에서에 대한 호출 bar()
은 다음의 테일 호출입니다 foo()
.
private void foo() {
// do something
bar()
}
테일 콜 최적화 는 테일 콜을보고 언어 구현이 일반적인 메소드 호출 (스택 프레임 생성)을 사용하지 않고 대신 브랜치를 생성 할 때 발생합니다. 이는 스택 프레임에 메모리가 필요하고 정보 (예 : 반송 주소)를 프레임으로 푸시하기 위해 CPU 사이클이 필요하고 콜 / 리턴 페어가 무조건 점프보다 더 많은 CPU 사이클이 필요하다고 가정하기 때문에 최적화입니다.
TCO는 종종 재귀에 적용되지만 이것이 유일한 용도는 아닙니다. 모든 재귀에도 적용 할 수 없습니다. 예를 들어 계승을 계산하는 간단한 재귀 코드는 함수에서 마지막으로 발생하는 것이 곱셈 연산이므로 테일 콜 최적화가 불가능합니다.
public static int fact(int n) {
if (n <= 1) return 1;
else return n * fact(n - 1);
}
테일 콜 최적화를 구현하려면 다음 두 가지가 필요합니다.
- 서브 트렁 틴 호출 외에 분기를 지원하는 플랫폼.
- 테일 콜 최적화가 가능한지 판단 할 수있는 정적 분석기.
그게 다야. 다른 곳에서 언급했듯이 JVM (다른 Turing-complete 아키텍처와 마찬가지로)에는 Goto가 있습니다. 무조건 goto 가 발생 하지만 조건부 분기를 사용하여 기능을 쉽게 구현할 수 있습니다.
정적 분석 부분은 까다 롭습니다. 단일 함수 내에서 문제가 없습니다. 예를 들어 다음은 값을 합산하는 꼬리 재귀 스칼라 함수입니다 List
.
def sum(acc:Int, list:List[Int]) : Int = {
if (list.isEmpty) acc
else sum(acc + list.head, list.tail)
}
이 함수는 다음 바이트 코드로 바뀝니다.
public int sum(int, scala.collection.immutable.List);
Code:
0: aload_2
1: invokevirtual #63; //Method scala/collection/immutable/List.isEmpty:()Z
4: ifeq 9
7: iload_1
8: ireturn
9: iload_1
10: aload_2
11: invokevirtual #67; //Method scala/collection/immutable/List.head:()Ljava/lang/Object;
14: invokestatic #73; //Method scala/runtime/BoxesRunTime.unboxToInt:(Ljava/lang/Object;)I
17: iadd
18: aload_2
19: invokevirtual #76; //Method scala/collection/immutable/List.tail:()Ljava/lang/Object;
22: checkcast #59; //class scala/collection/immutable/List
25: astore_2
26: istore_1
27: goto 0
goto 0
끝에 유의하십시오 . 이에 비해 동등한 Java 함수 ( Iterator
스칼라 목록을 머리와 꼬리로 나누는 동작을 모방 해야하는 )는 다음 바이트 코드로 바뀝니다. 마지막 두 연산은 이제 invoke 이고, 그 재귀 적 호출에 의해 생성 된 값을 명시 적으로 반환합니다.
public static int sum(int, java.util.Iterator);
Code:
0: aload_1
1: invokeinterface #64, 1; //InterfaceMethod java/util/Iterator.hasNext:()Z
6: ifne 11
9: iload_0
10: ireturn
11: iload_0
12: aload_1
13: invokeinterface #70, 1; //InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
18: checkcast #25; //class java/lang/Integer
21: invokevirtual #74; //Method java/lang/Integer.intValue:()I
24: iadd
25: aload_1
26: invokestatic #43; //Method sum:(ILjava/util/Iterator;)I
29: ireturn
단일 함수의 테일 호출 최적화는 쉽지 않습니다. 컴파일러는 호출 결과를 사용하는 코드가 없다는 것을 알 수 있으므로 호출 을로 대체 할 수 있습니다 goto
.
인생이 까다로운 곳은 여러 가지 방법이있는 경우입니다. JVM의 분기 명령어는 80x86과 같은 범용 프로세서의 명령어와 달리 단일 방법으로 제한됩니다. 개인 메소드가있는 경우 여전히 비교적 간단합니다. 컴파일러는 해당 메소드를 적절하게 인라인 할 수 있으므로 테일 호출을 최적화 할 수 있습니다 (어떻게 작동하는지 궁금한 경우 switch
동작을 제어 하는 일반적인 메소드를 고려하십시오 ). 동일한 클래스의 여러 공개 메소드로이 기술을 확장 할 수도 있습니다. 컴파일러는 메소드 본문을 인라인하고 공개 브릿지 메소드를 제공하며 내부 호출이 점프로 바뀝니다.
그러나이 클래스는 다른 클래스, 특히 인터페이스 및 클래스 로더에 비추어 공용 메소드를 고려할 때 분류됩니다. 소스 레벨 컴파일러에는 테일 콜 최적화를 구현하기에 충분한 지식이 없습니다. 그러나 "베어 메탈 (bare-metal)"구현 과는 달리 * JVM (핫스팟 컴파일러 (적어도 ex-Sun 컴파일러의 경우)의 형태로이를 수행하기위한 정보가 있습니다. 실제로 수행하는지 여부는 알 수 없습니다. 꼬리 호출 최적화, 그리고 의심하지,하지만 수 .
다음 중 질문의 두 번째 부분으로 연결되는 부분은 "우리가 관심을 가져야합니까?"
분명히 언어가 반복을위한 유일한 기본 요소로 재귀를 사용하는 경우주의를 기울입니다. 그러나이 기능이 필요한 언어는이를 구현할 수 있습니다. 유일한 문제는 해당 언어의 컴파일러가 임의의 Java 클래스에서 호출하고 호출 할 수있는 클래스를 생성 할 수 있는지 여부입니다.
그 경우를 제외하고는 다운 보트를 관련이 없다고 말하여 초대합니다. 내가 본 (그리고 많은 그래프 프로젝트로 작업 한) 대부분의 재귀 코드 는 꼬리 호출에 최적화되지 않습니다 . 단순한 계승과 마찬가지로 재귀를 사용하여 상태를 구축하며 테일 연산은 조합입니다.
테일 콜 최적화가 가능한 코드의 경우, 해당 코드를 반복 가능한 형식으로 변환하는 것이 종종 간단합니다. 예를 들어 sum()
앞에서 보여 드린 기능은로 일반화 할 수 있습니다 foldLeft()
. source 를 보면 실제로 반복 작업으로 구현 된 것을 볼 수 있습니다. Jörg W Mittag 에는 함수 호출을 통해 구현 된 상태 머신의 예가있었습니다. 점프로 변환되는 함수 호출에 의존하지 않는 효율적이고 유지 보수가 가능한 상태 머신 구현이 많이 있습니다.
완전히 다른 것으로 마무리하겠습니다. 당신이에서 당신의 방법을 구글로하면 각주 SICP, 당신은 끝낼 수 있습니다 여기에 . 필자는 개인적으로 내 컴파일러를로 대체 JSR
하는 것보다 훨씬 흥미로운 곳을 찾습니다 JUMP
.