반복보다 반복이 더 빠릅니까?


286

나는 재귀가 때로는 반복보다 훨씬 깨끗하다는 것을 알고 있으며 반복에 대해 재귀를 사용해야 할 때에 대해 묻지 않고 이미 그것에 대해 많은 질문이 있다는 것을 알고 있습니다.

내가 묻는 것은 루프보다 재귀 훨씬 빠릅 니까? 나에게는 루프가 항상 새로운 스택 프레임을 설정하지 않기 때문에 루프를 세분화하고 재귀 함수보다 더 빠르게 수행 할 수있는 것처럼 보입니다.

특히 정렬 기능, 이진 트리 등과 같이 데이터를 처리하는 올바른 방법 인 재귀가 빠른 응용 프로그램에서 재귀가 더 빠른지 여부를 찾고 있습니다.


3
때때로 일부 재발에 대한 반복 절차 또는 폐쇄 형 공식이 등장하는 데 몇 세기가 걸립니다. 그 당시에 만 재귀가 더 빠르다고 생각합니다 :) lol
Pratik Deoghare

24
나 자신을 말하면 반복을 선호합니다. ;-)
반복자



@PratikDeoghare 아니오, 문제는 완전히 다른 알고리즘을 선택하는 것이 아닙니다. 재귀 함수는 항상 루프를 사용하는 동일하게 작동하는 메소드로 변환 할 수 있습니다. 예를 들어, 이 답변에는 재귀 및 루핑 형식 모두에서 동일한 알고리즘이 있습니다 . 일반적으로 재귀 함수에 대한 인수 튜플을 스택에 넣고 호출 할 스택으로 푸시하고 스택에서 버리고 함수에서 반환합니다.
TamaMcGlinn

답변:


358

사용되는 언어에 따라 다릅니다. 당신은 '언어 불가지론'을 썼으므로, ​​몇 가지 예를 들겠습니다.

Java, C 및 Python에서 재귀는 새로운 스택 프레임의 할당이 필요하기 때문에 반복 (일반적으로)에 비해 상당히 비쌉니다. 일부 C 컴파일러에서는 컴파일러 플래그를 사용하여이 오버 헤드를 제거하여 특정 유형의 재귀 (실제로 특정 유형의 테일 호출)를 함수 호출 대신 점프로 변환 할 수 있습니다.

함수형 프로그래밍 언어 구현에서 때때로 반복이 매우 비싸고 재귀가 매우 저렴할 수 있습니다. 많은에서 재귀 간단한 점프로 변환되지만, (변경할 수있다), 루프 변수를 변경하는 것은 종종 , 특히 다수의 실행 스레드를 지원하는 구현 예에서, 몇몇 비교적 무거운 작업을 필요로한다. 두 환경이 동시에 실행될 수있는 경우, 뮤 테이터와 가비지 콜렉터 간의 상호 작용으로 인해 일부 환경에서는 뮤 테이션이 비쌉니다.

일부 Scheme 구현에서 재귀는 일반적으로 루핑보다 빠릅니다.

요컨대, 답변은 코드와 구현에 따라 다릅니다. 원하는 스타일을 사용하십시오. 기능적 언어를 사용하는 경우 재귀 더 빠를 있습니다. 당신이 필수적 언어를 사용하는 경우, 반복은 아마 더 빨리. 일부 환경에서는 두 가지 방법 모두 동일한 어셈블리가 생성됩니다 (파이프에 넣고 담배를 피 웁니다).

부록 : 일부 환경에서 가장 좋은 대안은 재귀 나 반복이 아니라 고차 함수입니다. 여기에는 "map", "filter"및 "reduce"( "fold"라고도 함)가 포함됩니다. 이러한 스타일이 선호되는 스타일 일뿐만 아니라 자주 더 깨끗할뿐만 아니라 일부 환경에서는 이러한 기능이 자동 병렬화 기능을 향상시키는 첫 번째 (또는 유일한) 기능이므로 반복 또는 재귀보다 훨씬 빠릅니다. Data Parallel Haskell이 그러한 환경의 예입니다.

리스트 이해는 또 다른 대안이지만, 일반적으로 반복, 재귀 또는 고차 함수의 구문 설탕입니다.


