스택을 사용하여 함수 호출 의미를 나타내는 대안은 무엇입니까?


19

우리는 함수 호출이 일반적으로 스택을 사용하여 구현된다는 것을 알고 사랑합니다. 프레임, 리턴 주소, 매개 변수, 전체 로트가 있습니다.

그러나 스택은 구현 세부 사항입니다. 호출 규칙은 다른 작업을 수행 할 수 있습니다 (예 : x86 빠른 호출은 (일부) 레지스터를 사용하고, MIPS 및 팔로어는 레지스터 창을 사용하는 등), 최적화는 다른 작업 (인라인, 프레임 포인터 생략, 테일 콜 최적화.).

물론 많은 머신 (JVM 및 CLR과 같은 VM과 PUSH / POP 등의 x86과 같은 실제 머신)에 편리한 스택 명령이 있기 때문에 함수 호출에 편리하게 사용할 수 있지만 경우에 따라 가능합니다. 콜 스택이 필요하지 않은 방식으로 프로그래밍하기 (여기서는 Continuation Passing Style 또는 메시지 전달 시스템의 Actors에 대해 생각하고 있습니다)

그래서 궁금해지기 시작했습니다. 다른 데이터 구조 (대기열 또는 아마도 연관 맵을 사용하여) 또는 스택없이 함수 호출 의미를 구현할 수 있습니까?
물론 스택이 매우 편리합니다 (유비쿼터스 인 이유가 있습니다).하지만 최근에 구현을 통해 궁금해졌습니다.

언어 / 기계 / 가상 기계에서 수행 된 적이 있는지, 그렇다면 현저한 차이점과 단점은 무엇입니까?

편집 : 내 직감은 다른 하위 계산 방식이 다른 데이터 구조를 사용할 수 있다는 것입니다. 예를 들어, 람다 미적분학은 스택 기반이 아니며 (함수 적용 개념은 축소에 의해 포착 됨) 실제 언어 / 기계 / 예제를보고있었습니다. 그래서 내가 묻는 이유는 ...


Clean 은 그래프와 그래프 재 작성 시스템을 사용하는데, 이는 3 스택 시스템을 사용하여 구현되지만 일반적으로 내용이 다릅니다.

가상 머신의 경우 링크 된 목록을 사용할 수 있습니다. 목록의 각 노드는 프레임입니다. VM에서 하드웨어 스택을 사용하므로 이로 인해 오버 헤드없이 힙에 프레임이 존재할 수 있습니다 realloc().
shawnhcorey

답변:


19

언어에 따라 호출 스택을 사용하지 않아도됩니다. 콜 스택은 재귀 또는 상호 재귀를 허용하는 언어로만 필요합니다. 언어가 재귀를 허용하지 않는 경우, 한 번에 하나의 프로 시저 호출 만 활성화 될 수 있으며 해당 프로 시저에 대한 로컬 변수가 정적으로 할당 될 수 있습니다. 이러한 언어는 인터럽트 처리를 위해 컨텍스트 변경을 제공해야하지만 여전히 스택이 필요하지는 않습니다.

호출 스택이 필요없는 언어의 예는 FORTRAN IV (이전) 및 COBOL 초기 버전을 참조하십시오.

호출 스택에 대한 직접적인 하드웨어 지원을 제공하지 않은 초기 슈퍼 컴퓨터의 예는 Control Data 6600 (및 이전 Control Data 시스템)을 참조하십시오. 콜 스택을 지원하지 않는 초기 미니 컴퓨터의 성공적인 예는 PDP-8을 참조하십시오.

내가 아는 한, Burroughs B5000 스택 머신은 하드웨어 콜 스택이있는 최초의 머신이었습니다. B5000 머신은 처음부터 ALGOL을 실행하도록 설계되었으므로 재귀가 필요했습니다. 또한 기능 아키텍처의 토대를 마련한 최초의 디스크립터 기반 아키텍처 중 하나를 보유했습니다.

내가 아는 한, MIT의 해커 커뮤니티가 하나를 전달하고 PUSHJ (Push Return Address and Jump) 작업을 발견했을 때 콜 스택 하드웨어를 대중화 한 것은 PDP-6 (DEC-10으로 성장)이었습니다. 십진수 인쇄 루틴을 50 명령어에서 10으로 줄였습니다.

재귀를 허용하는 언어에서 가장 기본적인 함수 호출 시맨틱은 스택과 잘 일치하는 기능이 필요합니다. 그게 전부라면 기본 스택은 훌륭하고 간단합니다. 그 이상이 필요한 경우 데이터 구조가 더 많은 일을해야합니다.

