Java가 꼬리 재귀에 대해 전혀 최적화되지 않은 이유는 무엇입니까?


92

내가 읽은 것에서 : 그 이유는 상속을 받았을 때 실제로 어떤 메소드가 호출 될지를 결정하는 것이 쉽지 않기 때문입니다.

그러나 왜 Java가 정적 메소드에 대해 꼬리 재귀 최적화를 가지고 있지 않고 컴파일러로 정적 메소드를 호출하는 적절한 방법을 시행하지 않는 이유는 무엇입니까?

왜 Java는 꼬리 재귀를 전혀 지원하지 않습니까?

여기에 어려움이 있는지 확실하지 않습니다.


대하여 제안 된 중복 하여 설명한 바와 같이, 르그 W MITTAG 1 :

  • 다른 질문은 TCO에 관한 것입니다.이 질문은 TRE에 관한 것입니다. TRE는 TCO보다 훨씬 간단합니다.
  • 또한 다른 질문은 JVM으로 컴파일하려는 언어 구현에 대해 JVM이 부과하는 제한 사항에 대해 묻습니다.이 질문은 JVM 사양에 의해 변경 될 수 있기 때문에 JVM에 의해 제한되지 않는 언어 인 Java에 대해 묻습니다. Java를 디자인하는 사람들과 동일합니다.
  • 마지막으로, JVM에는 TRE에 필요한 모든 메소드 내 GOTO가 있기 때문에 TRE에 대한 JVM에는 제한이 없습니다.

1 콜 아웃 포인트에 포맷이 추가되었습니다.


26
인용 에릭 Lippert의를 : "기능이 싸지 않다, 그들은 매우 고가이며, 그들은 단지 자신의 비용을 정당화해서는 안 그들은 우리가 예산으로 할 수 있었던 백 개 다른 기능을 수행하지 않는 기회 비용을 정당화해야합니다." Java는 부분적으로 C / C ++ 개발자에게 호소하도록 설계되었으며 해당 언어에서는 꼬리 재귀 최적화가 보장되지 않습니다.
Doval

5
내가 틀렸다면 나를 수정하지만 Eric Lippert는 꼬리 재귀 최적화가있는 C #의 디자이너입니까?
정보 : A

1
그는 C # 컴파일러 팀에 있습니다.
Doval

10
내가 이해에서 JIT를 그것을 할 수있는 , 특정 조건이 충족 될 경우 아마. 따라서 실제로는 신뢰할 수 없습니다. 그러나 C #의 유무는 에릭의 관점에서 중요하지 않습니다.
Doval

4
@InstructedA 조금 더 깊이 파고 들었다면 32 비트 JIT 컴파일러에서 수행 된 적이 없다는 것을 알 수 있습니다. 64 비트 JIT는 여러면에서 새롭고 똑똑합니다. 더 새로운 실험 컴파일러 (32 및 64 비트 모두)는 아직 더 똑똑하며 IL에서 명시 적으로 요구하지 않는 꼬리 재귀 최적화를 지원합니다. JIT 컴파일러에는 많은 시간이 필요하지 않습니다. 속도에 최적화되어 있습니다. C ++로 컴파일하는 데 몇 시간이 걸릴 수있는 응용 프로그램은 여전히 ​​최대 (적어도 부분적으로) 수백 ms 안에 IL에서 네이티브로 이동해야합니다.
Luaan

답변:


132

비디오 에서 Brian Goetz (Oracle의 Java Language Architect)가 설명한대로 :

jdk 클래스 [...]에는 jdk 라이브러리 코드와 호출 코드 사이의 스택 프레임을 계산하여 누가 호출하는지 파악하는 보안에 민감한 여러 가지 방법이 있습니다.

스택의 프레임 수를 변경하면 이로 인해 오류가 발생합니다. 그는 이것이 어리석은 이유라는 것을 인정하고 JDK 개발자들은이 메커니즘을 대체했다.

그는 또한 우선 순위가 아니라 꼬리 재귀라고 언급했다.

결국 끝날 것입니다.

NB 이는 HotSpot 및 OpenJDK에 적용되며 다른 VM은 다를 수 있습니다.


7
나는 이것과 같이 다소 잘 랐고 건조한 대답이 있다는 것에 놀랐습니다! 그러나 그것은 실제로 해답처럼 보입니다-그것은 이미 가능하지 않은 오래된 기술적 이유로 인해 가능합니다. 그래서 우리는 누군가 누군가 그것을 구현하기에 충분히 중요하다고 결정할 때까지 기다립니다.
BrianH