48
나는 그것을 +1하고 "재귀"와 "루프"는 인간이 코드의 이름을 짓는 것이라고 언급하고 싶다. 성능에 중요한 것은 물건 을 명명 하는 방법이 아니라 그것들이 어떻게 컴파일 / 해석되는지에 있습니다. 정의에 따르면 재귀는 수학적 개념이며 스택 프레임 및 어셈블리와 거의 관련이 없습니다.
P Shved

1
또한 재귀는 일반적으로 기능적 언어에서보다 자연스러운 접근 방식이며 반복은 일반적으로 명령형 언어에서보다 직관적입니다. 성능 차이는 눈에 띄지 않을 수 있으므로 특정 언어에 대해 더 자연스러운 느낌을 가진 것을 사용하십시오. 예를 들어 재귀가 훨씬 간단한 경우 Haskell에서 반복을 사용하지 않을 수 있습니다.
Sasha Chedygov

4
일반적으로 재귀는 루프 로 컴파일 되며 루프는 하위 레벨 구성입니다. 왜? 재귀는 일반적으로 일부 데이터 구조에서 잘 확립되므로 초기 F- 대수 를 유도하고 (재귀) 계산의 구조에 대한 유도 인수와 함께 종료에 대한 일부 속성을 증명할 수 있습니다. 재귀가 루프로 컴파일되는 프로세스는 테일 호출 최적화입니다.
Kristopher Micinski

가장 중요한 것은 수행되지 않은 작업입니다. "IO"가 많을수록 더 처리해야합니다. Un-IOing 데이터 (일명 인덱싱)는 시스템을 처음 처리 할 필요가 없기 때문에 항상 모든 시스템에서 가장 큰 성능 향상입니다.
Jeff Fischer

53

루프보다 재귀가 더 빠릅니까?

아니요, 반복은 항상 재귀보다 빠릅니다. (Von Neumann 아키텍처에서)

설명:

일반 컴퓨터의 최소 작업을 처음부터 작성하는 경우 "반복"이 먼저 빌딩 블록으로 제공되고 "재귀"보다 리소스 집약도가 낮 으면 ergo가 더 빠릅니다.

의사 컴퓨팅 머신을 처음부터 구축 :

스스로 에게 질문하십시오 : 알고리즘을 따르고 결과에 도달 하기 위해 값 을 계산 하려면 무엇이 필요 합니까?