내가 더 많이 경험 한 가장 좋은 예는 "연속", 중간에 계산을 일시 중단하고 얼어 붙은 상태의 거품으로 저장 한 후 나중에 여러 번 다시 시작하는 기능입니다. LISP의 Scheme dialect에서 계속되는 오류는 무엇보다도 오류 엑시트를 구현하는 방법으로 널리 사용되었습니다. 계속하려면 현재 실행 환경의 스냅 샷 기능을 필요로하며 나중에이를 재현해야하므로 스택이 다소 불편합니다.

Abelson & Sussman의 "컴퓨터 프로그램의 구조 및 해석" 은 연속에 대해 자세히 설명합니다.


2
그것은 위대한 역사적 통찰력이었습니다. 감사합니다! 질문을했을 때 나는 특히 연속 합격 스타일 (CPS)을 염두에 두었습니다. 이 경우 스택은 불편할뿐만 아니라 필요하지 않을 수도 있습니다. 반환 위치를 기억할 필요가 없으며 실행을 계속할 지점을 제공합니다. 다른 스택리스 접근 방식이 일반적인 경우 궁금해하고 알지 못하는 훌륭한 방법을 제공했습니다.
Lorenzo Dematté

약간 관련됨 : "언어가 재귀를 허용하지 않는 경우"를 올바르게 지적했습니다. 재귀가있는 언어, 특히 꼬리 재귀가 아닌 함수는 어떻습니까? "디자인에 따라"스택이 필요합니까?
Lorenzo Dematté

"콜 스택은 재귀 또는 상호 재귀를 허용하는 언어로만 필요합니다"-아니요. 함수가 (예를 들어, 두 개 이상의 장소에서 호출 할 수있는 경우 foobar호출 할 수 있습니다 baz), 다음 기능 요구로 돌아 알 수 있습니다. 이 "누가 돌아 오는"정보를 중첩하면 스택이됩니다. 무엇을 호출하든 또는 CPU의 하드웨어 또는 소프트웨어에서 에뮬레이션하는 것 (또는 정적으로 할당 된 항목의 링크 된 목록)에 의해 지원되는지 여부는 여전히 스택입니다.
Brendan

@Brendan은 반드시 그런 것은 아닙니다 (적어도, 그것은 내 질문의 전체 목적입니다). "다시 어디로 가야"또는 "다음으로 가야 할 곳"이 스택, 즉 LIFO 구조 여야합니까? 나무,지도, 줄 또는 다른 것이 될 수 있습니까?
Lorenzo Dematté

예를 들어, 내 직감은 CPS에 나무가 필요하다는 것입니다. 그러나 확실하지 않으며 어디를 볼지 모르겠습니다. 그것이 내가 묻는 이유입니다 ..
Lorenzo Dematté

6

일종의 스택을 사용하지 않고 함수 호출 의미론을 구현할 수 없습니다. 단어 게임 만 할 수 있습니다 (예 : "FILO return buffer"와 같은 다른 이름 사용).

함수 호출 시맨틱 (예 : 연속 전달 스타일, 액터)을 구현하지 않는 것을 사용하고 그 위에 함수 호출 시맨틱을 빌드 할 수 있습니다. 그러나 이것은 함수가 반환 될 때 제어가 전달되는 위치를 추적하기 위해 일종의 데이터 구조를 추가하는 것을 의미하며 해당 데이터 구조는 스택 유형 (또는 다른 이름 / 설명이있는 스택)이됩니다.

모두 서로를 호출 할 수있는 많은 기능이 있다고 상상해보십시오. 런타임시 각 함수는 함수가 종료 될 때 어디로 돌아갈 지 알아야합니다. first전화 하면 second다음이 있습니다.

second returns to somewhere in first

그런 다음 second전화 third하면

third returns to somewhere in second
second returns to somewhere in first

그런 다음 third전화 fourth하면

fourth returns to somewhere in third
third returns to somewhere in second
second returns to somewhere in first

각 함수가 호출 될 때 더 많은 "반환 위치"정보를 어딘가에 저장해야합니다.

함수가 리턴하면 "반환 위치"정보가 사용되며 더 이상 필요하지 않습니다. 예를 들어, fourth어딘가 third로 되돌아 가면 "돌아가는 곳"정보의 양은 다음과 같습니다.

third returns to somewhere in second
second returns to somewhere in first

원래; "함수 호출 의미론"은 다음을 의미합니다.

  • "반품 장소"정보가 있어야합니다
  • 함수가 호출 될 때 정보의 양이 증가하고 함수가 반환 될 때 감소
  • 저장된 "반환 위치"정보의 첫 번째 부분은 폐기 된 "반환 위치"정보의 마지막 부분입니다.

FILO / LIFO 버퍼 또는 스택에 대해 설명합니다.

