[첫화면으로]Perl/FileCache

마지막으로 [b]

5 번째 수정본

FileCache 코어 모듈

1. 테스트 기록
1.1. 테스트 상황
1.2. 메모리에 다 불러와서 정렬
1.3. 키 별로 개별 파일에 저장 후 병합
1.4. open 횟수를 줄이려는 시도
1.5. FileCache 모듈 사용
2. Comments

Perldoc:FileCache

프로그램이 실행될 때 동시에 열 수 있는 파일의 갯수에 제한이 있는데, FileCache 모듈을 쓰면 자기가 알아서 오랫동안 사용되지 않은 핸들을 닫고, 필요할 때 다시 열 수 있게 해 준다.

1. 테스트 기록

[펄스터디] 까페에 한군님이 대용량 파일의 내용 정렬 질문글을 올리셨던 것과 비슷한 상황을 만들어서 해 봤음.

1.1. 테스트 상황

다음과 같은 파일이 있다:
키      일련번호
0       1
0       2
0       3
1       1
2       1
2       2
...
첫번째 컬럼에 있는 숫자를 키라고 하고, 각 키마다 다시 일련번호가 붙는다. 각 키가 파일 안에 나타나는 횟수가 다 다르다. 이것을 키가 가장 많이 나타나는 순으로 정렬하고 싶다.

정렬 후:
0       1
0       2
0       3
2       1
2       2
1       1

일단 이런 형태의 데이타를 생성하기 위해서 Cpan:Math::Random 모듈의 random_exponential 함수를 사용했다. 결과적으로 다음과 같이 생성되었다. 원래 한군님이 정렬해야 될 데이타는 키의 가짓수는 6천여개였고 라인 수가 더 길었는데 (7백만 정도?), 시간이 너무 걸려서 좀 줄였다.

그리고 원본 파일의 구성을 두 가지로 준비했다

실행한 환경은

테스트에걸리는 시간은 코드 내에서 직접 측정해서 출력하게 했고, 메모리 사용량은 옆에서 top을 띄워둔 후 virtual image 크기를 대충 눈으로 봤음.

1.2. 메모리에 다 불러와서 정렬

일단 파일 전체를 메모리에 읽어들인 후 정렬하여 파일로 기록하는 코드:
    # 통채로 읽고
    my @array = <$in>;

    # 각 키의 빈도수를 먼저 카운트한 후
    my %freq = ();
    foreach my $line ( @array ) {
        my $key = (split /\s+/, $line)[0];
        $freq{$key}++;
    }

    # 정렬하고
    my @sorted =
        map { $_->[1] }
        sort { $freq{$b->[0]} <=> $freq{$a->[0]} or $a->[0] <=> $b->[0] }
        map { [ (split /\s+/, $_)[0], $_ ] }
        @array;

    # 파일에 저장

실행결과는 다음과 같음
[gypark@www perlstudy_1473]$ ./sort.pl 11
method 1 - in-memory sort. [input_seq.txt]
 laptime time after loading file : 1.730617
 laptime time after counting : 4.001903
 laptime time after sorting : 13.373383
method 1 - save [sorted_1_seq.txt]
 total elapsed time : 14.21391

사용메모리: 약 745M
[gypark@www perlstudy_1473]$ ./sort.pl 12
method 1 - in-memory sort. [input_rnd.txt]
 laptime time after loading file : 1.725655
 laptime time after counting : 4.212954
 laptime time after sorting : 40.480208
method 1 - save [sorted_1_rnd.txt]
 total elapsed time : 41.319748

사용메모리: 약 820M

파일을 읽어 배열에 넣는데 1.7초, 그 배열원소 3백만개를 순회하며 키를 뽑아내어 빈도수를 재는 데 2.3초 정도 걸리는 건 비슷하나, 그 다음 정렬할 때 차이가 많이 난다. 순차 파일의 경우 같은 키에 해당하는 원소들이 이미 정렬된 채로 모여 있어서 그런 듯.

(여담으로, 위 테스트를 처음에는 윈도우7이 깔린 컴퓨터에서 하려고 했는데, 파일을 읽는 데에만 십수분이 걸려도 끝나지 않았다 -_-;; /StringConcatenationBenchmark에서도 얘기가 나왔었지만 윈도우에서 메모리를 realloc 으로 계속 늘려나갈 때 문제가 있어서 그런 게 아닐까 짐작함)

문제는 메모리 사용량이다. 25MB 짜리 텍스트 파일을 읽어 배열에 저장할 때 400MB 이상을 쓰게되고, 정렬을 위해서 배열이 하나 복사되면서 두 배가 된다. 라인 수를 7백만개로 늘렸을 경우 내 테스트 환경에서는 순차 파일은 2GB를 간당간당하게 쓰면서 40초 정도 끝났는데, 랜덤 파일은 디스크에 스왑을 하게되는 듯 끝날 기미가 안 보여서 포기했음.

1.3. 키 별로 개별 파일에 저장 후 병합

메모리가 부족해서 통채로 읽을 수 없으니, 할 수 없이 디스크를 쓰기로 한다.