우리는 처음부터 시작하여 기본 핵심 개념을 처음부터 정의한 후 개념의 두 번째 레벨 개념을 구축하는 등 개념의 계층 구조를 설정합니다.

  1. 첫 번째 개념 : 메모리 셀, 저장, 주 . 무언가를하려면 최종 및 중간 결과 값을 저장할 장소 가 필요 합니다. Memory , M [0..Infinite] 라는 "정수"셀의 무한 배열이 있다고 가정 해 봅시다 .

  2. 지침 : 무언가를하십시오-셀을 변형시키고 그 값을 변경하십시오. 상태를 변경하십시오 . 모든 흥미로운 명령은 변형을 수행합니다. 기본 지침은 다음과 같습니다.

    a) 메모리 셀 설정 및 이동

    • 값을 메모리에 저장한다. 예 : store 5 m [4]
    • 값을 다른 위치로 복사하십시오. 예 : store m [4] m [8]

    b) 논리와 산술

    • 그리고, 또는, 또는
    • 추가, 하위, mul, div. 예를 들어 m [7] m [8] 추가
  3. Executing Agent : 최신 CPU 의 핵심 . "에이전트"는 명령어를 실행할 수있는 것입니다. 에이전트는 또한 종이에 알고리즘 다음과 같은 사람이 될 수 있습니다.

  4. 단계 순서 : 일련의 명령 : 즉 : 먼저 수행하고, 이후에 수행하십시오. 명령의 필수 순서. 한 줄 조차도 "필수적인 명령 순서"입니다. 특정 "평가 순서"가있는 표현식이있는 경우 단계가 있습니다. 이는 하나의 작성된 표현식조차도 암시 적 "단계"를 가지며 암시 적 로컬 변수도 갖습니다 ( "결과"라고 함). 예 :

    4 + 3 * 2 - 5
    (- (+ (* 3 2) 4 ) 5)
    (sub (add (mul 3 2) 4 ) 5)  
    

    위의 표현은 암시 적 "결과"변수가있는 3 단계를 의미합니다.

    // pseudocode
    
           1. result = (mul 3 2)
           2. result = (add 4 result)
           3. result = (sub result 5)
    

    따라서 특정 평가 순서가 있으므로 삽입 표현식도 명령의 필수 순서입니다 . 이 표현 은 특정 순서로 수행되는 일련의 작업을 의미 하며, 단계 가 있기 때문에 암시적인 "결과"중간 변수도 있습니다.

  5. 지시 포인터 : 일련의 단계가 있으면 암시적인 "명령 포인터"도 있습니다. 명령 포인터는 다음 명령을 표시하고 명령을 읽은 후 명령이 실행되기 전에 진행합니다.

    이 의사 컴퓨팅 머신에서 명령어 포인터는 Memory의 일부입니다 . (참고 : 일반적으로 명령어 포인터 는 CPU 코어에서 "특수 레지스터"가되지만 여기서는 개념을 단순화하고 모든 데이터 (레지스터 포함)가 "메모리"의 일부라고 가정합니다.

  6. 점프 -순서가 지정된 단계와 명령 포인터 가 있으면 " store "명령을 적용 하여 명령 포인터 자체의 값을 변경할 수 있습니다. 점포 명령어 의 이러한 특정 사용을 새로운 이름 인 Jump라고 합니다. 우리는 새로운 개념으로 생각하기 쉽기 때문에 새로운 이름을 사용합니다. 명령 포인터를 변경하여 에이전트에게 "x 단계로 이동"하도록 지시합니다.

  7. 무한 반복 : 뒤로 건너 뛰어 에이전트가 특정 단계를 "반복"할 수 있습니다. 이 시점에서 무한 반복이 있습니다.

                       1. mov 1000 m[30]
                       2. sub m[30] 1
                       3. jmp-to 2  // infinite loop
    
  8. 조건부 -조건부 명령 실행. "조건부"절을 사용하면 현재 상태 (이전 명령어로 설정할 수 있음)를 기반으로 여러 명령어 중 하나를 조건부로 실행할 수 있습니다.

  9. 적절한 반복 : 이제와 조건 절, 우리는의 무한 루프 탈출 할 수 점프 다시 지시를. 이제 조건부 루프가 있고 적절한 반복이 있습니다.

    1. mov 1000 m[30]
    2. sub m[30] 1
    3. (if not-zero) jump 2  // jump only if the previous 
                            // sub instruction did not result in 0
    
    // this loop will be repeated 1000 times
    // here we have proper ***iteration***, a conditional loop.
    
  10. 이름 지정 : 데이터를 보유하거나 단계를 보유한 특정 메모리 위치에 이름을 지정 합니다 . 이것은 단지 "편의"입니다. 메모리 위치에 "이름"을 정의 할 수있는 용량을 가짐으로써 새로운 지침을 추가하지 않습니다. “네이밍”은 에이전트의 지시가 아니며 단지 우리에게 편의 일뿐입니다. 이름을 지정 하면 코드 (이 시점)를보다 쉽게 ​​읽고 변경할 수 있습니다.

       #define counter m[30]   // name a memory location
       mov 1000 counter
    loop:                      // name a instruction pointer location
        sub counter 1
        (if not-zero) jmp-to loop  
    
  11. 1 단계 서브 루틴 : 자주 실행해야하는 일련의 단계가 있다고 가정합니다. 단계를 메모리의 명명 된 위치에 저장 한 다음 실행해야 할 때 해당 위치 로 이동할 수 있습니다 (호출). 시퀀스가 끝나면 실행을 계속하려면 호출 지점으로 돌아 가야 합니다 . 이 메커니즘을 사용하면 핵심 명령어를 작성 하여 새 명령어 (서브 루틴)를 만듭니다.

    구현 : (새로운 개념이 필요하지 않음)

    • 현재 명령어 포인터를 사전 정의 된 메모리 위치에 저장
    • 점프 서브 루틴에
    • 서브 루틴의 끝에서 사전 정의 된 메모리 위치에서 명령어 포인터를 검색하여 원래 호출 의 다음 명령어로 효과적으로 되돌아갑니다.

    단일 레벨 구현 의 문제점 : 서브 루틴에서 다른 서브 루틴을 호출 할 수 없습니다. 그렇게하면 반환 주소 (전역 변수)를 덮어 쓰므로 호출을 중첩 할 수 없습니다.

    서브 루틴에 대한 더 나은 구현을 위해서는 STACK이 필요합니다

  12. 스택 : "스택"으로 작동 할 메모리 공간을 정의하고 스택에서 값을 "푸시"하고 마지막 "푸시"값을 "팝"할 수 있습니다. 스택을 구현하려면 스택 의 실제 "헤드"를 가리키는 스택 포인터 (명령 포인터와 유사)가 필요합니다 . 값을 "푸시"하면 스택 포인터가 감소하고 값을 저장합니다. "팝"하면 실제 스택 포인터에서 값을 얻은 다음 스택 포인터가 증가합니다.

  13. 서브 루틴 이제 스택생겼으므로 중첩 된 호출을 허용 하는 적절한 서브 루틴을 구현할 수 있습니다 . 구현은 비슷하지만 사전 정의 된 메모리 위치에 명령어 포인터를 저장하는 대신 스택 의 IP 값을 "밀어 넣습니다" . 서브 루틴이 끝나면 스택에서 값을 "팝핑"하여 원래 호출 후 명령으로 효과적으로 되돌아갑니다 . "스택"이있는이 구현은 다른 서브 루틴에서 서브 루틴을 호출 할 수 있습니다. 이 구현 을 통해 핵심 명령어 나 다른 서브 루틴을 빌딩 블록으로 사용하여 새 명령어 를 서브 루틴으로 정의 할 때 여러 수준의 추상화를 만들 수 있습니다 .

  14. 재귀 : 서브 루틴이 자신을 호출하면 어떻게됩니까? 이것을 "재귀"라고합니다.

    문제점 : 로컬 중간 결과를 겹쳐 쓰면 서브 루틴이 메모리에 저장 될 수 있습니다. 당신이 호출되기 때문에 / 같은 단계를 재사용하는 경우 중간 결과가 미리 정의 된 메모리 위치 (전역 변수)에 저장되어 그들이 중첩 된 호출에 덮어 쓰게됩니다.

    솔루션 : 재귀를 허용하려면 서브 루틴은 로컬 중간 결과 를 스택에 저장해야 하므로 각 재귀 호출 (직접 또는 간접)에서 중간 결과는 다른 메모리 위치에 저장됩니다.

...

재귀에 도달하면 여기서 멈 춥니 다.

결론:

Von Neumann Architecture에서 "반복""재귀" 보다 단순하고 기본적인 개념이며 , "재귀" 는 개념 계층의 14 단계에있는 반면, 7 단계 에서는 "반복" 의 형태를 갖습니다 .

명령이 적으므로 CPU 사이클이 적기 때문에 머신 코드에서 반복 이 항상 빠릅니다.

어느 쪽이 더 낫습니까?

  • 단순하고 순차적 인 데이터 구조를 처리 할 때 "간단한 루프"가 수행 할 때마다 "반복"을 사용해야합니다.

  • 재귀 데이터 구조를 처리해야하는 경우 ( "Fractal Data Structures"라고 함) 재귀 솔루션이 더 "우아한"경우에는 "재귀"를 사용해야합니다.

조언 : 작업에 가장 적합한 도구를 사용하되 현명하게 선택하려면 각 도구의 내부 작동을 이해하십시오.

마지막으로 재귀를 사용할 수있는 많은 기회가 있습니다. 당신은 어디에서나 재귀 데이터 구조를 가지고 있고, 지금보고 있습니다 : 읽고있는 것을 지원하는 DOM의 일부는 RDS이고, JSON 표현식은 RDS이며, 컴퓨터의 계층 파일 시스템은 RDS입니다. 파일과 디렉토리를 포함하는 루트 디렉토리, 파일과 디렉토리를 포함하는 모든 디렉토리, 파일과 디렉토리를 포함하는 모든 디렉토리 ...


2
당신은 당신의 진행이 1) 필요하고 2) 당신이 한 곳에서 멈추었다 고 가정합니다. 그러나 1) 필요하지 않습니다 (예 : 수락 된 답변이 설명했듯이 재귀를 점프로 바꿀 수 있으므로 스택이 필요하지 않음) .2) 거기에서 멈출 필요가 없습니다 (예 : 결국 동시 처리에 도달합니다. 두 번째 단계에서 소개 한대로 변경 가능한 상태가 있으면 잠금이 필요할 수 있으므로 모든 것이 느려집니다. 기능적 / 재귀 적 솔루션과 같은 불변 솔루션은 잠금을 피하므로 더 빠르거나 병렬 일 수 있습니다) .
hmijail의 신음은 resignees