트리 유형을 사용하려고하면 트리의 모든 노드에 둘 이상의 자식이 없습니다. 참고 : 여러 자식이있는 노드는 함수가 동시에 두 개 이상의 함수 호출하는 경우에만 발생할 수 있으며 , 이는 일종의 동시성 (예 : 스레드, fork () 등)이 필요하고 "함수 호출 의미"가 아닙니다. 트리의 모든 노드에 둘 이상의 자식이없는 경우 "트리"는 FILO / LIFO 버퍼 또는 스택으로 만 사용됩니다. FILO / LIFO 버퍼 또는 스택으로 만 사용되기 때문에 "트리"가 스택이라고 주장하는 것이 공정합니다 (단, 차이점은 단어 게임 및 / 또는 구현 세부 사항입니다).

"함수 호출 의미론"을 구현하는 데 사용될 수있는 다른 데이터 구조에도 동일하게 적용됩니다. 이는 스택으로 사용될 것입니다 (단, 차이점은 단어 게임 및 / 또는 구현 세부 사항입니다). "함수 호출 의미론"을 위반하지 않는 한. 참고 : 가능하다면 다른 데이터 구조에 대한 예제를 제공하지만 약간 그럴듯한 다른 구조는 생각할 수 없습니다.

물론 스택을 구현하는 방법은 구현 세부 사항입니다. 메모리 영역 ( "현재 스택 상단"을 추적하는 위치)이거나 일종의 링크 된 목록 ( "현재 목록의 현재 항목"을 추적하는 위치) 일 수 있습니다. 다른 방법으로. 하드웨어가 기본적으로 지원되는지 여부는 중요하지 않습니다.

참고 : 한 번에 하나의 프로 시저 호출 만 활성화 될 수있는 경우; "반환 할 위치"정보를위한 공간을 정적으로 할당 할 수 있습니다. 이것은 여전히 ​​스택입니다 (예 : FILO / LIFO 방식으로 사용되는 정적으로 할당 된 항목의 링크 된 목록).

또한 "함수 호출 의미론"을 따르지 않는 것이 있습니다. 이러한 것들은 "잠재적으로 매우 다른 의미론"(예를 들어 연속 통과, 행위자 모델); 또한 동시성 (스레드, 파이버 등), setjmp/ longjmp, 예외 처리 등과 같은 "함수 호출 의미"에 대한 일반적인 확장을 포함합니다 .


정의에 따르면 스택은 LIFO 모음입니다. 큐는 FIFO 컬렉션입니다.
John R. Strohm 2016 년

그렇다면 스택이 유일하게 수용 가능한 데이터 구조입니까? 그렇다면 왜 그렇습니까?
Lorenzo Dematté

@ JohnR.Strohm : 고정 :-)
Brendan

1
재귀가없는 언어 (직접 또는 돌연변이)의 경우 각 메소드에 대해 해당 메소드가 마지막으로 호출 된 위치를 식별하는 변수를 정적으로 할당 할 수 있습니다. 링커가 그러한 것들을 알고 있다면, 모든 정적으로 가능한 실행 경로가 실제로 취해지면 스택이하는 것보다 나쁘지 않은 방식으로 그러한 변수를 할당 할 수 있습니다 .
supercat

4

완구 연결 언어 XY 는 콜 큐와 데이터 스택을 사용하여 실행합니다.

모든 계산 단계는 단순히 다음에 실행될 단어를 정의하고 내장의 경우 내부 함수에 데이터 스택 및 콜 큐를 인수로 제공하거나 userdefs를 사용하여 단어를 큐의 맨 앞에 밀어 넣습니다.

따라서 상단 요소를 두 배로 늘리는 기능이 있다면 :

; double dup + ;
// defines 'double' to be composed of 'dup' followed by '+'
// dup duplicates the top element of the data stack
// + pops the top two elements and push their sum

그런 다음 구성 기능 +dup다음을 스택 / 큐 일반 서명 :

// X is arbitraty stack, Y is arbitrary queue, ^ is concatenation
+      [X^a^b Y] -> [X^(a + b) Y]
dup    [X^a Y] -> [X^a^a Y]

역설적으로 double다음과 같이 보일 것입니다.

double [X Y] -> [X dup^+^Y]

따라서 XY는 스택이 없습니다.


와우 감사합니다! 나는 그것을 조사 할 것이다 ... 그것이 실제로 함수 호출에 적용되는지는 모르겠지만 어쨌든 살펴볼 가치가있다
Lorenzo Dematté

1
@ Karl Damgaard Asmussen "대기열의 앞쪽으로 단어를 푸쉬하는 것" "앞으로 밀기"그게 스택이지 않습니까?

@ guesttttttt222222222 실제로는 아닙니다. 콜 스택은 리턴 포인터를 저장하고 함수가 리턴하면 콜 스택이 팝됩니다. 실행 큐는 함수에 대한 포인터 만 저장하고 다음 함수를 실행할 때 해당 정의로 확장되어 큐의 전면으로 푸시됩니다. XY에서는 실행 큐의 뒷면에서도 작동하는 작업이 있기 때문에 실행 큐는 실제로 deque입니다.
Karl Damgaard Asmussen
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.