1
해결 방법을 구현하지 않는 이유는 무엇입니까? 인수에 대한 새로운 값을 사용하여 메소드 호출의 맨 위로 이동하는 Under-the-Covers 레이블 고토처럼? 이는 컴파일 타임 최적화이며 스택 프레임을 이동하거나 보안 위반을 유발할 필요가 없습니다.
James Watkins 2019

2
만약 당신이나 다른 누군가가 "보안에 민감한 방법"이 무엇인지 더 깊이 파고들 수 있다면 더 좋을 것입니다. 감사합니다!
InformedA

3
@InstructedA- Java 2 릴리스에서 Java Security Manager 시스템의 작동 방식에 대한 자세한 설명이 포함 된 securingjava.com/chapter-three/chapter-three-6.html 을 참조하십시오 .
Jules

3
한 가지 해결 방법은 다른 JVM 언어를 사용하는 것입니다. 예를 들어 스칼라는 런타임이 아닌 컴파일러에서이를 수행합니다.
James Moore

24

Java에는 대부분의 명령형 언어에없는 것과 같은 이유로 테일 콜 최적화 기능이 없습니다. 명령형 루프는 선호되는 언어 스타일이며 프로그래머는 꼬리 재귀를 명령형 루프로 대체 할 수 있습니다. 스타일의 문제로 사용을 권장하지 않는 기능에는 복잡성이 가치가 없습니다.

프로그래머가 때때로 FP 스타일로 다른 방식으로 쓰기를 원하는 것은 컴퓨터가 GHz 대신 코어로 스케일을 시작한 후 지난 10 년 동안 만 유행했습니다. 지금도 그렇게 인기가 없습니다. 직장에서 명령형 루프를 꼬리 재귀로 교체하도록 제안하면 코드 검토 자의 절반이 웃고 나머지 절반은 혼란스러워 보입니다. 함수형 프로그래밍에서도 고차 함수와 같은 다른 구문이 제대로 맞지 않으면 꼬리 재귀를 피할 수 있습니다.


36
이 답변이 옳지 않은 것 같습니다. 꼬리 재귀가 엄격하게 필요하지 않다고해서 유용하지 않다는 의미는 아닙니다. 재귀 솔루션은 반복보다 이해하기 쉬운 경우가 많지만 테일 호출 (tail call)이 없으면 재귀 알고리즘이 스택을 날려 버릴 큰 문제 크기에 대해 잘못됩니다. 이것은 성능이 아니라 정확성에 관한 것입니다. 정답에서 알 수 있듯이 테일 콜 (tail call)의 부족은 스택 추적에 의존하는 이상한 보안 모델 때문입니다.
amon

4
@amon 제임스 고슬링 (James Gosling)은 한 번 그가 자바에 기능을 추가 할 것이라고 말했다 여러 사람이 그것을 요구하지 않는 한 , 오직 그때 그는 것이라고 생각 을. 따라서 대답의 일부가 실제로 "항상 for루프를 사용할 수 있습니다 "(일등 함수와 객체의 경우도 마찬가지)라는 사실에 놀라지 않을 것 입니다. 나는 그것을 "불완전한 편견"이라고 부르지 않을 것이다. 그러나 1995 년에 가장 큰 관심사가 아마도 자바의 속도 였고 제네릭의 부족 일 때 그것이 매우 수요가 많았을 것이라고 생각하지 않는다.
Doval

7
보안 모델 인 @amon은 기존 언어에 TCO를 추가하지 않는 것이 합법적 인 이유이지만, 우선 언어에 언어를 설계하지 않는 나쁜 이유입니다. 사소한 비하인드 기능을 포함하기 위해 프로그래머가 볼 수있는 주요 기능을 버리지 않습니다. "Java 8에 TCO가없는 이유"는 "Java 1.0에 TCO가없는 이유"와는 다른 질문입니다. 나는 후자에 대답했다.
Karl Bielefeldt

3
@rwong, 모든 재귀 함수를 반복 함수로 변환 할 수 있습니다 . (I, 나는 예를 들어 작성한 수있는 경우 여기에 )
알랭

3
@ 잔 린스 그것은 문제의 유형에 따라 다릅니다. 예를 들어, 방문자 패턴 은 FP와 관련이없는 반면 구조 및 방문자의 특성에 따라 TCO의 이점을 얻을 수 있습니다. 트램폴린을 사용한 변환은 문제에 따라 약간 더 자연스럽게 보이지만 상태 머신을 통과하는 것은 비슷합니다. 또한 기술적으로 루프를 사용하여 트리를 구현할 수는 있지만 재귀는 훨씬 더 자연 스럽습니다 (이 경우 DFS와 유사하지만 TCO에서도 스택 제한으로 인해 재귀를 피할 수 있음).
Maciej Piechotka

