내가 본 대부분의 아키텍처는 함수 호출 전에 컨텍스트를 저장 / 복원하기 위해 호출 스택에 의존합니다. 푸시 및 팝 작업이 대부분의 프로세서에 내장되어 있다는 것은 일반적인 패러다임입니다. 스택없이 작동하는 시스템이 있습니까? 그렇다면 어떻게 작동하며 무엇을 위해 사용됩니까?
내가 본 대부분의 아키텍처는 함수 호출 전에 컨텍스트를 저장 / 복원하기 위해 호출 스택에 의존합니다. 푸시 및 팝 작업이 대부분의 프로세서에 내장되어 있다는 것은 일반적인 패러다임입니다. 스택없이 작동하는 시스템이 있습니까? 그렇다면 어떻게 작동하며 무엇을 위해 사용됩니까?
답변:
콜 스택에 대한 대중적인 대안은 연속 입니다.
예를 들어 Parrot VM은 연속 기반입니다. 데이터는 완전히 스택이 없습니다. 데이터는 Dalvik 또는 LuaVM과 같은 레지스터에 유지되고 Parrot은 레지스터 기반입니다. Dalvik 또는 LuaVM과 달리 호출 스택이있는 제어 흐름은 연속으로 표시됩니다.
Smalltalk 및 Lisp VM에서 일반적으로 사용되는 또 다른 널리 사용되는 데이터 구조는 스택 네트워크와 유사한 스파게티 스택입니다.
으로 @rwong 지적 , 계속 - 패싱 스타일은 호출 스택에 대한 대안입니다. 연속 전달 스타일로 작성되었거나 변환 전달 된 프로그램은 반환되지 않으므로 스택이 필요하지 않습니다.
다른 관점에서 질문에 대한 답변 : 스택 프레임을 힙에 할당하여 별도의 스택 없이도 호출 스택을 가질 수 있습니다. 일부 Lisp 및 Scheme 구현이이를 수행합니다.
예전에는 프로세서에 스택 명령어가 없었고 프로그래밍 언어는 재귀를 지원하지 않았습니다. 시간이 지남에 따라 점점 더 많은 언어가 재귀를 지원하도록 선택했으며 하드웨어는 스택 프레임 할당 기능을 갖춘 제품군을 따릅니다. 이 지원은 프로세서에 따라 수년에 걸쳐 크게 달라졌습니다. 일부 프로세서는 스택 프레임 및 / 또는 스택 포인터 레지스터를 채택했습니다. 단일 명령에서 스택 프레임 할당을 수행하는 일부 채택 명령.
프로세서가 단일 레벨, 다중 레벨 캐시로 발전함에 따라 스택의 중요한 장점 중 하나는 캐시 로컬 리티입니다. 스택의 상단은 거의 항상 캐시에 있습니다. 캐시 적중률이 큰 작업을 수행 할 수있을 때마다 최신 프로세서를 사용하는 것이 좋습니다. 스택에 적용된 캐시는 로컬 변수, 매개 변수 등이 거의 항상 캐시에 있으며 최고 수준의 성능을 누릴 수 있음을 의미합니다.
요컨대 스택 사용은 하드웨어와 소프트웨어 모두에서 발전했습니다. 다른 모델들 (예를 들어, 데이터 흐름 컴퓨팅이 오랜 기간 동안 시도 되었음)이 있지만 스택의 지역 성은 실제로 작동합니다. 또한 절차 적 코드는 성능을 위해 프로세서가 원하는 것입니다. 한 명령은 다른 명령에 따라 수행 할 작업을 지시합니다. 명령어가 선형 순서를 벗어나면 프로세서는 순차 액세스만큼 빠른 임의 액세스를 만드는 방법을 알지 못했기 때문에 적어도 아직 느려집니다. (Btw, 캐시에서 메인 메모리, 디스크에 이르기까지 각 메모리 수준에서 비슷한 문제가 있습니다.)
순차적 액세스 명령의 입증 된 성능과 콜 스택의 유익한 캐싱 동작 사이에는 최소한 현재 성능 모델이 있습니다.
(우리는 데이터 구조의 변경 가능성을 작품에 넣을 수도 있습니다 ...)
그렇다고 다른 프로그래밍 모델이 작동하지 않는다는 의미는 아닙니다. 특히 오늘날의 하드웨어의 순차적 명령어와 호출 스택 모델로 변환 될 수있는 경우에는 더욱 그렇습니다. 그러나 하드웨어가있는 위치를 지원하는 모델에는 뚜렷한 이점이 있습니다. 그러나 상황이 항상 동일하게 유지되는 것은 아니므로 다른 메모리 및 트랜지스터 기술이 더 많은 병렬 처리를 허용하므로 향후 변화를 볼 수 있습니다. 항상 프로그래밍 언어와 하드웨어 기능 사이의 혼란에 빠질 것입니다.
TL; DR
이 답변의 나머지 부분은 무작위로 생각과 일화를 모아서 다소 조직화되지 않은 것입니다.
함수 호출 메커니즘으로 설명한 스택은 명령형 프로그래밍에만 해당됩니다.
명령형 프로그래밍 아래에는 머신 코드가 있습니다. 머신 코드는 작은 명령 시퀀스를 실행하여 호출 스택을 에뮬레이션 할 수 있습니다.
머신 코드 아래에는 소프트웨어 실행을 담당하는 하드웨어가 있습니다. 현대의 마이크로 프로세서는 여기에 설명하기에는 너무 복잡하지만, 매우 단순한 디자인이 느리지 만 여전히 동일한 머신 코드를 실행할 수 있다고 상상할 수 있습니다. 이러한 단순한 디자인은 디지털 로직의 기본 요소를 활용합니다.
다음 논의에는 명령형 프로그램을 구성하는 대안적인 방법의 많은 예가 포함되어 있습니다.
프로그램과 같은 구조는 다음과 같습니다.
void main(void)
{
do
{
// validate inputs for task 1
// execute task 1, inlined,
// must complete in a deterministically short amount of time
// and limited to a statically allocated amount of memory
// ...
// validate inputs for task 2
// execute task 2, inlined
// ...
// validate inputs for task N
// execute task N, inlined
}
while (true);
// if this line is reached, tell the programmers to prepare
// themselves to appear before an accident investigation board.
return 0;
}
이 스타일은 마이크로 컨트롤러, 즉 소프트웨어를 하드웨어 기능의 동반자로 보는 사람들에게 적합합니다.
반드시 그런 것은 아닙니다.
Appel의 오래된 종이 가비지 콜렉션을 읽으면 스택 할당보다 빠를 수 있습니다 . 연속 전달 스타일을 사용 하고 스택리스 구현을 보여줍니다.
또한 이전 컴퓨터 아키텍처 (예 : IBM / 360 )에는 하드웨어 스택 레지스터가 없습니다. 그러나 OS와 컴파일러는 규칙 에 따라 스택 포인터에 대한 레지스터를 예약했습니다 ( 호출 규칙 과 관련 ) . 소프트웨어 호출 스택을 가질 수 있습니다 .
원칙적으로 전체 프로그램 C 컴파일러와 옵티마이 저는 호출 그래프가 정적으로 알려져 있고 재귀 (또는 함수 포인터)가없는 경우 (임베디드 시스템에서 일반적 임)를 감지 할 수 있습니다. 이러한 시스템에서 각 기능은 반환 주소를 고정 된 고정 위치에 유지할 수 있습니다 (그리고 1970 년대 컴퓨터에서 Fortran77이 작동 한 방식이었습니다 ).
요즘 프로세서에는 CPU 캐시를 인식하는 호출 스택 (및 호출 및 반환 시스템 명령어)도 있습니다.
SUBROUTINE
하고 FUNCTION
. 그래도 이전 버전 (FORTRAN-IV 및 가능하면 WATFIV)에 적합합니다.
TR
와 TRT
.
지금까지 좋은 답변이 있습니다. 스택이나 "제어 흐름"이라는 개념없이 언어를 디자인 할 수있는 방법에 대한 실용적이지 않고 교육적인 예를 드리겠습니다. 계승을 결정하는 프로그램은 다음과 같습니다.
function f(i) => if i == 0 then 1 else i * f(i - 1)
let x = f(3)
이 프로그램을 문자열에 넣고 텍스트 대체로 프로그램을 평가합니다. 따라서 평가 할 때 다음과 f(3)
같이 검색을 수행하고 i를 3으로 바꿉니다.
function f(i) => if i == 0 then 1 else i * f(i - 1)
let x = if 3 == 0 then 1 else 3 * f(3 - 1)
큰. 이제 또 다른 텍스트 대체를 수행합니다. "if"의 조건이 false이고 다른 문자열 대체를 수행하여 프로그램을 생성합니다.
function f(i) => if i == 0 then 1 else i * f(i - 1)
let x = 3 * f(3 - 1)
이제 상수를 포함하는 모든 하위 표현식에서 다른 문자열 바꾸기를 수행합니다.
function f(i) => if i == 0 then 1 else i * f(i - 1)
let x = 3 * f(2)
그리고 당신은 이것이 어떻게 진행되는지 봅니다; 나는 더 이상 요점을 밝히지 않을 것이다. 우리는 우리가 끝날 때까지 일련의 문자열 대체를 계속할 수 있었고 우리는 let x = 6
끝났습니다.
우리는 전통적으로 지역 변수와 연속 정보에 스택을 사용합니다. 스택은 당신이 어디에서 왔는지 알려주지 않고, 그 리턴 값을 가지고 다음에 어디로 갈지 알려줍니다.
프로그래밍의 문자열 대체 모델에서는 스택에 "로컬 변수"가 없습니다. 공식 매개 변수는 함수가 스택의 찾아보기 테이블에 배치되지 않고 인수에 적용될 때 해당 값으로 대체됩니다. 프로그램 평가는 단순히 문자열 대체를위한 간단한 규칙을 적용하여 다르지만 동등한 프로그램을 생성하기 때문에 "다음 단계로 진행"은 없습니다.
물론 실제로 문자열 대체를 수행하는 것은 아마도 갈 길이 아닙니다. 그러나 "등식 추론"(예 : Haskell)을 지원하는 프로그래밍 언어는 논리적 으로이 기술을 사용합니다.
1972 년 Parnas가 시스템을 모듈로 분해하는 데 사용되는 기준 에 대한 출판 이후 소프트웨어에 숨겨져있는 정보가 좋은 것이라고 합리적으로 받아 들여졌습니다. 이것은 60 년대에 걸쳐 구조적 분해 및 모듈 식 프로그래밍에 대한 긴 논쟁에 이어 진행됩니다.
다중 스레드 시스템에서 서로 다른 그룹에 의해 구현 된 모듈 간의 블랙 박스 관계에 필요한 결과는 재진입을 허용하는 메커니즘과 시스템의 동적 호출 그래프를 추적하는 수단이 필요합니다. 제어 된 실행 흐름은 여러 모듈로 들어오고 나가야합니다.
어휘 범위가 동적 동작을 추적하기에 충분하지 않은 경우, 차이를 추적하기 위해 일부 런타임 부기 (runkeeping)가 필요합니다.
모든 스레드 (정의에 따라)에 단일 현재 명령 포인터 만있는 경우 각 호출을 추적하는 데 LIFO 스택이 적합합니다.
따라서 연속 모델은 스택에 대해 명시 적으로 데이터 구조를 유지 관리하지 않지만 어딘가에 유지되어야하는 중첩 된 모듈 호출이 여전히 있습니다!
선언적 언어조차도 평가 이력을 유지하거나 성능상의 이유로 실행 계획을 반대로 전개하고 다른 방식으로 진행 상황을 유지합니다.
rwong 으로 식별되는 무한 루프 구조 는 정적 스케줄링을 사용하는 고 신뢰성 응용 프로그램에서 일반적이며 많은 일반적인 프로그래밍 구조를 허용하지 않지만 전체 응용 프로그램을 중요한 정보 숨기기없이 흰색 상자로 간주해야합니다.
여러 개의 동시 무한 루프는 함수를 호출하지 않기 때문에 리턴 주소를 보유하는 구조가 필요하지 않으므로 질문이 모호합니다. 공유 변수를 사용하여 통신하면 레거시 포트란 스타일의 반환 주소 아날로그로 쉽게 변질 될 수 있습니다.
모든 이전 메인 프레임 (IBM System / 360)에는 스택 개념이 전혀 없었습니다. 예를 들어, 260에서 매개 변수는 메모리의 고정 된 위치에 구성되었으며 서브 루틴이 호출 될 때 R1
매개 변수 블록 을 가리키고 R14
리턴 주소를 포함하여 호출되었습니다 . 호출 된 루틴은 다른 서브 루틴을 호출하려는 경우 R14
해당 호출을하기 전에 알려진 위치 에 저장 해야합니다.
모든 것이 컴파일 타임에 설정된 고정 메모리 위치에 저장 될 수 있고 프로세스가 스택을 다 사용하지 않도록 100 % 보장 할 수 있기 때문에 이것은 스택보다 훨씬 더 안정적입니다. 오늘날 우리가해야 할 "1MB 할당 및 손가락 교차"는 없습니다.
키워드를 지정하여 PL / I에서 재귀 서브 루틴 호출이 허용되었습니다 RECURSIVE
. 이는 서브 루틴이 사용하는 메모리가 정적으로 할당되지 않고 동적으로 할당되었음을 의미합니다. 그러나 재귀 호출은 지금처럼 드물었습니다.
스택리스 작업은 대규모 멀티 스레딩을 훨씬 쉽게 만들어 주므로 현대 언어를 스토킹없이 만들려고하는 경우가 많습니다. 예를 들어 C ++ 컴파일러를 스택 대신 동적으로 할당 된 메모리를 사용하도록 백엔드로 수정할 수없는 이유는 전혀 없습니다.