TOP / 環境構築 / パフォーマンスチューニング

パフォーマンスチューニング

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日 - 記述.


イバラキングへのリンク Get Firefox Valid XHTML 1.1 Apple Darwinへのリンク