5

JVM에는 꼬리 호출을위한 바이트 코드가 없기 때문에 키 호출 최적화 기능이 없습니다 ( 정적으로 알려지지 않은 함수 포인터 (예 : 일부 vtable의 메소드)).

사회적인 (아마도 기술적 인) 이유로, JVM에서 새로운 바이트 코드 작업을 추가하면 (이 JVM의 이전 버전과 호환되지 않을 것임) JVM 사양 소유자에게는 굉장히 어렵습니다.

JVM 스펙에 새로운 바이트 코드를 추가하지 않는 기술적 이유는 실제 JVM 구현이 매우 복잡한 소프트웨어 조각이라는 점입니다 (예 : 많은 JIT 최적화로 인해).

알려지지 않은 함수에 대한 Tail 호출은 현재 스택 프레임을 새 것으로 교체해야하며 해당 작업은 JVM에 있어야합니다 (바이트 코드 생성 컴파일러를 변경하는 것만이 아닙니다).


17
문제는 꼬리 호출에 관한 것이 아니라 꼬리 재귀에 관한 것입니다. 그리고 질문은 JVM 바이트 코드 언어에 관한 것이 아니라, 완전히 다른 언어 인 Java 프로그래밍 언어에 관한 것입니다. 스칼라는 JVM 바이트 코드로 컴파일하며 꼬리 재귀 제거 기능이 있습니다. JVM에서의 스킴 구현에는 전체적인 Tail-Call이 있습니다. Java 7 (JVM Spec, 3rd Ed.)은 새로운 바이트 코드를 추가했습니다. IBM의 J9 JVM은 특수 바이트 코드가 없어도 TCO를 수행합니다.
Jörg W Mittag

1
@ JörgWMittag : 사실이지만, Java 프로그래밍 언어에 적절한 테일 호출이없는 것이 사실인지 궁금합니다. 자바 프로그래밍 언어가되지 않는 상태로 더 정확한 될 수 있는 사양의 의무 거기에 그 아무것도, 적절한 꼬리 통화를 할 수 있습니다. (즉, 스펙의 어떤 것도 실제로 테일 호출을 제거하는 것을 금지 한다고 확신 하지 않습니다. 단순히 언급되지 않았습니다.)
ruakh

4

언어에 테일 콜 (재귀 적 또는 다른 방식)을 작성하기위한 특별한 구문이없고 테일 콜이 요청되었지만 생성 될 수 없을 때 컴파일러가 스 쿼크를 수행하지 않는 한 "선택적"테일 콜 또는 테일 재귀 최적화는 조각이 발생하는 상황을 초래합니다. 코드는 한 시스템에서 100 바이트 미만의 스택이 필요하지만 다른 시스템에서는 100,000,000 바이트 이상의 스택이 필요할 수 있습니다. 그러한 차이는 단순한 양적인 것이 아니라 질적 인 것으로 간주되어야합니다.

머신은 스택 크기가 다를 수 있으므로 코드는 항상 한 머신에서 작동하지만 다른 머신에서는 스택을 날릴 수 있습니다. 그러나 일반적으로 스택이 인위적으로 수축 된 경우에도 한 시스템에서 작동하는 코드는 "일반"스택 크기의 모든 시스템에서 작동 할 수 있습니다. 그러나 1,000,000 심도를 되풀이하는 방법이 한 시스템에서 테일 콜 최적화되었지만 다른 시스템에서는 최적화되지 않은 경우, 이전 시스템에서의 실행은 스택이 비정상적으로 작더라도 작동하고 스택이 비정상적으로 큰 경우에도 실패합니다. .


2

테일 콜 재귀는 Java에서 주로 사용되지 않는다고 생각합니다. 스택 트레이스가 변경되어 프로그램 디버깅이 훨씬 어려워지기 때문입니다. Java의 주요 목표 중 하나는 프로그래머가 코드를 쉽게 디버깅 할 수 있도록하는 것이며, 특히 객체 지향 프로그래밍 환경에서 스택 추적을 수행하는 데 필수적입니다. 대신 반복을 사용할 수 있으므로 언어위원회는 꼬리 재귀를 추가 할 가치가 없다는 것을 이해해야합니다.

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