intsum(int n){ return n <= 1 ? n : n + sum(n-1); }
sum(100); 을 실행하면 결과 값 5050이 잘 리턴된다.
하지만 큰 숫자를 넣고 실행하면 어떻게 될까?
sum(100000); 을 실행하면 우리에게 익숙한 StactOverflowError가 발생한다. (컴퓨터마다 한계숫자는 차이가 있음) 십만은 컴퓨터에게 그리 큰 숫자가 아닌데 왜 메모리가 모자른걸까?
개발자라면 모두 아는 사이트
Recursion And Stack in Java
자바는 메소드가 호출될 때 현재 하고 있는 일을 중단하고(suspend) 현재 컨텍스트의 환경을 스택 메모리에 저장(push)한다.
그리고 메소드가 리턴되었을 때 다시 스택에서 가져와(pop) 실행을 재개한다.(resume)
위에서 큰 수를 넣었을 때 재귀함수가 반복적으로 호출되면서 스택에 계속 push만 되다보니 메모리 허용치를 넘어서 에러가 나는 것이였다. 스택메모리는 빠르지만 공간은 작다.
현재 환경을 스택에 저장하는 이유는 메소드 호출 후에 돌아올 지점을 기억하고, 돌아와서 나머지 작업을 재개해야하기 때문이다. 그렇다면 반환 후 나머지 작업을 할 필요가 없을 경우엔 스택에 저장 할 필요가 없다는 것이 아닐까??
이처럼 재귀호출 부분을 맨 마지막 꼬리에 위치시키는 방법을 Tail Call Recursion(꼬리물기 재귀) 라고하며. Tail Call Recursion을 했을 때 스택저장을 피하는 것을 TCE(Tail Call Elimination) 또는 TCO(Tail Call Optimization)라고 한다. 나머지 작업이 없기 때문에 스택에 저장할 필요가 없어진다.
Tail Call Elimination (꼬리물기 최적화)
정말 그런지 확인해보자 위의 sum 함수를 Tail Call Recursion으로 만들면 다음과 같다.
1 2 3
intsum(int n, int acc){ return n <= 1 ? acc : sum(n-1, n+acc); }
처음에 나온 sum 함수는 내부에서 sum(n-1)을 호출한 후 + 연산을 재개해야 하기 때문에 Tail Call이 아니다.
하지만 변경된 위의 예제는 마지막에 호출하고 뒤에 아무일도 하지 않기 때문에 tail call 이라고 할 수 있다.
변경된 함수로 sum(100000); 을 실행해보자.. 역시나 에러가 나는 것을 볼 수 있다.
불행히도 자바에서는 TCE가 구현되어 있지 않다. 함수형 언어같은 경우 위처럼 했을 때 최적화가되어 스택메모리에 저장이 안되지만 자바는 여전히 저장된다.
Tail Call Elimination in Java
자바는 TCE가 지원되지 않지만 비슷하게 구현할 수는 있다. 먼저 TailCall 이라는 추상클래스를 만들고 3개의 추상메소드를 만든다.
1 2 3 4 5
publicabstractclassTailCall<T> { publicabstract TailCall<T> resume(); publicabstract T eval(); publicabstractbooleanisSuspend(); }