Archive::Zip
Zip 압축파일을 만들거나 풀 때 사용하는 모듈.
1. 반복적인 read() 호출과 extract() 관련 문제점
1.1. 실험1
1.2. 실험2
1.3. 실험3
1.4. 실험4 - 매번 객체 만들기
1.5. 중간 결론
2. 기타 & Comments
1. 반복적인 read() 호출과 extract() 관련 문제점
[스무번째 날: Gearman 사용 사례 -- 펄 크리스마스 달력 #2013] 기사를 작성하는 도중에 발견.
하나의 Archive::Zip 오브젝트에 대하여 read
메쏘드를 호출하여 압축 파일을 읽을 때마다 점점 더 메모리를 차지하게 되고
이것이 영향을 미치는 걸로 보이는데, extractTree
또는 extractMemberWithoutMethods
메쏘드를 호출하여 내용을 추출할 때 점점 더 많은 시간을 소모하게 된다.
따라서 수천 회 이상 압축 파일을 다루게 될 경우 속도가 눈에 띄게 저하될 수 있으므로 Archive::Zip 오브젝트를 재사용하지 말고 새로 생성하여 사용할 것을 권장함
일단 한 번만 read()하고, 루프를 돌며 2만번 extract()하는 테스트.
my $zip = Archive::Zip->new ();
$zip ->read ($file ) == AZ_OK;
for my $num ( 1 .. 20000 ) {
$zip ->extractMemberWithoutPaths($member );
}
전체 코드는 길어서 접음
!/ usr / bin/env perl
use strict ;
use warnings ;
use Archive::Zip qw/ :ERROR_CODES :CONSTANTS / ;
use Time::HiRes qw/ gettimeofday tv_interval / ;
use Memory::Usage;
my $member = ' LIST.txt ' ;
my $file = ' /home/gypark/temp/gearman/data/00001.zip ' ;
my $mu = Memory::Usage->new ;
my $zip = Archive::Zip->new ();
my $time_read = 0 ;
my $time_extract = 0 ;
my $t0 ;
my $elapsed ;
die ' read erorr ' unless ( $zip ->read ($file ) == AZ_OK );
$mu ->record(' before loop ' );
for my $num ( 1 .. 20000 ) {
$t0 = [ gettimeofday ];
$zip ->extractMemberWithoutPaths($member );
$elapsed = tv_interval($t0 );
$time_extract += $elapsed ;
if ( $num % 1000 == 0 ) {
printf (" %d , %.6f, %.6f \n " , $num , $time_read /1000 , $time_extract /1000 );
$time_read = 0 ;
$time_extract = 0 ;
}
unlink $member or die " unlink: $! " ;
}
$mu ->record(' after loop ' );
$mu ->dump ();
결과:
1000, 0.000000, 0.000742 <-- 매 천 번 반복하는 동안 read()와 extract..()의 평균 실행 시간
2000, 0.000000, 0.000750
3000, 0.000000, 0.000746
4000, 0.000000, 0.000768
5000, 0.000000, 0.000772
6000, 0.000000, 0.000771
7000, 0.000000, 0.000756
8000, 0.000000, 0.000649
9000, 0.000000, 0.000626
10000, 0.000000, 0.000754
11000, 0.000000, 0.000764
12000, 0.000000, 0.000764
13000, 0.000000, 0.000583
14000, 0.000000, 0.000658
15000, 0.000000, 0.000495
16000, 0.000000, 0.000514
17000, 0.000000, 0.000608
18000, 0.000000, 0.000589
19000, 0.000000, 0.000708
20000, 0.000000, 0.000760
time vsz ( diff) rss ( diff) shared ( diff) code ( diff) data ( diff)
0 94868 ( 94868) 8920 ( 8920) 2068 ( 2068) 1412 ( 1412) 7136 ( 7136) before loop
14 94868 ( 0) 8968 ( 48) 2096 ( 28) 1412 ( 0) 7136 ( 0) after loop
read()는 처음 한 번만 하고 루프 내에서는 실행하지 않았으니 시간이 0.0이고
extractM emberWithoutPaths()의 평균 수행 시간은 루프를 천 번 반복하는 동안 합산한 후 1000으로 나눈 값이다. 즉 처음 천 번 루프를 도는 동안 이 함수를 한번 호출할 때 수행 시간은 평균 0.742밀리초였고, 그 다음 천 번 루프를 도는 동안은 평균 0.750밀리초...이런 식. 위 결과를 보면 처음 천 번 돌 때나 마지막 스무 번째 천 번 돌 때나 평균시간은 항상 0.8밀리초 이내를 유지 한다.
마지막 두 줄은 루프를 돌기 전과 후의 사용 메모리 변화인데, data size 에 전혀 변화가 없다 .
이번에는 2만번 루프를 돌면서 매번 read()하고 extract()하는 테스트.
my $zip = Archive::Zip->new ();
for my $num ( 1 .. 20000 ) {
$zip ->read ($file );
$zip ->extractMemberWithoutPaths($member );
}
전체 코드 - 길어서 접음
#!/usr/bin/env perl
use strict ;
use warnings ;
use Archive::Zip qw/ :ERROR_CODES :CONSTANTS / ;
use Time::HiRes qw/ gettimeofday tv_interval / ;
use Memory::Usage;
my $member = ' LIST.txt ' ;
my $file = ' /home/gypark/temp/gearman/data/00001.zip ' ;
my $mu = Memory::Usage->new ;
my $zip = Archive::Zip->new ();
my $time_read = 0 ;
my $time_extract = 0 ;
my $t0 ;
my $elapsed ;
$mu ->record(' before loop ' );
for my $num ( 1 .. 20000 ) {
$t0 = [ gettimeofday ];
$zip ->read ($file );
$elapsed = tv_interval($t0 );
$time_read += $elapsed ;
$t0 = [ gettimeofday ];
$zip ->extractMemberWithoutPaths($member );
$elapsed = tv_interval($t0 );
$time_extract += $elapsed ;
if ( $num % 1000 == 0 ) {
printf (" %d , %.6f, %.6f \n " , $num , $time_read /1000 , $time_extract /1000 );
$time_read = 0 ;
$time_extract = 0 ;
}
unlink $member or die " unlink: $! " ;
}
$mu ->record(' after loop ' );
$mu ->dump ();
결과:
1000, 0.000462, 0.000902
2000, 0.000469, 0.001179
3000, 0.000461, 0.001391
4000, 0.000450, 0.001615
5000, 0.000409, 0.001711
6000, 0.000355, 0.001777
7000, 0.000351, 0.001891
8000, 0.000394, 0.002372
9000, 0.000403, 0.002621
10000, 0.000384, 0.002761
11000, 0.000334, 0.002657
12000, 0.000383, 0.003267
13000, 0.000389, 0.003607
14000, 0.000453, 0.004447
15000, 0.000339, 0.003767
16000, 0.000423, 0.004907
17000, 0.000402, 0.005094
18000, 0.000412, 0.005584
19000, 0.000437, 0.006368
20000, 0.000446, 0.006684
time vsz ( diff) rss ( diff) shared ( diff) code ( diff) data ( diff)
0 94872 ( 94872) 8884 ( 8884) 2036 ( 2036) 1412 ( 1412) 7140 ( 7140) before loop
74 173532 ( 78660) 87472 ( 78588) 2096 ( 60) 1412 ( 0) 85800 ( 78660) after loop
루프를 2만번 도는 동안 read()의 실행 시간은 항상 0.4밀리초 안팎으로 일정 하다.
그러나 extract 함수의 실행 시간은 점점 길어져서, 마지막에는 처음과 비교해서 거의 7~8배로 늘어난다 . 그래서 스크립트 전체가 도는 시간도 실험1은 14초 정도였으나 실험2는 73초 정도 걸렸다.
메모리 역시, 루프를 돌고 나면 78.7MB 정도가 늘어나 있다 . 루프 한 번 돌때마다 4KB 정도 늘어난 셈
(실험1과 실험2에서 extract 함수의 실행시간 비교)
이건 확인 차원에서, 반대로 read 만 2만번 반복하게 한 것.
my $zip = Archive::Zip->new ();
for my $num ( 1 .. 20000 ) {
$zip ->read ($file );
}
전체 코드 - 길어서 감춤
#!/usr/bin/env perl
use strict ;
use warnings ;
use Archive::Zip qw/ :ERROR_CODES :CONSTANTS / ;
use Time::HiRes qw/ gettimeofday tv_interval / ;
use Memory::Usage;
my $member = ' LIST.txt ' ;
my $file = ' /home/gypark/temp/gearman/data/00001.zip ' ;
my $mu = Memory::Usage->new ;
my $zip = Archive::Zip->new ();
my $time_read = 0 ;
my $time_extract = 0 ;
my $t0 ;
my $elapsed ;
$mu ->record(' before loop ' );
for my $num ( 1 .. 20000 ) {
$t0 = [ gettimeofday ];
$zip ->read ($file );
$elapsed = tv_interval($t0 );
$time_read += $elapsed ;
if ( $num % 1000 == 0 ) {
printf (" %d , %.6f, %.6f \n " , $num , $time_read /1000 , $time_extract /1000 );
$time_read = 0 ;
$time_extract = 0 ;
}
}
$mu ->record(' after loop ' );
$mu ->dump ();
결과:
1000, 0.000449, 0.000000
2000, 0.000444, 0.000000
3000, 0.000442, 0.000000
4000, 0.000444, 0.000000
5000, 0.000442, 0.000000
6000, 0.000446, 0.000000
7000, 0.000443, 0.000000
8000, 0.000449, 0.000000
9000, 0.000446, 0.000000
10000, 0.000422, 0.000000
11000, 0.000355, 0.000000
12000, 0.000286, 0.000000
13000, 0.000285, 0.000000
14000, 0.000287, 0.000000
15000, 0.000286, 0.000000
16000, 0.000296, 0.000000
17000, 0.000303, 0.000000
18000, 0.000308, 0.000000
19000, 0.000327, 0.000000
20000, 0.000445, 0.000000
time vsz ( diff) rss ( diff) shared ( diff) code ( diff) data ( diff)
0 94868 ( 94868) 8880 ( 8880) 2036 ( 2036) 1412 ( 1412) 7136 ( 7136) before loop
8 171756 ( 76888) 85876 ( 76996) 2076 ( 40) 1412 ( 0) 84024 ( 76888) after loop
read() 시간은 역시 항상 비슷비슷하다.
extract 함수는 호출하지 않았으니 볼 것 없고
메모리 사용량이 실험2와 동일하게 늘었다 .
1.4. 실험4 - 매번 객체 만들기
생각해보니까, 정작 이 테스트를 빼먹었다. 루프를 2만 번 돌면서 매번 Archive::Zip 객체를 새로 생성해서 사용하자.
for my $num ( 1 .. 20000 ) {
my $zip = Archive::Zip->new ();
$zip ->read ($file );
$zip ->extractMemberWithoutPaths($member );
}
전체 코드 - 길어서 감춤
#!/usr/bin/env perl
use strict ;
use warnings ;
use Archive::Zip qw/ :ERROR_CODES :CONSTANTS / ;
use Time::HiRes qw/ gettimeofday tv_interval / ;
use Memory::Usage;
my $member = ' LIST.txt ' ;
my $file = ' /home/gypark/temp/gearman/data/00001.zip ' ;
my $mu = Memory::Usage->new ;
my $time_read = 0 ;
my $time_extract = 0 ;
my $t0 ;
my $elapsed ;
$mu ->record(' before loop ' );
for my $num ( 1 .. 20000 ) {
my $zip = Archive::Zip->new ();
$t0 = [ gettimeofday ];
$zip ->read ($file );
$elapsed = tv_interval($t0 );
$time_read += $elapsed ;
$t0 = [ gettimeofday ];
$zip ->extractMemberWithoutPaths($member );
$elapsed = tv_interval($t0 );
$time_extract += $elapsed ;
if ( $num % 1000 == 0 ) {
printf (" %d , %.6f, %.6f \n " , $num , $time_read /1000 , $time_extract /1000 );
$time_read = 0 ;
$time_extract = 0 ;
}
unlink $member or die " unlink: $! " ;
}
$mu ->record(' after loop ' );
$mu ->dump ();
결과:
1000, 0.000419, 0.000705
2000, 0.000361, 0.000610
3000, 0.000412, 0.000692
4000, 0.000428, 0.000721
5000, 0.000457, 0.000770
6000, 0.000459, 0.000787
7000, 0.000475, 0.000797
8000, 0.000421, 0.000705
9000, 0.000428, 0.000721
10000, 0.000424, 0.000715
11000, 0.000418, 0.000706
12000, 0.000439, 0.000735
13000, 0.000388, 0.000636
14000, 0.000478, 0.000790
15000, 0.000456, 0.000777
16000, 0.000433, 0.000727
17000, 0.000425, 0.000721
18000, 0.000322, 0.000543
19000, 0.000392, 0.000655
20000, 0.000395, 0.000665
time vsz ( diff) rss ( diff) shared ( diff) code ( diff) data ( diff)
0 94872 ( 94872) 8884 ( 8884) 2036 ( 2036) 1412 ( 1412) 7140 ( 7140) before loop
24 94872 ( 0) 8976 ( 92) 2096 ( 60) 1412 ( 0) 7140 ( 0) after loop
read() 시간도 일정
extract() 시간도 일정
메모리도 별다른 누수는 없는 걸로 보임
매번 객체를 생성하는 오버헤드는 어쩔 수 없이 있겠지만, 전체 실행 시간을 생각하면 훨씬 이득이다. (24초 정도 걸렸음)
read()가 압축 파일을 읽을 때 기존에 읽었던 메모리를 제대로 반납하지 않는지, read()를 할 때마다 메모리를 차지함
게다가 압축 파일의 내용을 읽어 추출할 때 이렇게 늘어난 메모리의 양의 영향을 받는 걸로 보인다.
테스트는 extractTree()와 extractM emberWithoutPaths() 두 가지만 가지고 했으나, 아마도 extract... 함수들 모두에 적용될 것으로 짐작됨.
이제 증상은 파악했으니 모듈 소스를 들여다보고 문제점을 찾으면 좋겠으나... 그럴 의지도 시간도 없어서 버그 리포트나 해야겠다. 어쩌면 이미 리포트도 되어 있는지도 모르겠음.
아무튼 이 모듈을 쓰는 사람은 이것을 명심하여 다수의 압축 파일을 처리할 때는 매번 Archive::Zip 객체를 새로 만드는 게 좋겠다.
2. 기타 & Comments
컴퓨터분류