Programing

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

lottogame 2020. 3. 29. 08:41
반응형

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


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

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

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


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

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

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

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

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

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

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


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

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

설명:

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

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

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

우리는 처음부터 시작하여 기본 핵심 개념을 처음부터 정의한 다음 개념과 함께 2 단계 개념을 구축하는 등 개념의 계층 구조를 설정합니다.

  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)  
    

    위의 표현은 암시 적 "result"변수가있는 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에서 명확하게 "반복""재귀" 보다 단순하고 기본적인 개념이며 , 7 단계에서 "반복" 의 형태를 갖는 반면 "재귀" 는 개념 계층의 14 단계에 있습니다.

명령이 적으므로 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를 통해 재귀 적 접근 방식으로 대략적인 패리티에 도달 할 수 있습니다. 그러나 적어도 내 컴퓨터에서는 힙 메모리 액세스 비용이 재귀 호출 비용보다 큽니다.

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


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


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

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

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

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


함수형 프로그래밍은 " how " 보다는 " what " 에 관한 입니다.

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

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


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


이론에 따르면 같은 것입니다. 동일한 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;
  }

참고 URL : https://stackoverflow.com/questions/2651112/is-recursion-ever-faster-than-looping

반응형