중첩 함수 호출을 인라인 할 수있는 경우 프로그램에서 호출 스택을 사용하는 이유는 무엇입니까?


32

컴파일러가 다음과 같은 프로그램을 사용하지 않는 이유는 무엇입니까?

function a(b) { return b^2 };
function c(b) { return a(b) + 5 };

다음과 같은 프로그램으로 변환하십시오.

function c(b) { return b^2 + 5 };

따라서 컴퓨터가 c (b)의 반송 주소를 기억할 필요가 없습니까?

프로그램을 저장하고 컴파일을 지원하는 데 필요한 하드 디스크 공간과 RAM이 증가했기 때문에 콜 스택을 사용하는 이유라고 생각합니다. 그 맞습니까?


30
의미있는 크기의 프로그램에서이 작업을 수행하면 어떻게되는지 확인하십시오. 특히 함수는 여러 곳에서 호출됩니다.
user253751

10
또한 때때로 컴파일러는 어떤 함수가 호출되는지 알지 못합니다! 바보 같은 예 :window[prompt("Enter function name","")]()
user253751

26
function(a)b { if(b>0) return a(b-1); }스택없이 어떻게 구현 합니까?
pjc50

8
함수형 프로그래밍과의 관계는 어디에 있습니까?
mastov 2016 년

14
@ pjc50 : 꼬리 재귀이므로 컴파일러는 그것을 mutable을 가진 루프로 변환합니다 b. 그러나 모든 재귀 함수가 재귀를 제거 할 수있는 것은 아니며, 함수가 원칙적으로 가능하더라도 컴파일러가 그렇게 똑똑하지 않을 수도 있습니다.
Steve Jessop

답변:


75

이를 "인라이닝"이라고하며 많은 컴파일러가이를 이해하는 경우이를 최적화 전략으로 수행합니다.

특정 예에서이 최적화는 공간과 실행 시간을 모두 절약합니다. 그러나 함수가 프로그램의 여러 위치에서 호출되면 (드문 일이 아닙니다!) 코드 크기가 커져 전략이 더 모호해집니다. (물론 직접 또는 간접적으로 함수 자체를 호출하면 인라인하는 것이 불가능할 것입니다. 왜냐하면 코드의 크기는 무한대가 될 것입니다.)

그리고 분명히 "개인"기능 만 가능합니다. 외부 호출자에게 노출되는 기능은 동적 링크가있는 언어로는 최적화 할 수 없습니다.


7
@Blrfl : 현대 컴파일러는 더 이상 헤더에 정의가 필요하지 않습니다. 번역 단위에서 인라인 할 수 있습니다. 그래도 괜찮은 링커가 필요합니다. 헤더 파일의 정의는 바보 링커에 대한 해결 방법입니다.
MSalters 2016 년

3
"외부 발신자에게 노출되는 기능은 최적화 할 수 없습니다"– 기능이 존재해야하지만 특정 호출 사이트 (자신의 코드 또는 소스가있는 경우 외부 발신자)에 인라인 될 수 있습니다.
Random832

14
와우, 28은 모든 것을 인라인하는 것이 불가능한 이유를 언급하지 않는 대답에 찬성했습니다.
mastov

3
@R .. : LTO는 LOAD 시간 최적화가 아니라 LINK 시간 최적화입니다.
MSalters 2016 년

2
@immibis : 그러나 컴파일러가 명시 적 스택을 도입하면 해당 스택 호출 스택입니다.
user2357112는 Monica

51

질문에는 두 가지 부분이 있습니다. 왜 함수 호출을 정의로 바꾸는 대신 여러 함수를 사용하고 왜 다른 곳에 데이터를 정적으로 할당하는 대신 호출 스택으로 해당 함수를 구현해야합니까?

첫 번째 이유는 재귀입니다. "이 목록의 모든 단일 항목에 대해 새 함수 호출을 작성하십시오"유형뿐만 아니라 동시에 두 개의 함수 호출이 활성화되어 있고 그 사이에 많은 다른 함수가있는 적절한 종류도 있습니다. 이를 지원하기 위해 로컬 변수를 스택에 넣어야하며 일반적으로 재귀 함수를 인라인 할 수 없습니다.

