-
- 1. 개요
-
- 2. 재귀호출하는 익명 서브루틴을 만들 때의 문제점
-
- 3. 메모리 누수 문제 해결을 위한 코드
-
- 4. CPAN 모듈 사용
-
- 5. 참고 문서
-
- 6. Comments
-
Perl에서 재귀적으로 호출되는 익명 서브루틴을 만들려고 하니 문제가 생겨서, 이것저것 뒤져본 내용.
2. 재귀호출하는 익명 서브루틴을 만들 때의 문제점
재귀 호출을 써서 팩토리알을 계산하는 서브루틴은 다음과 같이 만들 수 있다:
sub fac {
my $n = shift;
return 1 if $n == 1;
return $n * fac( $n-1 );
}
print "10! = ", fac(10), "\n";
그런데, 익명 서브루틴으로 다음과 같이 만들 수는 없다:
my $fac = sub {
my $n = shift;
return 1 if $n == 1;
return $n * $fac->( $n-1 );
};
print "10! = ", $fac->(10), "\n";
- 변수
$fac
은 아직 정의되지 않은 상태이기 때문에 컴파일 시점에 에러가 난다.
변수 선언을 서브루틴 정의보다 먼저 해 주어서 해결할 수는 있다:
my $fac;
$fac = sub {
my $n = shift;
return 1 if $n == 1;
return $n * $fac->( $n-1 );
};
print "10! = ", $fac->(10), "\n";
그런데, 이렇게 하면 $fac 은 익명 서브루틴을 참조하고 있고, 이 서브루틴은 $fac 변수를 참조한다. 따라서 순환 참조가 생기고, 스코프를 벗어난 후에도 메모리에서 제거가 되지 않는다:
foreach ( 0 .. 100000 ) {
my $fac;
$fac = sub {
my $n = shift;
return 1 if $n == 1;
return $n * $fac->( $n-1 );
};
$fac->(10);
}
print "hit any key:";
<STDIN>;
- 루프 안에서 익명 서브루틴을 생성하고 호출하는 과정을 반복한다. 매 반복 때마다 $fac과 저 익명 서브루틴은 소멸되어야 하지만, 실제로는 소멸되지 않고 계속 쌓인다.
- 루프를 십만 번 돌고 나면, 종료 전에 메모리 워킹셋 사이즈가 300MB를 차지한다:
3. 메모리 누수 문제 해결을 위한 코드
이를 해결하기 위해서, $fac을 weak reference로 만들어 주어야 한다:
use Scalar::Util qw/weaken/;
foreach ( 0 .. 100000 ) {
my $fac;
$fac = sub {
my $n = shift;
return 1 if $n == 1;
return $n * $fac->( $n-1 );
};
$fac->(10);
weaken($fac);
}
그런데, 인터넷에서 검색하다 찾은 글[1]에서는 weaken()
은 서브루틴을 호출하기 전에 수행해야 한다고 한다. (호출하기 전과 후에 할 때 어떤 차이가 있는지 주인장은 잘 모르겠다.) 문제는, weaken()을 수행하고 나면 그 시점에서 익명 서브루틴의 레퍼런스 카운트가 0이 되어 버려서 없어져 버린다는 거다. 따라서, 다음과 같은 트릭이 필요하다 (이 트릭 역시 [1]에 언급됨)
use Scalar::Util qw/weaken/;
foreach ( 0 .. 100000 ) {
my ($fac, $f);
$f = $fac = sub {
my $n = shift;
return 1 if $n == 1;
return $n * $fac->( $n-1 );
};
weaken($fac);
$fac->(10);
}
-
$f
변수를 추가로 사용하여 레퍼런스 카운트를 1늘렸다.
- 메모리 사용량은 앞의 경우와 동일하다. (KB단위라서, 정확히 동일하지 않을 수도 있다)
4. CPAN 모듈 사용
이 외에, 이 문제를 해결하기 위한 Sub::Recursive 또는 Sub::Current 등의 모듈도 있다. 모듈을 사용할 경우의 장점은 앞에서와 같은 트릭을 쓰지 않아도 된다는 것과, 앞에서는 어쨌거나 어떤 변수에 서브루틴 레퍼런스를 담아야 하지만 아래의 모듈을 사용하면 foo( recursive { ... } );
와 같이 익명 서브루틴을 생성과 동시에 직접 서브루틴의 인자로 넣을 수 있다는 점 등이다.
Sub::Recursive 모듈을 사용한 예:
use Sub::Recursive;
foreach ( 0 .. 100000 ) {
my $fac = recursive {
my $n = shift;
return 1 if $n == 1;
return $n * $REC->( $n-1 );
};
$fac->(10);
}
-
sub
키워드 대신에 recursive
를 사용
- 현재 서브루틴의 레퍼런스를 담는
$REC
변수가 익스포트된다.
Sub::Current 모듈을 사용한 예:
use Sub::Current;
foreach ( 0 .. 100000 ) {
my $fac = sub {
my $n = shift;
return 1 if $n == 1;
return $n * ROUTINE->( $n-1 );
};
$fac->(10);
}
- 현재 수행중인 서브루틴의 레퍼런스를 반환하는
ROUTINE->()
을 제공한다.
[1]에서는 Sub::Recursive는 local
을 사용하는데 이게 비용이 많이 드는 일이라서 Sub::Current가 더 낫다고 판단하고 있다.
위의 팩토리알 코드를 가지고 벤치마크를 해 보았는데 (테스트가 제대로 이뤄졌다는 확신은 없으니 참고만 하자),
- 일단 익명 서브루틴을 만든 후에, 호출만 십만번 반복하게 했을 때는 Sub::Recursive 가 더 빨랐다. 그리고 5!, 10!, 15!, 20!, 25!을 계산할 때 속도 차이가 더 벌어졌다.
- 익명 서브루틴을 생성하고 한 번 호출하는 과정 전체를 루프 안에 넣고 십만번 반복했을 때는, 5!,10!,15! 계산 때는 Sub::Current 가 더 빨랐고, 20!,25! 때는 Sub::Recursive가 점점 더 빨라졌다.
- 요컨데 이 결과만 놓고 보면, 익명 서브루틴을 생성하는 과정에서는 Sub::Current가 더 빠른데, 생성한 서브루틴을 호출할 때, 특히 재귀 호출의 단계가 깊어질수록 Sub::Recursive가 더 빨라진다.
5. 참고 문서
6. Comments
컴퓨터분류