"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.