그런 다음 라이브러리에 문제가 있습니다. 어느 함수가 언제 어디서 자주 호출되는지 알 수 없으므로 "라이브러리"를 실제로 컴파일 할 수 없으며 모든 클라이언트에게 편리한 고급 형식으로 만 제공됩니다. 응용 프로그램에 인라인됩니다. 이것에 대한 다른 문제 외에도 모든 장점을 가진 동적 연결이 완전히 손실됩니다.

또한 다음과 같은 경우에도 함수를 인라인하지 않는 데는 여러 가지 이유가 있습니다.

  1. 반드시 더 빠를 필요는 없습니다. 스택 프레임을 설정하고 분해하는 것은 실행 시간의 0.1 %가 아닌 많은 대형 또는 루핑 기능의 경우 단일 사이클 명령 일 수 있습니다.
  2. 느려질 수 있습니다. 코드 복제에는 비용이들 수 있습니다. 예를 들어 명령 캐시에 더 많은 압력이 가해집니다.
  3. 일부 함수는 매우 크고 많은 곳에서 호출되므로 모든 곳에서 함수를 인라인하면 바이너리가 합리적 수준 이상으로 증가합니다.
  4. 컴파일러는 종종 매우 큰 기능으로 어려움을 겪습니다. 크기가 2 * N 인 함수는 크기가 N 인 함수에 T 시간이 걸리는 2 * T 이상의 시간이 걸립니다.

1
포인트 4에 놀랐습니다. 그 이유는 무엇입니까?
JacquesB

12
@JacquesB 많은 최적화 알고리즘은 2 차, 3 차 또는 기술적으로 NP- 완전합니다. 일반적인 예는 레지스터 할당으로, 그래프 채색과 유사하게 NP가 완료됩니다. (일반적으로 컴파일러는 정확한 솔루션을 시도하지 않지만, 매우 빈약 한 휴리스틱 몇 개만 선형 시간으로 실행합니다.) 많은 간단한 원 패스 최적화에는 제어 흐름의 지배력 (일반적으로 지배력에 의존하는 모든 것)과 같은 초 선형 분석 단계가 먼저 필요합니다. n 기본 블록이있는 n log n 시간).

2
"여기에 정말 두 가지 질문이 있습니다."아닙니다. 단지 하나-함수 호출을 컴파일러가 호출 된 함수의 코드로 대체 할 수있는 자리 표시 자로 취급하지 않는 이유는 무엇입니까?
moonman239

4
@ moonman239 그러면 당신의 말이 나를 버렸습니다. 그럼에도 불구하고 귀하의 질문 내 대답에서와 같이 분해 될 있으며 이것이 유용한 관점이라고 생각합니다.

16

스택을 사용하면 유한 한 수의 레지스터에 의해 부과 된 한계를 우아하게 우회 할 수 있습니다.

정확히 26 개의 전역이 "z를 등록"한다고 생각한다고 상상해보십시오 (또는 8080 칩의 7 바이트 크기 레지스터 만 포함).이 앱에서 작성하는 모든 기능은이 플랫리스트를 공유합니다.

순진한 시작은 첫 번째 몇 개의 레지스터를 첫 번째 함수에 할당하는 것입니다. 두 번째 함수의 경우 "d"로 시작하여 3 개만 걸린다는 것을 알고 있습니다.

대신, 튜링 머신과 같은 은유 적 테이프를 사용하는 경우, 사용하는 모든 변수를 테이프 에 저장 하고 전달 하여 () 테이프를 사용하여 각 함수가 "다른 함수 호출"을 시작하도록 할 수 있습니다. 원하는대로 등록합니다. 수신자가 완료되면 필요에 따라 수신자의 출력을 잡아야 할 위치를 알고있는 부모 기능으로 제어를 리턴 한 다음 테이프를 뒤로 재생하여 상태를 복원합니다.