2
"재귀는 점프로 바뀔 수있다"는 거짓이다. 정말 유용한 재귀는 점프로 바뀔 수 없습니다. 테일 콜 "재귀"는 컴파일러에서 루프로 단순화 할 수있는 "재귀"를 코딩하는 특수한 경우입니다. 또한 "불변"을 "재귀"와 동일시하고 있습니다. 그것들은 직교 개념입니다.
Lucio M. Tato

"정말 유용한 재귀는 점프로 바뀔 수 없습니다"-> 테일 콜 최적화는 어떻게 쓸모 없습니까? 또한 불변과 재귀는 직교 적 일 수 있지만 반복 가능한 카운터와 루핑을 연결하십시오. 단계 9를보십시오. 루핑과 재귀는 근본적으로 다른 개념이라고 생각하는 것 같습니다. 그들은 아닙니다. stackoverflow.com/questions/2651112/…
hmijail

@hmijail "유용한"보다 더 나은 단어는 "참"이라고 생각합니다. 꼬리 재귀는 함수 호출 구문을 사용하여 무조건 분기, 즉 반복을 위장하기 때문에 진정한 재귀가 아닙니다. 진정한 재귀는 우리에게 역 추적 스택을 제공합니다. 그러나 꼬리 재귀는 여전히 표현 적이므로 유용합니다. 정확성을 위해 코드를 쉽게 또는 쉽게 분석 할 수있는 재귀 속성은 테일 호출을 사용하여 표현 될 때 반복 코드에 부여됩니다. 때로는 추가 매개 변수와 같은 테일 버전의 추가 합병증으로 약간 상쇄됩니다.
카즈

