[performance] 반복보다 반복이 더 빠릅니까?

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

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

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



답변

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

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

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

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

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

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

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


답변

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

아니요, 반복은 항상 재귀보다 빠릅니다. (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입니다. 파일과 디렉토리를 포함하는 루트 디렉토리, 파일과 디렉토리를 포함하는 모든 디렉토리, 파일과 디렉토리를 포함하는 모든 디렉토리 …


답변

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

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

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


답변

꼬리 재귀 는 반복만큼 빠릅니다. 많은 기능적 언어에는 꼬리 재귀가 구현되어 있습니다.


답변

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

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

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

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


답변

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

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


답변

여기에있는 대부분의 답변이 잘못되었습니다 . 정답은 그것이 달려 있다는 것 입니다. 예를 들어, 트리를 통과하는 두 개의 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를 통해 재귀 적 접근 방식으로 대략적인 패리티에 도달 할 수 있습니다. 그러나 적어도 내 컴퓨터에서는 힙 메모리 액세스 비용이 재귀 호출 비용보다 큽니다.

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