기본 호출 프레임은 바로 그 것이며 컴파일러가 한 함수에서 다른 함수로 전환하는 표준화 된 기계 코드 시퀀스에 의해 생성 및 삭제됩니다. (C 스택 프레임을 기억해야한지 오래되었지만 X86_calling_conventions 에서 누가 무엇을 떨어 뜨릴 지에 대한 의무를 다양한 방법으로 읽을 수 있습니다 .)

(재귀는 훌륭하지만 스택없이 레지스터를 저글링해야한다면 스택에 정말로 감사 할 것 입니다.)


프로그램을 저장하고 컴파일을 지원하는 데 필요한 하드 디스크 공간과 RAM이 증가했기 때문에 콜 스택을 사용하는 이유라고 생각합니다. 그 맞습니까?

요즘 더 많이 인라인 할 수는 있지만 ( "더 빠른 속도"는 항상 좋습니다. "더 적은 kb의 어셈블리"는 비디오 스트림의 세계에서는 거의 의미가 없습니다.) 주요 제한 사항은 특정 유형의 코드 패턴에서 평탄화하는 컴파일러의 능력에 있습니다.

예를 들어, 다형성 객체-처리 할 객체의 유일한 유형을 모르면 평평 할 수 없습니다. 객체의 기능 테이블을보고 그 포인터를 호출해야합니다 ... 런타임에하는 것은 쉽지만 컴파일 타임에는 인라인 할 수 없습니다.

최신 툴체인은 호출자가 충분히 펑크하면 obj가 어떤 풍미 인지 정확히 있을 때 다형성으로 정의 된 함수를 행복하게 인라인 할 수 있습니다 .

class Base {
    public: void act() = 0;
};
class Child1: public Base {
    public: void act() {};
};
void ActOn(Base* something) {
    something->act();
}
void InlineMe() {
    Child1 thingamabob;
    ActOn(&thingamabob);
}

위의 컴파일러는 InlineMe에서 act () 내부의 모든 것을 통해 정적으로 인라인을 유지하거나 런타임에 vtable을 만질 필요가 없도록 선택할 수 있습니다.

그러나 동일한 기능의 다른 호출 인라인되어 있어도 객체의 풍미에 대한 불확실성으로 인해 개별 함수에 대한 호출로 남을 수 있습니다.


11

해당 접근법이 처리 할 수없는 경우 :

function fib(a) { if(a>2) return fib(a-1)+fib(a-2); else return 1; }

function many(a) { for(i = 1 to a) { b(i); };}

있습니다 제한 또는 전혀 호출 스택 언어와 플랫폼. PIC 마이크로 프로세서의 하드웨어 스택은 2-32 개 항목으로 제한됩니다 . 이것은 디자인 제약을 만듭니다.

COBOL은 재귀를 금지합니다 : https://stackoverflow.com/questions/27806812/in-cobol-is-it-possible-to-recursively-call-a-paragraph

재귀를 금지한다는 것은 프로그램의 전체 호출 그래프를 정적으로 DAG로 나타낼 수 있음을 의미합니다. 그런 다음 컴파일러는 각 위치에 대해 함수 대신 복사본 대신 고정 된 점프로 함수의 복사본 하나를 생성 할 수 있습니다. 스택이 필요하지 않고 더 많은 프로그램 공간이 필요하며 복잡한 시스템의 경우 잠재적으로 상당히 많습니다. 그러나 소형 임베디드 시스템의 경우 런타임에 스택 오버플로가 발생하지 않도록 보장 할 수 있으며 이는 원자로 / 제트 터빈 / 자동차 스로틀 제어 등에 나쁜 소식입니다.


12
첫 번째 예는 기본 재귀이며 정확합니다. 그러나 두 번째 예제는 다른 함수를 호출하는 for 루프 인 것 같습니다. 인라인 기능은 루프를 언 롤링하는 것과 다릅니다. 루프를 풀지 않고도 함수를 인라인 할 수 있습니다. 아니면 미묘한 세부 사항을 놓쳤습니까?
jpmc26 2016 년