34

대안이 언급 한 정렬 또는 이진 트리 알고리즘과 같이 스택을 명시 적으로 관리하는 경우 재귀 속도가 더 빠를 수 있습니다.

Java에서 재귀 알고리즘을 다시 작성하면 속도가 느려지는 경우가 있습니다.

따라서 올바른 접근 방식은 먼저 가장 자연스러운 방식으로 작성하고 프로파일 링이 중요하다고 판단되는 경우에만 최적화 한 다음 예상되는 개선을 측정하는 것입니다.


2
" 가장 자연스러운 방법으로 쓰기 "에 대해 +1 , 특히 " 프로파일 링이 중요하다는 것을 보여줄 때만 최적화 "
TripeHound

2
+1은 하드웨어 스택이 수동으로 구현 된 힙 내 소프트웨어보다 빠를 수 있음을 인정합니다. 모든 "아니오"답변이 잘못되었음을 효과적으로 보여줍니다.
sh1


12

각 반복, 재귀에 대해 반드시 수행해야 할 작업을 고려하십시오.

  • 반복 : 루프 시작으로 점프
  • 재귀 : 호출 된 함수의 시작으로 점프

여기에 차이점이 많지 않다는 것을 알 수 있습니다.

(재귀는 꼬리 호출이고 컴파일러는 해당 최적화를 알고 있다고 가정합니다).


9

여기서 대부분의 대답은 반복이 반복 솔루션보다 종종 느린 이유에 대한 명백한 범인을 잊어 버립니다. 스택 프레임의 빌드 업 및 해제와 연결되어 있지만 정확히는 아닙니다. 일반적으로 각 재귀에 대한 자동 변수 저장에 큰 차이가 있습니다. 루프가있는 반복 알고리즘에서 변수는 종종 레지스터에 유지되며 변수가 유출 되더라도 레벨 1 캐시에 상주합니다. 재귀 알고리즘에서 변수의 모든 중간 상태는 스택에 저장되므로 메모리에 더 많은 유출이 발생합니다. 즉, 동일한 양의 작업을 수행하더라도 핫 루프에서 많은 메모리 액세스가 발생하고 더 나쁜 점은 이러한 메모리 작업의 재 사용률이 높아 캐시의 효율성이 떨어집니다.

