TCO가 없을 때 스택을 날릴 염려가 언제 있습니까?


14

JVM을 대상으로하는 새로운 프로그래밍 언어에 대한 토론이있을 때마다 필연적으로 다음과 같은 사람들이 있습니다.

"JVM은 테일 콜 최적화를 지원하지 않으므로 많은 폭발 스택을 예측합니다."

그 주제에는 수천 가지 변형이 있습니다.

이제 Clojure와 같은 일부 언어에는 사용할 수 있는 특별한 재귀 구문이 있음을 알고 있습니다.

내가 이해하지 못하는 것은 테일 콜 최적화가 얼마나 심각합니까? 언제 걱정해야합니까?

내 혼란의 주요 원인은 아마도 Java가 가장 성공적인 언어 중 하나이며 JVM 언어 중 상당수가 상당히 잘 작동하는 것 같습니다. 그게 어떻게 TCO의 부족으로 정말이면 가능하다 어떤 문제?


4
당신이 TCO없이 스택을 날려 재귀 깊은 충분히있는 경우에 당신은 심지어 TCO에 문제가있는 것
래칫 괴물

18
@ratchet_freak 말도 안 돼요. 스키마에는 루프가 없지만 사양에 TCO 지원이 필요하기 때문에 많은 데이터 세트에 대한 재귀 반복은 명령 루프보다 비용이 많이 들지 않습니다 (Scheme 구조가 값을 반환하는 보너스 포함).
itsbruce

6
@ratchetfreak TCO는 원하는 방식으로 스택을 날려 버릴 수없는 방식으로 재귀 함수를 작성하는 메커니즘입니다 (예 : 꼬리 재귀). 귀하의 진술은 꼬리 재귀로 작성되지 않은 재귀에만 의미가 있습니다.이 경우에는 정확하며 TCO는 도움이되지 않습니다.
Evicatos

2
마지막으로, 80x86은 (기본) 테일 콜 최적화를 수행하지 않습니다. 그러나 언어 개발자가이를 사용하는 언어를 포팅하는 것을 막을 수는 없습니다. 컴파일러는 점프 대 jsr을 사용할 수있는 시점을 식별하며 모두 행복합니다. JVM에서 동일한 작업을 수행 할 수 있습니다.
kdgregory

3
@ kdgregory : 그러나 x86에는 GOTO, JVM에는 없습니다. 그리고 x86은 interop 플랫폼으로 사용되지 않습니다. JVM에는없고 GOTOJava 플랫폼을 선택하는 주된 이유 중 하나가 interop입니다. JVM에서 TCO를 구현 하려면 스택에 무언가 를 수행해야 합니다 . 직접 관리하십시오 (즉, JVM 호출 스택을 전혀 사용하지 마십시오), 트램펄린을 사용하고 예외를로 사용하십시오 GOTO. 이러한 경우 모두 JVM 호출 스택과 호환되지 않습니다. Java와 스택 호환이 가능하고 TCO가 있으며 고성능이 불가능합니다. 이 세 가지 중 하나를 희생해야합니다.
Jörg W Mittag

답변:


16

이것을 고려해 봅시다. Java의 모든 루프를 제거했다고 가정 해 봅시다 (컴파일러 작성자가 파업 또는 다른 일을하고 있습니다). 이제 우리는 계승을 쓰고 싶기 때문에 다음과 같이 맞을 것입니다.

int factorial(int i){ return factorial(i, 1);}
int factorial(int i, int accum){
  if(i == 0) return accum;
  return factorial(i-1, accum * i);
}

이제 우리는 꽤 영리하게 느끼고 있으며 루프 없이도 계승을 작성할 수있었습니다! 그러나 테스트 할 때 합리적인 크기의 숫자를 사용하면 TCO가 없으므로 스택 오버플로 오류가 발생합니다.

실제 Java에서는 이것이 문제가되지 않습니다. 꼬리 재귀 알고리즘이 있다면 루프로 변환하고 괜찮을 수 있습니다. 그러나 루프가없는 언어는 어떻습니까? 그럼 당신은 막 호스입니다. 그래서 clojure 가이 recur양식을 사용하지 않고이 양식을 사용하는 이유는 완전하지 않습니다 (무한 루프를 수행 할 수있는 방법 없음).

JVM, Frege, Kawa (Scheme), Clojure를 대상으로하는 기능적 언어 클래스는 항상 테일 콜 (Tall Call) 부족을 처리하려고 시도합니다. 이러한 언어에서 TC는 루프를 수행하는 관용적 방식이기 때문입니다! 구성표로 변환하면 위의 계승이 좋은 계승이됩니다. 5000 번 반복하여 프로그램이 중단되면 매우 불편합니다. 이 문제는 recur특수한 형태로 자체 통화 최적화, 트램폴린 등을 암시하는 주석으로 해결할 수 있습니다 . 그러나 이들은 모두 성능 저 하나 프로그래머의 불필요한 작업을 강요합니다.