1
첫 번째 예가 피보나치 시리즈를 정의하려는 경우에는 잘못된 것입니다. ( fib전화 가 없습니다 .)
Paŭlo Ebermann

1
재귀를 금지한다는 것은 전체 통화 그래프가 DAG로 표현 될 수 있다는 것을 의미하지만, 합리적인 양의 공간에서 중첩 된 통화 시퀀스의 전체 목록을 나열 할 수있는 것은 아닙니다. 128KB의 코드 공간을 가진 마이크로 컨트롤러의 한 프로젝트에서 최대 매개 변수 -RAM 요구 사항에 영향을 줄 수있는 모든 기능을 포함하는 호출 그래프를 요구하는 실수를 범했고 그 호출 그래프는 공연을 넘어 섰습니다. 완전한 호출 그래프는 훨씬 길었고 128K 코드 공간에 맞는 프로그램이었습니다.
supercat

8

함수 인라이닝 을 원하고 대부분의 ( 최적화 ) 컴파일러가 그렇게하고 있습니다.

인라이닝은 호출 된 함수를 알아야합니다 (그리고 호출 된 함수가 너무 크지 않은 경우에만 효과적 임). 개념적으로 호출 된 함수를 다시 작성하여 호출을 대체하기 때문입니다. 따라서 일반적으로 알 수없는 함수 (예 : 함수 포인터- 동적 링크 된 공유 라이브러리의 함수를 포함하는 함수 포인터)를 인라인 할 수 없습니다 ( 일부는 vtable 에서 가상 메소드로 볼 수 있지만 일부 컴파일러는 때로는 가상화 기술을 통해 최적화 할 수 있음 ). 물론이 재귀 함수를 인라인 할 수없는 경우도 중 (일부 영리한 컴파일러가 사용할 수있는 부분적인 평가를 하고에 어떤 경우 재귀 함수를 인라인 할 수 있습니다).

인라인은 쉽게 가능하더라도 항상 효과적이지는 않습니다. 실제로 (컴파일러) CPU 캐시 (또는 분기 예측기 )가 덜 효율적으로 작동하고 프로그램이 실행되도록 코드 크기를 크게 늘릴 수 있습니다. 느리게.

qestion에 태그를 지정했기 때문에 함수형 프로그래밍 스타일 에 중점을 둡니다 .

당신이 상관 할 필요가 없습니다 호출 스택 (이하 "호출 스택"표현의 기계 감각에 이상을). 힙만 사용할 수 있습니다.

따라서 연속을 살펴보고 연속 전달 스타일 (CPS) 및 CPS 변환 에 대해 자세히 알아보십시오 (직관적으로 연속 클로저 를 힙에 할당 된 "호출 프레임"으로 사용할 수 있으며 호출 스택을 모방 한 것입니다. 효율적인 가비지 수집기 가 필요합니다 ).

Andrew Appel은 Continuations로 컴파일 하는 책을 썼으며 오래된 종이 가비지 콜렉션은 스택 할당보다 빠를 수 있습니다 . A.Kennedy의 논문 (ICFP2007) 계속 컴파일, 계속 참조

또한 Queinnec의 Lisp In Small Pieces 책을 읽는 것이 좋습니다.이 책에는 연속 및 편집과 관련된 여러 장이 있습니다.

또한 일부 언어 (예 : Brainfuck ) 또는 추상 기계 (예 : OISC , RAM )에는 호출 기능이 없지만 여전히 Turing-complete 이므로 (이론적으로는) 어떤 함수 호출 메커니즘도 필요하지 않습니다. 매우 편리합니다. BTW, 일부 이전 명령어 세트 아키텍처 (예 : IBM / 370 )에는 하드웨어 콜 스택 또는 푸시 콜 머신 명령어도 없습니다 (IBM / 370에는 지점 및 링크 머신 명령어 만 있음)

마지막으로 전체 프로그램 (필요한 모든 라이브러리 포함)에 재귀가없는 경우 각 함수의 반환 주소 (및 실제로 정적이되는 "로컬"변수)를 정적 위치에 저장할 수 있습니다. 오래된 Fortran77 컴파일러는 1980 년대 초에 그렇게했습니다 (따라서 컴파일 된 프로그램은 그 당시에 호출 스택을 사용하지 않았습니다).


