パフォーマンスチューニング
OkiBlogのパフォーマンス
RubyでOkiBlogを書いているが、データ構造の問題などからパフォーマンスが悪い。実際に2006年4月1日時点でどの程度かabを使って測定.
-nでリクエスト回数、-cで同時リクエスト回数
-bash-3.00$ which ab
/usr/local/apache2/bin/ab
-bash-3.00$ ab -n 1 -c 1 http://www.oklab.org/cgi-bin/OkiBlog.cgi
This is ApacheBench, Version 2.0.41-dev <$Revision: 1.121.2.12 $> apache-2.0
Copyright (c) 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Copyright (c) 1998-2002 The Apache Software Foundation, http://www.apache.org/
Benchmarking www.oklab.org (be patient).....done
Server Software: Apache
Server Hostname: www.oklab.org
Server Port: 80
Document Path: /cgi-bin/OkiBlog.cgi
Document Length: 190149 bytes
Concurrency Level: 1
Time taken for tests: 1.568561 seconds
Complete requests: 1
Failed requests: 0
Write errors: 0
Total transferred: 190302 bytes
HTML transferred: 190149 bytes
Requests per second: 0.64 [#/sec] (mean)
Time per request: 1568.561 [ms] (mean)
Time per request: 1568.561 [ms] (mean, across all concurrent requests)
Transfer rate: 117.94 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 13 13 0.0 13 13
Processing: 1554 1554 0.0 1554 1554
Waiting: 1070 1070 0.0 1070 1070
Total: 1567 1567 0.0 1567 1567
ネットワークのパフォーマンス測定
netperf
はじめに
ネットワークのパフォーマンス測定ソフトです。検索エンジンで調べると日本語の情報も結構ありますのでこのツールを利用してパフォーマンスを図ってみたいと思います。Copyrightを確認すると、どうやらHP(Hewlett-Packard Companyの製品のようです。GNUのツールではないのでLinuxでお約束のconfigure,make,make installをするようではないようです。
ダウンロード
The Public Netperf Homepageからnetperf-2.2p14.tar.gzをダウンロード。
インストール
root権限で/usr/local/netperf-2.2pl4に展開します。makefileを確認するとデフォルトで/opt/netperfにインストールするようです。特に問題がないのでmakeコマンドを実行します。
# cp /tmp/netperf-2.2p14.tar.gz /usr/local/.
# tar -zxvf netperf-2.2p14.tar.gz
# cd netperf-2.2p14.tar.gz
# make
エラーが出ました。
cc -O -DDEBUG_LOG_FILE="\"/tmp/netperf.debug\"" \
-DNEED_MAKEFILE_EDIT -c -o netperf.o netperf.c
netperf.c:2:2: #error you must first edit and customize the makefile to your platform
make: *** [netperf.o] エラー 1
CLFAGS変数に-DNEED_MAKEFILE_EDITが指定してあるので削除してください。makeファイルを自分のシステム用にカスタマイズしたことを証明するフラグになっているようです。
# make install
/opt/netperfにインストールされました。
#cd /opt/netperf
# ls
netperf snapshot_script tcp_rr_script udp_rr_script
netserver tcp_range_script tcp_stream_script udp_stream_script
上記のようにコマンドがインストールされたことを確認してください。インストールマニュアル(英語)次にターゲットとなる環境にも同様にnetperfをインストールします。ターゲットにnetperfのサーバを立ち上げてそのサーバとパフォーマンス測定を行うためです。ターゲットマシンにインストールしたら以下のコマンドでサーバを起動します。
#/opt/netperf/netserver -p 12865
#ps -ef | grep net
root 29110 1 0 Apr19 ? 00:00:00 xinetd -stayalive -pidfile /var/
root 16427 1 0 00:17 ? 00:00:00 ./netserver -p 12865
root 16429 16198 0 00:17 pts/0 00:00:00 grep net
サーバが起動していることが確認できます。デフォルトのポートが12865番であるようです。はじめにインストールしたときにターゲット側には、netperfが必要ないと思っていたので以下のようにコマンドを実行しました。
# ./netperf -H 192.168.11.8
establish_control: control socket connect failed: Connection refused
Are you sure there is a netserver running on 192.168.11.8 at port 12865?
すると、対象のサーバはあがっているのか?ポートは12865だぞ!っと怒られましたので英語のマニュアルを読んでこの部分は解決しました。
netperfインストールした環境
- OS:Red Hat Linux 8.0 (Psyche)
- Kernel:2.4.18-14
- CPU: Intel Celeron 2.50GHz
- Mem: 512MB
- DISK: 約16GB
- IP: 192.168.11.5
対象(ターゲット)環境
- OS: Red Hat Linux 8.0 (Psyche)
- Kernel:2.4.18-14
- CPU:Intel Celeron 330MHz
- Mem: 643MB (cat /etc/proc)
- DISK: 約20GB
- IP:192.168.11.8
テスト
マニュアルを読むとTCP Stream Performanceという項目があるのでこれを実行してみます。pingと同様これが正常に行えれば今後テストも可能なので、もし正常に動作しなかった場合はいろいろ情報を調べてみてください。
TCP Stream Performance
netperfデフォルトのテストです。最も簡単なコマンドは以下になります。
/opt/netperf/netperf -H 192.168.11.8
10秒すると結果が表示されます。
TCP STREAM TEST to 192.168.11.8
Recv Send Send
Socket Socket Message Elapsed
Size Size Size Time Throughput
bytes bytes bytes secs. 10^6bits/sec
87380 16384 16384 10.01 93.74
ちょっと縦に表示されるので見づらいので整形すると、以下のようになると思います。
Recv Socket Size Bytes 87380
Send Sccket Size Bytes 16384
Send Message Size Bytes 16384
Elapsed Time secs 10.01
Throghput 10^6bits/sec 93.74
netperfは多岐にわたる測定が可能なようです。CPU rate calibration(CPU割合測定)も可能なようです。つまりネットワークの負荷時にどの程度CPUに負荷がかかっているかの測定です。単純に測定するだけは英文を読めばいいのですが、何を測定しているかがわからない状態になりますので、C言語などでTCP/IPプログラミングをした後に詳細に進んでいきたいと思います。
netstat ホストのネットワーク統計と状態確認
- 2004年9月14日 - 記事作成
- 2006年4月1日 - 加筆、修正、XHTML対応
C言語,Perl, Java言語,BourneShellの速度検証
はじめに
ソフトウェア開発では、テストを行う際にテストデータを作成する。テストデータも簡単なものであればスクリプト言語などで自動生成したほうが良い。テストデータを作成するためにBourneShellでスクリプトを記述したら自分の予想に反してプログラムの実行速度が遅かった。そこで同じようなプログラムを4つの言語で記述してみた。
プログラム仕様は、16bytesのデータを65536 (1024 * 64)回ループさせ、1048576(1024 * 1024=1024KB)のテストデータをファイルに出力するものである。本来は16bytesのデータをプログラム上で動的に生成するがこの速度検証では固定値を利用している。
どのプログラムも10回ぐらい実行して平均的な速度をリストする。
開発環境
- OS:x86 Solaris9
- CPU: Celeron300MHz*2,
- Memory: 636MB,
- Disk:IDE 16GB
2006年4月1日追記 - これらプログラムの情報でコンパイラや言語の実装バージョンを明記すべきであったと加筆、修正して感じた。例えば、C言語はコンパイラなどで速度が大きく差が出るし、Javaなども毎度のバージョンアップで基本性能は変わらないが、APIなどIOのチューニングは当然のように性能向上している。
C言語
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define MAX_SIZE 65536
int main(int argc, char *argv[])
{
int cnt;
FILE *fp;
char buf [17] = "0123456789ABCDEF";
char *file_nm = "c_test.dat";
unlink(file_nm);
fp = fopen(file_nm, "a+");
if ( fp == NULL )
{
perror("open error\n");
exit(1);
}
for ( cnt = 0; cnt < MAX_SIZE; cnt++)
{
printf("%d\n", cnt);
fwrite(buf, strlen(buf), 1, fp);
}
fflush(fp);
fclose(fp);
return 0;
}
Makefile
CC=gcc
CFLAGS= -std=c89 -pedantic -Wall -O2
CPPFLAGS=
LIBS=
INCLUDES=
PROGRAM=counter
OBJS= counter.o
all : $(PROGRAM)
$(PROGRAM) : $(OBJS)
$(CC) -o $@ $(OBJS) $(LIBS)
SUFFIXES : .c .o
.c.o :
$(CC) $(CFLAGS) -c $< $(INCLUDES)
clean :
$(RM) -f $(OBJS) $(PROGRAM)
C言語のコンパイルには、-O2オプションで最適化を行う。
Java言語
import java.io.*;
public class Counter {
private static final int MAX_SIZE = 65536;
private static String testData = "0123456789ABCDEF";
private static StringBuffer sb = new StringBuffer();
public static void main(String []args) {
int cnt;
for ( cnt = 0; cnt < MAX_SIZE; cnt++ ) {
System.out.println(cnt);
sb.append(testData);
}
System.out.println(sb.toString().length());
try {
File file = new File("java_counter.dat");
file.createNewFile();
Writer out = new BufferedWriter(new FileWriter(file),
sb.toString().length());
out.write(sb.toString(), 0, sb.toString().length());
out.flush();
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
JVM実行時のオプションに-serverを加える。
time java -server -cp . Counter
Perl
#!/usr/local/bin/perl
$cnt=0;
$MAX_SIZE=65536;
$tmp_data="0123456789ABCDEF";
$out_data="";
while ( $cnt < $MAX_SIZE ) {
$cnt++;
$out_data = $out_data . $tmp_data;
print $cnt . "\n";
}
print $out_data;
BourneShell
counter.sh
#!/usr/bin/sh
CNT=0
MAX_SIZE=65536
TMP_DATA="0123456789ABCDEF"
OUT_DATA=""
while [ ${CNT} -lt ${MAX_SIZE} ]; do
CNT=`expr ${CNT} + 1`
OUT_DATA="${TMP_DATA}${OUT_DATA}"
echo ${CNT}
done
echo OUT_DATA
counter.sh プログラムでは、メモリが足りなくなりプログラムが停止してしまった。BourneShellでは文字列の演算時にメモリを動的にその領域分確保して、開放していないことが予想される。そのため代案としてcounter2.shを用意した。counter2.shでは文字列演算を行わずファイル出力している。
counter2.sh
#!/usr/bin/sh
CNT=0
MAX_SIZE=65536
TMP_DATA="0123456789ABCDEF"
OUT_FILE="test.dat"
while [ ${CNT} -lt ${MAX_SIZE} ]; do
CNT=`expr ${CNT} + 1`
/usr/bin/echo "${TMP_DATA}\c" >> ${OUT_FILE}
echo ${CNT}
done
cat ${OUT_FILE}
簡単な検証
timeコマンドで実行速度を測定して見た結果
Language time
C 0m3.034s
Java 0m16.030s(-clientオプション:0m13.856s)
Perl 10m27.255s
BourneShell 36m26.860s
Perlのチューニング
BourneShellが遅いことは予想できたが、Perlがあまりにも遅かったのでプログラミングPerl 改訂版を参考に修正したら驚くほどのパフォーマンス向上が出来た。
counter2.pl
#!/usr/local/bin/perl
$cnt=0;
$tmp_data="0123456789ABCDEF";
$out_data="$tmp_data" x 65536;
$out_data="";
while ( $cnt < 65536 ) {
$cnt++;
print "$cnt\n";
$out_data .= $tmp_data;
}
open(FH,">perl_test.dat");
print FH $out_data;
close(FH);
まず、文字列の領域をあらかじめ確保して、while文の演算中にメモリ領域確保を行わないようにした。また、文字列の演算を.=にした。驚くほどのパフォーマンスが得られた。Javaのチューニングを何もしていないとはいえCに勝る性能。
Language time
C 0m3.034s
Java 0m16.030s(-clientオプション:0m13.856s)
Perl 0m4.692s
BourneShell 36m26.860s
また、標準出力にカウンタを表示しているため速度低下が見られたのですべての言語でループ内の標準出力処理を行わないようにして再測定
Language time
C 0m0.094s
Java 0m2.468s(-clientオプション:0m1.654s)
Perl 0m0.297s
BourneShell 36m26.860s
Perlの速度検証のためにループ中にファイル出力してみる
counter2-1.pl
#!/usr/local/bin/perl
$cnt=0;
$tmp_data="0123456789ABCDEF";
#$out_data="$tmp_data" x 65536;
#$out_data="";
open(FH,">perl_test21.dat");
while ( $cnt < 65536 ) {
$cnt++;
# $out_data .= $tmp_data;
print FH $tmp_data;
}
close(FH);
0m0.343s やはり一度にファイル出力する方が速い。Perlにも出力する際のbuffering機能はあるのだろうか?要調査。
BourneShellのチューニング
BourneShellがあまりにも遅いのでチューニングの調査。
counter2.sh
#!/usr/bin/sh
CNT=0
MAX_SIZE=65536
TMP_DATA="0123456789ABCDEF"
OUT_FILE="test.dat"
while [ ${CNT} -lt ${MAX_SIZE} ]; do
CNT=`expr ${CNT} + 1`
/usr/bin/echo "${TMP_DATA}\c" >> ${OUT_FILE}
echo ${CNT}
done
cat ${OUT_FILE}
まず、``バッククォートによる、exprコマンドの呼び出しが非常に遅いのが分かったため、perl -eでfor文が実行される前に、条件に固定値を与えた。GNU sh-utilsにseqというsequenceを出力するコマンドがある。これとperlの速度を検証した結果perlの方が若干速かった。しかしseqの方か可読性が良い。
bash-2.05# time perl -e 'for ($j=0; $j < 65536; $j++) { print "$j "}' > aaa
real 0m0.382s
user 0m0.320s
sys 0m0.040s
bash-2.05# time seq 1 65536 >> aaa
real 0m0.406s
user 0m0.350s
sys 0m0.030s
次に、/usr/bin/echoなど外部コマンドの呼び出しコストが高すぎたのでechoにより、Shellscriptのビルドインコマンドに修正した。
counter3.sh
#!/usr/bin/sh
TMP_DATA="0123456789ABCDEF"
OUT_FILE="sh_counter3.dat"
for i in `perl -e 'for ($j=0; $j < 65536; $j++) { print "$j "}'`; do
echo "${TMP_DATA}\c" >> ${OUT_FILE}
done
Language time
C 0m0.094s
Java 0m2.468s(-clientオプション:0m1.654s)
Perl 0m0.297s
BourneShell 0m11.394s
Javaプログラムのチューニング。
C,Perlは、native言語であるが、コンパイルかインタプリタの違いが速度結果に反映されている。Java言語はnativeコードにコンバートするためどうしてもボトルネックになってしまう。せめてPerlに勝てるようなソースコードを作成してみたい。
import java.io.*;
public class Counter3 {
private final int MAX_SIZE = 65536;
private void calc() throws IOException {
String testData = "0123456789ABCDEF";
File file = new File("java_counter.dat");
int tmp = testData.length();
Writer out = new BufferedWriter(new FileWriter(file, true), tmp);
for ( int cnt = MAX_SIZE; cnt > 0; cnt-- ) {
out.write(testData, 0, tmp);
}
out.flush();
out.close();
}
public static void main(String []args) throws IOException {
new Counter3().calc();
}
}
1000ミリ秒ぐらいは少なく出来た。
Language time
C 0m0.094s
Java 0m1.836s(-clientオプション:0m1.348s)
Perl 0m0.297s
BourneShell 0m11.394s
java -Xrunhprof:cpu=samples,depth=6 -cp . Counter3
上記のような実行でプロファイルを見るとやはり、nativeに出力する際にボトルネックがあるようだ。
CPU SAMPLES BEGIN (total = 55) Sun Sep 12 08:21:29 2004
rank self accum count trace method
1 85.45% 85.45% 47 8 sun.io.CharToByteEUC_JP_Solaris.convert
2 3.64% 89.09% 2 9 java.io.FileOutputStream.writeBytes
3 1.82% 90.91% 1 5 sun.net.www.protocol.file.Handler.createFileURL
Connection
4 1.82% 92.73% 1 2 java.util.jar.JarFile.getEntry
5 1.82% 94.55% 1 3 java.util.zip.ZipFile.getInputStream
6 1.82% 96.36% 1 4 sun.misc.URLClassPath$FileLoader.
7 1.82% 98.18% 1 6 java.security.Permissions.add
8 1.82% 100.00% 1 1 sun.misc.URLClassPath$JarLoader.getJarFile
CPU SAMPLES END
上記JVMの起動に500ミリ秒近くかかるのは仕方ないとして、ファイル出力の際のConvertを何とかできないものか。C言語のようにjava.lang.Stringオブジェクトを返さずにbyte配列を使う
import java.io.*;
public class Counter4 {
private final int MAX_SIZE = 65536;
private void calc() throws IOException {
byte testData [] =
{ '0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F' };
int tmp = testData.length;
OutputStream out = new BufferedOutputStream(
new FileOutputStream(new File("java_counter4.dat")));
for ( int cnt = MAX_SIZE; cnt > 0; cnt-- ) {
out.write(testData, 0, tmp);
}
out.flush();
out.close();
}
public static void main(String []args) throws IOException {
new Counter4().calc();
}
}
bash-2.05$ time java -client -cp . Counter4
real 0m0.876s
user 0m0.660s
sys 0m0.150s
bash-2.05$ time java -server -cp . Counter4
real 0m1.186s
user 0m1.140s
sys 0m0.240s
CPU SAMPLES BEGIN (total = 15) Tue Sep 14 01:24:16 2004
rank self accum count trace method
1 53.33% 53.33% 8 9 java.io.FileOutputStream.writeBytes
2 6.67% 60.00% 1 3 java.util.jar.JarFile.hasClassPathAttribute
3 6.67% 66.67% 1 4 java.lang.StringCoding$ConverterSE.encode
4 6.67% 73.33% 1 2 sun.misc.SharedSecrets.<clinit>
5 6.67% 80.00% 1 6 java.io.FilePermission$1.run
6 6.67% 86.67% 1 5 java.io.ObjectStreamField.<init>
7 6.67% 93.33% 1 8 java.lang.ClassLoader.findLoadedClass
8 6.67% 100.00% 1 1 sun.misc.URLClassPath$3.run
CPU SAMPLES END
他の言語と異なり、Javaはnativeとのやり取りに負荷がかかるのでメモリにバッファリングしてファイル出力した方が効率がよさそうだ。
JVMの起動時間測定
bash-2.05$ time java -client -cp . 1> /dev/null 2>&1
real 0m0.593s
user 0m0.430s
sys 0m0.070s
結果
はじめに作ったプログラムと最終的なプログラムが以下になる。
Language time
C 0m3.034s
Java 0m16.030s(-clientオプション:0m13.856s)
Perl 10m27.255s
BourneShell 36m26.860s
Language time
C 0m0.094s
Java 0m1.186s(-clientオプション:0m0.876s)
Perl 0m0.297s
BourneShell 0m11.394s
結論としていいたいことは、憶測などや誰かが言っていたという情報ははっきり言って説得力がない。自分でテストしてみる。またはその憶測の情報源を提示するというのが大切である。今の時代は自分で書かなくてもインターネットという共有知識が代弁してくれる。
CPUのパフォーマンス測定
sysstatのインストール
SolarisやRed Hat Enterprise Linuxなどには、システム統計情報を収集するsysstatツールがデフォルトでインストールされる。Linux版sysstatユーティリティは、Sebastien Godard氏によりメンテナンスされているためRed Hat 8にインストールする。
http://perso.wanadoo.fr/sebastien.godard/からsysstat-5.0.6.tar.gzをダウンロードして以下のコマンドでインストールをおこなう。
$ tar -zxvf sysstat-5.0.6.tar.gz
$ cd sysstat-5.0.6
$ make config
$ make
# make install
更新履歴
- 2006年4月1日 - XHTMLに修正
- 2004年9月11日 - 記述.