TL; DR 재귀 알고리즘은 일반적으로 반복 알고리즘보다 캐시 동작이 더 나쁩니다.


6

여기에있는 대부분의 답변이 잘못되었습니다 . 정답은 그것이 달려 있다는 것 입니다. 예를 들어, 트리를 통과하는 두 개의 C 함수가 있습니다. 먼저 재귀 적 인 것 :

static
void mm_scan_black(mm_rc *m, ptr p) {
    SET_COL(p, COL_BLACK);
    P_FOR_EACH_CHILD(p, {
        INC_RC(p_child);
        if (GET_COL(p_child) != COL_BLACK) {
            mm_scan_black(m, p_child);
        }
    });
}

그리고 반복을 사용하여 구현 된 동일한 기능은 다음과 같습니다.

static
void mm_scan_black(mm_rc *m, ptr p) {
    stack *st = m->black_stack;
    SET_COL(p, COL_BLACK);
    st_push(st, p);
    while (st->used != 0) {
        p = st_pop(st);
        P_FOR_EACH_CHILD(p, {
            INC_RC(p_child);
            if (GET_COL(p_child) != COL_BLACK) {
                SET_COL(p_child, COL_BLACK);
                st_push(st, p_child);
            }
        });
    }
}

코드의 세부 사항을 이해하는 것은 중요하지 않습니다. 그것은 바로 p노드이며 P_FOR_EACH_CHILD걷는 것입니다. 반복 버전에서는 st노드를 푸시 한 다음 팝 및 조작 할 명시 적 스택이 필요합니다 .

재귀 함수는 반복 함수보다 훨씬 빠르게 실행됩니다. 그 이유는 후자에서 각 항목에 대해 a CALL기능 st_push이 필요하고 다른 항목 이 필요하기 때문 st_pop입니다.

전자에서는 CALL각 노드에 대한 재귀 만 있습니다 .

또한 콜 스택에서 변수에 액세스하는 것이 매우 빠릅니다. 항상 가장 안쪽에있는 메모리에서 읽을 수 있음을 의미합니다. 반면에 명시 적 스택 malloc은 힙에서 : ed 메모리 로 백업 해야하므로 액세스 속도가 훨씬 느립니다.

인라인 st_push및과 같은 신중한 최적화 st_pop를 통해 재귀 적 접근 방식으로 대략적인 패리티에 도달 할 수 있습니다. 그러나 적어도 내 컴퓨터에서는 힙 메모리 액세스 비용이 재귀 호출 비용보다 큽니다.

그러나이 논의는 재귀적인 트리 워킹이 잘못 되었기 때문에 대부분 헛소리 입니다. 트리가 충분히 크면 콜 스택 공간이 부족하므로 반복 알고리즘을 사용해야합니다.


비슷한 상황에 처했고 재귀가 힙의 수동 스택보다 빠를 수있는 상황이 있음을 확인할 수 있습니다. 특히 함수 호출의 오버 헤드를 피하기 위해 컴파일러에서 최적화가 설정된 경우.
while1fork

1
7 노드 이진 트리의 사전 주문 순회를 10 ^ 8 회 수행했습니다. 재귀 25ns. 명시 적 스택 (바운드 검사 여부)-15ns. 재귀는 밀고 점프하는 것 외에도 더 많은 작업 (등록 저장 및 복원 + (보통) 더 엄격한 프레임 정렬)을 수행해야합니다. 동적으로 링크 된 라이브러리에서 PLT를 사용하면 상황이 악화됩니다. 명시 적 스택을 힙 할당 할 필요는 없습니다. 첫 번째 프레임이 일반 호출 스택에있는 obstack을 수행 할 수 있으므로 첫 번째 블록을 초과하지 않는 가장 일반적인 경우 캐시 위치를 희생하지 않습니다.
PSkocik

3

일반적으로, 재귀는 두 가지 형태로 구현 가능한 현실적인 사용법의 루프보다 빠르지 않습니다. 필자는 영원히 걸리는 루프를 코딩 할 수는 있지만 재귀를 통해 동일한 문제의 구현을 능가하는 동일한 루프를 구현하는 더 좋은 방법이있을 것입니다.