2
CPS에 "호출 스택"이 없다는 것은 매우 논쟁의 여지가 있습니다. 그것은에 아닙니다 스택 을 통해 하드웨어 지원의 조금이 일반 RAM의 신비 지역 %esp등,하지만 여전히 RAM의 다른 지역에서 적절하게 이름 스파게티 스택에 해당하는 회계 장부를 유지합니다. 특히 반송 주소는 본질적으로 연속적으로 인코딩된다. 물론 인라인을 통해 전화를 전혀하지 않는 것보다 연속성이 빠르지 않습니다 (그리고 이것이 OP가 얻는 것 같습니다) .

Appel의 오래된 논문은 CPS가 콜 스택만큼 빠를 수 있다고 주장했습니다.
Basile Starynkevitch 2016 년

나는 그것에 대해 회의적이지만 그것이 무엇이든 내가 주장한 것은 아닙니다.

1
실제로 이것은 1980 년대 후반 MIPS 워크 스테이션이었습니다. 아마도 현재 PC의 캐시 계층 구조는 성능이 약간 다를 수 있습니다. Appel의 주장을 분석 한 논문이 여러 건 있습니다 (실제로 현재 머신에서는 스택 할당이 신중하게 만들어진 가비지 수집보다 몇 퍼센트 정도 약간 더 빠를 수 있음 )
Basile Starynkevitch 2016 년

1
@Gilles : Cortex M0 및 M3 (및 아마도 M4와 같은 다른)과 같은 많은 최신 ARM 코어는 인터럽트 처리와 같은 하드웨어 스택을 지원합니다. 또한 Thumb 명령어 세트에는 LR이 있거나없는 R0-R7과 LR이 있거나없는 R0-R7의 조합이있는 STRMDB R13 및 PC가 있거나없는 R0-R7의 임의 조합의 LDRMIA R13이 포함 된 STRM / STRM 명령어의 제한된 서브 세트가 포함됩니다. 스택 포인터로서의 R13.
supercat

8

인라이닝 (동일한 기능으로 함수 호출 대체)은 작은 간단한 함수에 대한 최적화 전략으로 작동합니다. 함수 호출의 오버 헤드는 추가 된 프로그램 크기의 작은 페널티 (또는 경우에 따라 페널티가없는)에 대해 효과적으로 교환 될 수 있습니다.

그러나 다른 기능을 호출하는 큰 기능은 모든 것이 인라인 된 경우 프로그램 크기가 엄청나게 폭발 할 수 있습니다.

호출 가능한 기능의 핵심은 프로그래머뿐만 아니라 기계 자체에 의한 효율적인 재사용을 용이하게하는 것이며 합리적인 메모리 또는 디스크 공간을 차지하는 공간과 같은 속성을 포함합니다.

그만한 가치가 있습니다 : 호출 스택없이 호출 가능한 함수를 가질 수 있습니다. 예를 들어, IBM System / 360입니다. 해당 하드웨어에서 FORTRAN과 같은 언어로 프로그래밍 할 때 프로그램 카운터 (반환 주소)는 함수 진입 점 바로 앞에 예약 된 작은 메모리 섹션에 저장됩니다. 재사용 가능한 기능은 허용하지만 재귀 또는 다중 스레드 코드는 허용하지 않습니다 (재귀 또는 재진입 호출을 시도하면 이전에 저장된 리턴 주소를 덮어 쓰게됩니다).

다른 답변에서 설명했듯이 스택은 좋은 것입니다. 재귀 및 다중 스레드 호출을 용이하게합니다. 재귀를 사용하도록 코딩 된 알고리즘은 재귀에 의존하지 않고 코딩 될 수 있지만 결과는 더 복잡하고 유지 관리가 어려우며 효율성이 떨어질 수 있습니다. 스택리스 아키텍처가 멀티 스레딩을 전혀 지원할 수 있을지 모르겠습니다.

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