const expr을 그렇게 빨리 평가할 수있는 방법


13

컴파일 타임에 평가되는 const 표현식을 시험해 보았습니다. 그러나 컴파일 타임에 실행될 때 엄청나게 빠른 예를 들었습니다.

#include<iostream> 

constexpr long int fib(int n) { 
    return (n <= 1)? n : fib(n-1) + fib(n-2); 
} 

int main () {  
    long int res = fib(45); 
    std::cout << res; 
    return 0; 
} 

이 코드를 실행하면 실행하는 데 약 7 초가 걸립니다. 여태까지는 그런대로 잘됐다. 내가 변경할 때 long int res = fib(45)까지 const long int res = fib(45)그것은 심지어 두 번째를하지합니다. 내 이해로는 컴파일 타임에 평가됩니다. 그러나 컴파일에는 약 0.3 초가 걸립니다.

컴파일러는 이것을 어떻게 그렇게 빨리 평가할 수 있지만 런타임에 훨씬 더 많은 시간이 걸립니까? gcc 5.4.0을 사용하고 있습니다.


7
컴파일러가 함수 호출을 캐시한다고 추측합니다 fib. 위의 피보나치 수의 구현은 너무 느립니다. 런타임 코드에서 함수 값을 캐싱하면 훨씬 빠릅니다.
n314159

4
이 재귀 피보나치는 엄청나게 비효율적입니다 (지수 적 런타임이 있음). 제 생각에 컴파일 시간 평가가 이것보다 더 영리하고 계산을 최적화합니다.
Blaze

1
@AlanBirtles 예 -O3으로 컴파일했습니다.
Peter234

1
우리는 컴파일러가 함수 호출을 캐시한다고 가정하면 함수는 2 ^ 45 번 대신 46 번 (각 가능한 인수 0-45에 대해 한 번만) 풀어야한다고 가정합니다. 그러나 gcc가 그렇게 작동하는지 모르겠습니다.
churill

3
@Someprogrammerdude 내가 알고 있습니다. 그러나 런타임에 평가에 많은 시간이 걸리면 어떻게 컴파일이 그렇게 빨라질 수 있습니까?
Peter234

답변:


5

컴파일러는 더 작은 값을 캐시하므로 런타임 버전만큼 재 계산할 필요가 없습니다.
(최적화는 매우 우수하며 이해할 수없는 특별한 경우가있는 속임수를 포함하여 많은 코드를 생성합니다. 순진한 2 ^ 45 재귀에는 몇 시간이 걸립니다.)

이전 값도 저장하는 경우 :

int cache[100] = {1, 1};

long int fib(int n) {
    int res = cache[n];
    return res ? res : (cache[n] = fib(n-1) + fib(n-2));
} 

런타임 버전은 컴파일러보다 훨씬 빠릅니다.


캐싱을하지 않으면 재귀를 두 번 피하는 방법은 없습니다. 옵티마이 저가 일부 캐싱을 구현한다고 생각하십니까? 컴파일러 출력에서 ​​이것을 보여줄 수 있습니까? 정말 흥미로울까요?
수마

... 캐싱 컴파일러 대신 컴파일러가 fib (n-2)와 fib (n-1) 사이의 관계를 증명할 수 있으며 fib (n-1)을 호출하는 대신 fib (n-2)에 사용합니다. ) 값으로 계산합니다. constexpr을 제거하고 -O2를 사용하여 5.4의 출력에서 ​​볼 수있는 것과 일치한다고 생각합니다.
수마

1
컴파일 타임에 수행 할 수있는 최적화를 설명하는 링크 또는 기타 소스가 있습니까?
Peter234

관찰 가능한 동작이 변경되지 않는 한 옵티마이 저는 거의 모든 것을 자유롭게 수행 할 수 있습니다. 주어진 fib기능에는 부작용이 없으며 (외부 변수는 참조하지 않고 출력은 입력에만 의존) 영리한 최적화 프로그램을 통해 많은 작업을 수행 할 수 있습니다.
수마

@Suma 한 번만 되풀이해도 문제가 없습니다. 반복 버전이 있기 때문에 물론 꼬리 재귀와 같은 재귀 버전도 있습니다.
Ctx

1

5.4 기능이 완전히 제거되지는 않았으므로 흥미로울 수 있습니다. 최소 6.1 이상이 필요합니다.

캐싱이 발생하지 않는다고 생각합니다. 나는 옵티마이 저가 두 번째 호출 사이의 관계를 입증 fib(n - 2)하고 fib(n-1)완전히 피할 수 있을만큼 똑똑하다고 확신합니다 . 이것은 no constexpr및 -O2를 갖는 GCC 5.4 출력 (godbolt에서 얻음)입니다 .

fib(long):
        cmp     rdi, 1
        push    r12
        mov     r12, rdi
        push    rbp
        push    rbx
        jle     .L4
        mov     rbx, rdi
        xor     ebp, ebp
.L3:
        lea     rdi, [rbx-1]
        sub     rbx, 2
        call    fib(long)
        add     rbp, rax
        cmp     rbx, 1
        jg      .L3
        and     r12d, 1
.L2:
        lea     rax, [r12+rbp]
        pop     rbx
        pop     rbp
        pop     r12
        ret
.L4:
        xor     ebp, ebp
        jmp     .L2

나는 -O3으로 출력을 이해하지 못한다는 것을 인정해야한다. 생성 된 코드는 놀랍게도 복잡하고 많은 메모리 액세스와 포인터 산술이 가능하며 이러한 설정으로 일부 캐싱 (메모리)이 가능합니다.


나는 내가 틀렸다고 생각한다. .L3에 루프가 있으며, fib가 모든 하위 fib에 루핑됩니다. -O2를 사용하면 여전히 지수입니다.
수마
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.