기본 컨셉은, 하나의 키에 해당하는 라인들만 뽑아내어 파일 하나에 저장하는 식으로 분리를 한 후, 빈도수가 높은 (즉 라인수가 많은) 키의 파일들부터 읽어서 하나의 파일에 기록하는 식.

    my %freq = ();
    open my $in, '<', $input_file;
    while ( my $line = <$in> ) {
        my $key = (split /\s+/, $line)[0];
        $freq{$key}++;

# 한 라인을 읽을 때마다 매번 그 라인에 해당하는 키 파일을 열고 거기에 덧붙인 후 닫음
        open my $tmp, '>>', $key;
        print {$tmp} $line;
        close $tmp;
    }
    close $in;

    open my $out, ">", $output_file;
    foreach my $key ( sort { $freq{$b} <=> $freq{$a} or $a <=> $b } keys %freq ) {
# 정렬한 키 순서대로 키 파일을 읽어서 최종 결과물에 덧붙임
        open my $tmp, '<', $key;
        while ( my $line = <$tmp> ) {
            print {$out} $line;
        }
        close $tmp;
        unlink $key;
    }
    close $out;

실행 결과:
[gypark@www perlstudy_1473]$ ./sort.pl 21
method 2 - open-close in each iteration. [input_seq.txt]
 laptime time after loading, counting, splitting : 132.328214
method 2 - save [sorted_2_seq.txt]
 total elapsed time : 134.268979

사용메모리: 약 10M

[gypark@www perlstudy_1473]$ ./sort.pl 22
method 2 - open-close in each iteration. [input_rnd.txt]
 laptime time after loading, counting, splitting : 1081.486577
method 2 - save [sorted_2_rnd.txt]
 total elapsed time : 1089.66655

사용메모리: 약 10M

메모리는, 쓸래야 쓸 일이 없다. 항상 한 라인씩 처리하기 때문에 10MB를 넘지 않는다.

순차 파일과 랜덤 파일 둘 다 정렬하고 최종 파일에 기록하는 건 몇 초 걸리지 않는데, 입력을 읽으며 키 파일들을 만드는 데 시간의 차이가 크게 난다. 어차피 3백만 번 오픈,클로즈를 꼬박꼬박 하는 건 마찬가지인데 이렇게 차이가 나는 건, 순차 파일의 경우는 한번 열었던 파일을 다시 열게 되니까 이 때는 버퍼 캐시 등의 도움으로 오버헤드가 줄어들기 때문으로 생각됨.

1.4. open 횟수를 줄이려는 시도

3백만 라인을 읽는 동안 매번 파일을 열고,닫는 건 너무 오버헤드가 크다. 이걸 줄여볼 요량으로, 어떤 키가 처음 나타날 때 그 키에 해당하는 파일을 열고, 그 다음부터는 계속 연 채로 두기로 한다.

    my %freq = ();

# 각 키에 해당하는 파일 핸들들의 해시
    my %fh = ();

    open my $in, '<', $input_file;
    while ( my $line = <$in> ) {
        my $key = (split /\s+/, $line)[0];
        $freq{$key}++;

# 어떤 키가 처음 나오면 그 때 열고
        if ( not exists $fh{$key} ) {
            open $fh{$key}, '>', $key;
        }
# 열린 파일 핸들을 계속 사용
        print {$fh{$key}} $line;
    }
    close $in;

# 나머지는 앞 섹션과 동일

실행 결과:

[gypark@www perlstudy_1473]$ ./sort.pl 31
method 3 - open-close once. [input_seq.txt]
Failed to load 'autodie::exception'.
This may be a typo in the 'autodie->exception_class' method,
or the 'autodie::exception' module may not exist.

 Can't locate autodie/exception.pm:   Too many open files at (eval 105) line 2, <$_[...]> line 1917511.
 at /home/gypark/perl5/perlbrew/perls/perl-5.14.2/lib/5.14.2/Fatal.pm line 1250, <$_[...]> line 1917511.
        Fatal::throw('autodie', 'function', 'CORE::open', 'args', 'ARRAY(0xa0997f8)', 'pragma', 'autodie', 

'errno', 'Too many open files', ...) called at (eval 19) line 114
        main::__ANON__('GLOB(0xa0997b8)', '>', 1020) called at ./sort.pl line 108

안타깝게도... 시스템에서 한 프로세스가 동시에 열 수 있는 파일의 갯수에는 제한이 있다. 이 시스템에서는 1024로 설정되어 있고, 수정하려면 /etc/security/limits.conf에서 해 줄 수 있긴 하다. 윈도우에서는 2048개 정도인 듯 한데 수정하는 방법을 알아내지 못 했다. (구글링해보면 말이 다 다름)

한군님이 데이타 사이즈를 줄여서 테스트한 게 있는데, 거기에서는 매번 오픈-클로즈 할 때에 비해 속도가 절반으로 줄어들었다고 한다.

1.5. FileCache 모듈 사용

2. Comments

이름:  
Homepage:
내용:
 

기타분류

이 수정본 편집일: 2012-10-30 12:03 pm (변경사항 [d])
2247 hits | Permalink | 변경내역 보기 [h] | 현재 수정본 보기 | 5 번째 수정본 소스 보기