그 이유에 관해 머리에 못을 박았다. 스택 프레임을 생성하고 파괴하는 것은 단순한 점프보다 비쌉니다.

그러나 나는 "두 가지 형태로 실행 가능한 구현이있다"고 말했다. 많은 정렬 알고리즘과 같은 것들에는 본질적으로 프로세스의 일부인 자식 "태스크"가 생성되기 때문에 자체 스택 버전을 효과적으로 설정하지 않는 알고리즘을 구현하는 매우 실용적인 방법이 없습니다. 따라서 재귀는 루핑을 통해 알고리즘을 구현하는 것만 큼 빠를 수 있습니다.

편집 :이 답변은 대부분의 기본 데이터 유형을 변경할 수있는 비 기능적 언어를 가정합니다. 기능적 언어에는 적용되지 않습니다.


그렇기 때문에 재귀가 자주 사용되는 언어로 컴파일러에서 여러 재귀 사례를 최적화하는 경우가 종종 있습니다. 예를 들어 F #에서는 .tail opcode를 사용하여 tail 재귀 함수를 완벽하게 지원할뿐만 아니라 재귀 함수가 루프로 컴파일되는 경우가 종종 있습니다.
em70

네. 테일 재귀는 때로는 재귀적인 작업을 구현하는 기능적으로 "적절한"방법과 루프 사용 성능 모두에서 최고 일 수 있습니다.
Amber

1
이것은 일반적으로 정확하지 않습니다. 일부 환경에서는 돌연변이 (GC와 상호 작용)가 꼬리 재귀보다 비싸므로 추가 스택 프레임을 사용하지 않는 출력에서 ​​더 간단한 루프로 변환됩니다.
Dietrich Epp

2

실제 시스템에서 아니오, 스택 프레임을 만드는 것은 항상 INC 및 JMP보다 비쌉니다. 이것이 바로 훌륭한 컴파일러가 꼬리 재귀를 동일한 프레임에 대한 호출로 자동으로 변환하는 이유입니다. 즉, 오버 헤드없이 더 읽기 쉬운 소스 버전과보다 효율적인 컴파일 버전을 얻을 수 있습니다. 정말, 정말 좋은 컴파일러는 심지어이 가능한 경우 꼬리 재귀로 정상적인 재귀를 변환 할 수 있어야한다.


1

함수형 프로그래밍은 " how "가 아닌 " what " 에 관한 것 입니다.

언어 구현자는 필요한 것보다 더 최적화하려고하지 않으면 코드가 작동하는 방식을 최적화 할 수있는 방법을 찾을 것입니다. 테일 콜 최적화를 지원하는 언어 내에서 재귀를 최적화 할 수도 있습니다.

프로그래머 관점에서 더 중요한 것은 처음부터 최적화보다는 가독성과 유지 관리 성입니다. 다시, "조기 최적화는 모든 악의 근원"입니다.


0

이것은 추측이다. 일반적으로 재귀는 종종 좋은 알고리즘 (구현 어려움을 세지 않음)을 사용하는 경우 자주 또는 종종 적절한 크기의 문제에서 루핑을 이길 수 없습니다. 꼬리 호출 재귀 가있는 언어 (꼬리 재귀 알고리즘) 와 함께 사용하면 다를 수 있습니다 그리고 언어의 일부로 루프를 사용)-아마도 매우 유사하고 때로는 재귀를 선호합니다.


0

이론에 따르면 같은 것입니다. 동일한 O () 복잡도를 가진 재귀와 루프는 동일한 이론적 속도로 작동하지만 실제 속도는 언어, 컴파일러 및 프로세서에 따라 다릅니다. 숫자의 거듭 제곱을 가진 예는 O (ln (n))을 사용하여 반복 방식으로 코딩 할 수 있습니다.

  int power(int t, int k) {
  int res = 1;
  while (k) {
    if (k & 1) res *= t;
    t *= t;
    k >>= 1;
  }
  return res;
  }

1
Big O는 "비례"입니다. 따라서 둘 다 O(n)이지만 하나 x 다른 것보다 시간이 오래 걸릴 있습니다 n.
ctrl-alt-delor
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.