이제 TCO에 더 많은 것이 있기 때문에 Java는 자유 로워지지 않습니다. 단지 재귀 함수는 어떻습니까? 루프로 직접 변환 할 수는 없지만 여전히 JVM에 의해 최적화되지 않습니다. 따라서 Java를 사용하여 상호 재귀를 사용하여 알고리즘을 작성하려고 시도하는 것은 대단히 불쾌합니다. 성능 / 범위를 원한다면 루프에 맞추려면 암흑 마법을 사용해야하기 때문입니다.

요약하면, 이것은 많은 경우에 큰 문제가 아닙니다. 대부분의 테일 콜은 하나의 스택 프레임 깊이 만 진행합니다.

return foo(bar, baz); // foo is just a simple method

또는 재귀입니다. 그러나 이것에 맞지 않는 TC 클래스의 경우 모든 JVM 언어가 고통을 느낍니다.

그러나 아직 TCO가없는 적절한 이유가 있습니다. JVM은 스택 추적을 제공합니다. TCO를 사용하면 "두 메드 (doomed)"라는 스택 프레임을 체계적으로 제거 할 수 있지만 JVM은 나중에 나중에 스택 추적을 위해이를 원할 수도 있습니다! 각 상태가 다음을 호출하는 FSM을 이와 같이 구현한다고 가정 해보십시오. 우리는 이전 상태의 모든 기록을 지웠으므로 역 추적은 어떤 상태를 보여 주지만 우리가 어떻게 도착했는지는 알 수 없습니다.

또한 더 압박 적으로 많은 바이트 코드 검증은 스택 기반이므로 바이트 코드를 검증하는 것이 즐거운 전망이 아닙니다. 이것과 Java에 루프가 있다는 사실 사이에서 TCO는 JVM 엔지니어에게 가치가있는 것보다 조금 더 문제처럼 보입니다.


2
가장 큰 문제는 스택 검사를 완전히 기반으로하는 바이트 코드 검증기입니다. JVM 사양의 주요 버그입니다. 25 년 전, JVM이 설계되었을 때 사람들은 JVM 바이트 코드 언어를 안전하지 않은 상태로 만드는 것이 아니라 실제로 바이트 바이트 검증에 의존하는 것이 더 낫다고 말했다. 그러나 Scheme 커뮤니티의 주요 인물 중 하나 인 Matthias Felleisen은 바이트 코드 검증기를 유지하면서 테일 호출이 JVM에 추가 될 수있는 방법을 보여주는 논문을 작성했습니다.
Jörg W Mittag

2
흥미롭게도, IBM에 의해 J9 JVM는 않습니다 총 소유 비용 (TCO)을 수행합니다.
Jörg W Mittag

1
@jozefg 흥미롭게도, 아무도 루프에 대한 스택 트레이스 항목에 신경 쓰지 않으므로 스택 트레이스 인수는 적어도 꼬리 재귀 함수에 대해 물을 보유하지 않습니다.
Ingo

2
@MasonWheeler 정확히 내 요점입니다. stacktrace는 어떤 반복이 발생했는지 알려주지 않습니다. 루프 변수 등을 검사하여 간접적으로 만 이것을 볼 수 있습니다. 그렇다면 왜 꼬리 재귀 함수의 몇 개의 hundert 스택 추적 항목이 필요합니까? 마지막 것만 흥미 롭습니다! 루프와 마찬가지로 로컬 변수, 인수 값 등을 검사하여 재귀를 확인할 수 있습니다.
Ingo

3
@Ingo : 함수 자체 만 재귀하는 경우 스택 추적이 많이 표시되지 않을 수 있습니다. 그러나 함수 그룹이 음의 재귀 적 인 경우 스택 추적이 때로는 많은 것을 보여줄 수 있습니다.
supercat

7

테일 호출 최적화는 테일 재귀 때문에 주로 중요합니다. 그러나 JVM이 테일 호출을 최적화하지 않는 것이 실제로 좋은 이유는 다음과 같습니다. TCO가 스택의 일부를 재사용 할 때 예외의 스택 추적이 불완전하므로 디버깅이 조금 더 어려워집니다.

JVM의 한계를 해결하는 방법이 있습니다.

  1. 간단한 꼬리 재귀는 컴파일러에 의해 루프에 최적화 될 수 있습니다.
  2. 프로그램이 연속 합격 스타일 인 경우 "트램폴린"을 사용하는 것이 쉽지 않습니다. 여기서 함수는 최종 결과를 반환하지 않고 연속을 외부에서 실행합니다. 이 기술을 사용하면 컴파일러 작성자가 임의로 복잡한 제어 흐름을 모델링 할 수 있습니다.

더 큰 예가 필요할 수 있습니다. 닫는 언어 (예 : JavaScript 또는 유사한 언어)를 고려하십시오. 계승을 다음과 같이 쓸 수 있습니다

def fac(n, acc = 1) = if (n <= 1) acc else n * fac(n-1, acc*n)

print fac(x)

이제 대신 콜백을 반환하도록 할 수 있습니다.

def fac(n, acc = 1) =
  if (n <= 1) acc
  else        (() => fac(n-1, acc*n))  // this isn't full CPS, but you get the idea…

var continuation = (() => fac(x))
while (continuation instanceof function) {
  continuation = continuation()
}
var result = continuation
print result

이것은 이제 일정한 스택 공간에서 작동합니다. 어쨌든 꼬리 재귀이기 때문에 어리석은 일입니다. 그러나이 기술은 모든 테일 호출을 일정한 스택 공간으로 평탄화 할 수 있습니다. 그리고 프로그램이 CPS에 있으면 호출 스택이 전체적으로 일정하다는 것을 의미합니다 (CPS에서는 모든 호출이 테일 호출 임).

이 기술의 주요 단점은 디버깅하기가 훨씬 어렵고 구현하기가 약간 어렵고 성능이 떨어진다는 것입니다. 사용중인 모든 클로저 및 간접 참조를 참조하십시오.

이러한 이유로 VM이 테일 콜 op를 구현하도록하는 것이 매우 바람직합니다. 테일 콜을 지원하지 않는 적절한 이유가있는 언어는이를 사용하지 않아도됩니다.


1
"TCO가 스택의 일부를 재사용 할 때 예외의 스택 추적이 불완전합니다."-그러나 루프 내에서 스택 추적도 불완전합니다. 루프가 얼마나 자주 실행되었는지 기록하지 않습니다. -아아, JVM이 적절한 테일 호출을 지원하더라도 디버깅 중에 여전히 옵트 아웃 할 수 있습니다. 그런 다음 생산을 위해 TCO가 코드가 100,000 또는 100,000,000 테일 호출로 실행되도록합니다.
Ingo

1
@Ingo No. (1) 루프가 재귀로 구현되지 않으면 스택에 루프가 표시 될 근거가 없습니다 (꼬리 호출 ≠ 점프 ≠ 호출). (2) TCO는 테일 재귀 최적화보다 일반적입니다. 내 대답은 재귀를 예로 사용 합니다. (3) TCO 를 사용 하는 스타일로 프로그래밍하는 경우이 최적화 기능을 끄는 것은 옵션이 아닙니다. 전체 TCO 또는 전체 스택 추적은 언어 기능이거나 그렇지 않습니다. 예를 들어 Scheme은 고급 예외 시스템과 TCO 단점의 균형을 유지합니다.
amon

1
(1) 완전히 동의합니다. 그러나 동일한 추론에 의해, 물론 return foo(....);방법 foo(2) 에서 모두 지적하는 수백, 수천 개의 스택 추적 항목을 유지할 근거는 없다 . 여전히 루프, 할당 (!), 명령문 시퀀스에서 불완전한 추적을 허용합니다. 예를 들어, 변수에서 예상치 못한 값을 찾으면 해당 값이 어떻게 도달했는지 알고 싶습니다. 그러나이 경우 누락 된 흔적에 대해 불평하지 않습니다. 그것은 우리의 두뇌에 어떻게 든 새겨 져 있기 때문에 a) 그것은 호출에서만 발생합니다. b) 모든 호출에서 발생합니다. 둘 다 말이되지 않습니다, IMHO.
Ingo

(3) 동의하지 않음. 크기가 N 인 문제로 코드를 디버깅 할 수없는 이유를 알 수 없습니다 .N은 작은 크기로 일반 스택을 피할 수 있습니다. 그런 다음 스위치를 켜고 TCO를 켜려면 프로브 크기에 대한 제약 조건을 효과적으로 떨어 뜨립니다.
Ingo

@Ingo“동의. 크기가 N 인 문제로 코드를 디버깅 할 수없는 이유를 알 수 없습니다. N은 보통의 스택에서 벗어날 수있을 정도로 작습니다.” TCO / TCE가 CPS 변환 용인 경우이를 끄면 스택이 오버플로되고 프로그램이 중단되므로 디버깅이 불가능합니다. 구글 때문에 발생하는이 문제의, V8 JS에서 총 소유 비용 (TCO)을 구현하기를 거부 부수적 . 그들은 프로그래머가 TCO를 원하고 스택 추적의 손실을 원한다고 선언 할 수 있도록 특별한 구문을 원할 것입니다. TCO에 의해 예외가 해결되는지 아는 사람이 있습니까?
쉘비 무어 III

6

프로그램에서 호출의 상당 부분은 테일 호출입니다. 모든 서브 루틴에는 마지막 호출이 있으므로 모든 서브 루틴에는 적어도 하나의 테일 호출이 있습니다. 테일 콜은 성능 특성이 GOTO있지만 서브 루틴 콜의 안전성을 갖습니다 .

적절한 테일 콜이 있으면 다른 방법으로는 쓸 수없는 프로그램을 작성할 수 있습니다. 상태 머신을 예로 들어 보겠습니다. 각 상태를 서브 루틴으로하고 각 상태 전이를 서브 루틴 호출로하여 상태 머신을 매우 직접 구현할 수 있습니다. 이 경우, 호출 후 호출 후 호출하여 상태에서 상태로 전환하고 실제로는 절대 리턴 하지 않습니다 ! 적절한 꼬리 호출이 없으면 즉시 스택을 날려 버립니다.

PTC가 없으면 GOTO제어 흐름 또는 이와 유사한 것으로 트램펄린 또는 예외 를 사용해야 합니다. 그것은 훨씬 더 추악하고 상태 머신의 직접적인 1 : 1 표현은 아닙니다.

(I 영리 "루프"예 지루한을 사용하여 피할 방법을 참고.이 PTCS 심지어 언어에 유용한 예이다 루프.)

저는 여기서 TCO 대신 "적절한 테일 콜"이라는 용어를 사용했습니다. TCO는 컴파일러 최적화입니다. PTC는 모든 컴파일러에서 TCO를 수행 해야하는 언어 기능입니다 .


The vast majority of calls in a program are tail calls. 호출 된 메소드의 "대다수"가 둘 이상의 자체 호출을 수행하는 경우에는 아닙니다. Every subroutine has a last call, so every subroutine has at least one tail call. 이것은 명백히 거짓으로 증명할 수 있습니다 return a + b. (물론 기본 산술 연산이 함수 호출로 정의되는 미친 언어가 아닌 한)
Mason Wheeler

1
"두 숫자를 추가하면 두 숫자가 추가됩니다." 그렇지 않은 언어는 제외하십시오. 단일 산술 연산자가 임의의 수의 인수를 취할 수있는 Lisp / Scheme의 + 연산은 어떻습니까? (+ 1 2 3) 그것을 구현하는 유일한 방법은 함수입니다.
Evicatos

1
@Mason Wheeler : 추상화 반전은 무엇을 의미합니까?
Giorgio

1
@MasonWheeler 의심 할 여지없이, 내가 본 기술적 주제에 대해 가장 손에 흔드는 위키피디아 항목입니다. 모호한 항목을 보았지만 그저 ... 와우.
Evicatos

1
@MasonWheeler : On Lisp의 22 페이지와 23 페이지의 목록 길이 기능에 대해 이야기하고 있습니까? 테일 콜 버전은 약 1.2 배로 복잡해 3 배에 가깝습니다. 또한 추상화 반전의 의미에 대해 명확하지 않습니다.
Michael Shaw

4

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


테일 콜 오피 코드가 존재하는 경우, 테일 콜 최적화는 왜 호출하는 메소드가 나중에 코드를 실행해야하는지 여부를 각 콜 사이트에서 관찰하는 것 외에 다른 것을 요구하는 이유는 무엇입니까? 경우에 따라 스택을 조작하고 점프를 수행하는 코드를 생성하는 것보다 return foo(123);인라인으로 더 나은 명령문을 실행할 수 foo있지만 꼬리 호출이 일반 호출과 다른 이유는 알 수 없습니다. 그런 점.
supercat

@supercat-귀하의 질문이 무엇인지 잘 모르겠습니다. 이 글의 첫 번째 요점은 컴파일러가 잠재적 인 모든 수신자의 스택 프레임이 어떻게 보이는지 알 수 없다는 것입니다 (스택 프레임에는 함수 인수뿐만 아니라 로컬 변수도 포함 함을 기억하십시오). 호환 가능한 프레임에 대한 런타임 검사를 수행하는 opcode를 추가 할 수 있다고 가정하지만 게시물의 두 번째 부분으로 이동합니다. 실제 값은 무엇입니까?
kdgregory
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.