Windows7,2008R2に引き続き、これまた1年越しの作業になりましたが、我がohfuji.nameをホストするマシンをOpenBlockS 600(正確にはOpenBlockS 600D相当)に置き換えました。
OpenBlockS 600とは、ぷらっとホーム社さんが製造・販売しているマイクロサーバーで、
こちらが製品情報になります。ちなみに2月現在キャンペーンをやっておられます。
OpenBlockS 600自体の解説はいろいろな場所で行われているので、そちらにおまかせしますが、特質すべきは、抜群の低消費電力で、私がエコワットで測定した結果は9Wでした。またファンレスでストレージはコンパクトフラッシュを使うので音が出なくてかつ障害に強く、商業利用はもちろん、自宅サーバーとしても重宝するかと思います。
OSですが、OpenBlockS 600はSSD Linuxがプリインストールされています。また600DはDebianがプリインストールされています。メモリは1GB積んでいますのでDNSサーバーやメールサーバーとしては申し分ないスペックです。
難点が、CPUにPOWER-PCを使用しているところで、私のようなプログラミングをする人間にとっては開発環境を別途用意しないといけないのと、さらにそのCPUの動作周波数が600MHzとお世辞にも速いと言えないところで、Apacheで静的なページを運用するならともかく動的なページは難があるかと思います。特に普通のサーバーでも重たいWordpressをOpenBlockS 600で運用するのは厳しいかと思います。
では、このブログ(Wordpressなんですが・・)はどうしているのかと言いますと、このページはADPで作成したブログビューアーで表示しています。我がADPもOpen BlockS 600Dに移植しまして、このとおり動作しておる次第です。このページを頻繁に訪問される方は気がついておられたかと思いますが、最近Wordpressが重くなっていたので、どげんかせんといかんと思っておったところです。このような厳しい条件を克服するのはソフトウェアエンジニアとしてロマンを感じたりします。
しばらく運用してみてOKであれば、OpenBlockS 600D版のADPと共にブログビューアー(Adp WorPdress bLOG viewer - AWPLOG)のソースを公開しようかと思っております。
2011/06/23 追記:節電の為、自宅サーバー類は仮想マシンとして別のサーバーに集約しましたので、現在このサーバーはOpenBlocks 600D上では動作していません。
以前に書いた
この記事に関してコメントをもらいちょうど記事にしようかと思っていたところでしたので、ADPのキャッシュ機能を使い、
この記事の実験をADPでやったらどうなるかみてみます。
SQLでjoin(結合)と言えばSQLに慣れた方にとっては馴染み深いものですが、初心者にとっては一種の登竜門のようで、joinを避けたコードを見かけたりすることがあります(まぁ私も十数年前にはこのような理由でjoinを避けたコードを書いた記憶があります)。また、O/Rマッパーではテーブル毎にクラスを対応させる関係で、joinの取扱がややこしかったりします。
それ以外でも、私の場合になりますが、過去にパフォーマンス上の理由からjoinを行わなかったことがあります。
今回は、前回の実験と同様に
・SQLでjoinさせる。
・ADPでjoinさせる。
でパフォーマンスの違いについていくつかの実験を行い計測します。
実験環境
JOINのパフォーマンス実験環境はこちらに記述しています。
実験1 素直にSQL側でjoinをさせたものを実行
例により、SQLで素直にjoinさせてみます。以下のようなコードになります。
,$db = "DSN=Trade"
,$str = "SELECT Price.CODE, RDATE, OPEN, CLOSE, NAME FROM Price "
"INNER JOIN Company ON (Price.CODE = Company.CODE)"
,sql@($db,$str,[]).csv.prtn,next;
少しコードの説明を、
1行目の、$db=~ の部分は、ODBCの接続文字列を指定します。上記のコードは、ODBCのデータソース名Tradeを指定している接続文字列になっています。
2,3行目の、$strの部分はSQL文を変数$strに代入しています。本来は1行で書けますが、wordpressで見やすいように2行で書いています。
4行目の
,sql@($db,$str,[]).csv.prtn,next;
sqlは組み込みの述語で、「ODBC-APIを使いsqlを実行し、結果を配列(@)で受け取り、csvに変換し、prtnで画面に出力し、nextで全ての結果を出力する」というコードになります。
自画自賛になりますが、必要最低限の情報だけで簡単にSQLが発行できているので、ADPの開発目標の一つである「SQLとの親和性が高い言語を目指す」を具現している例だと思います。
実行時間ですが、
D:\>adp -t sql_test_1.p > sql_test1.txt
time is 119192ms.
で、約119秒となりました。
実験2-A ADP側でjoin(ネステッドループ)
続いて、ADP側でネステッドループjoinさせてみましょう。
,$db = "DSN=Trade"
,$price = "SELECT CODE,RDATE,OPEN,CLOSE FROM Price"
,$company = "SELECT NAME FROM Company WHERE CODE = ?"
,sql( $db, $price, [], @rec)
,sql( $db,$company, [$rec[0]], $name)
,csv($rec,$name).prtn,next;
ADPのDBライブラリは、前に紹介しました
ODBCライブラリがベースになっていますので、ODBCのパラメータクエリが使えます。
5行目のコードがパラメータクエリを使っています。
実行時間ですが、
D:\>adp -t sql_test_2.p > sql_test2.txt
time is 1717284ms.
で、約1717秒となりました。実験1と比べて約14倍の実行時間です。
実験2-B ADP側でjoin(ネステッドループ&キャッシュ)
さらに続いて、ネステッドループjoinをADPのキャッシュ機能を使って高速化をはかります。
,$db = "DSN=Trade"
,$price = "SELECT CODE,RDATE,OPEN,CLOSE FROM Price"
,$company = "SELECT NAME FROM Company WHERE CODE = ?"
,sql( $db, $price, [], @rec)
,sql$( $db,$company, [$rec[0]], $name)
,csv($rec,$name).prtn,next;
呼び出し述語名の後ろに$をつければキャッシュ機能がONになります。上記のコードでは5行目の sql$ がキャッシュ機能を使用しています。
では、実行時間をみてみましょう。
D:\>adp -t sql_test_2.p > sql_test2.txt
time is 116770ms.
で、約117秒となりました。
実験2-Aと比べるとかなり高速化がはかられたかと思います。キャッシュのこのような使い方は、かなり有効だとうことが解るかと思います。繰り返しになりますが、ADPならお手軽にキャッシュ機能を使うことができます。
実験3 ADP側でjoin(事前にマップ作成)
ちなみに、ADPでも事前にマップを作成し、joinを行うことができます。
以下、コード例です。
,$db = "DSN=Trade"
,@tbl = {}
,sql($db, "SELECT CODE,NAME FROM Company",[], @r)
,@tbl = @tbl + [ $r["CODE"] | $r["NAME"] ]
,next
,sql($db, "SELECT CODE,RDATE,OPEN,CLOSE FROM Price",[],@rec)
,$key == $rec["CODE"].str
,csv($rec,$tbl[$key]).printn,next;
前回の記事ではC++でハッシュjoinを行うと書いたので『ハッシュJOINを言語で再開発するのは非効率』とコメントをもらいました。
コードを良く読んで頂ければ解るかと思いますが、実はC++の例でもjoin自体はプログラミング言語(ライブラリ)の機能を使っており、取り立てて複雑なことはしていません。
やっていることを説明しますと、マスターテーブル用のマップを事前に作成し、それを使ってjoinを行っています。慣れていない人にとっては難しいかもしれませんが、古くはperlの連想記憶、最近(これも古いが)の例ではVBScriptのディクショナリに相当します。DBMSを使わないで日常的にファイル処理を行っている方にとっては日常的なコードかと思います。
ちなみに、ADPのコード例ですが非常にすっきりとしているかと思います。C++の例と比べると本来やろうとしていることが明確になっているかと思います。
実行時間は、
D:\>adp -t sql_test_3.p > test3.txt
time is 110988ms.
で、約111秒とやはり実験1より速くなっていることが解ります。
こうしてみると、実験2-Bが思いのほか速くなっていないと思わるでしょう。
これはSQLの実行回数に関係しています。
各実験のSQLの実行回数を見てみましょう。
SQLの実行回数
実験1 | 1回 |
実験2-A | 約470万回(Priceテーブルの行数+1) |
実験2-B | 約2000回(Companyテーブルの行数+1) |
実験3 | 2回 |
になります。実験2のコードではテーブルの行数に比例した数だけSQLを実行することになります。実験2-Bが実験2-Aより速いのは、Priceテーブルの行数よりComapnyテーブルの行数が圧倒的に少ないから、つまり1対nの結合を行っているからで、仮に1対1の結合では速くならないということになります。
実験3がなぜ実験1より速いかですが、DBMS側から転送されるデータ量が違います。
以下、CSVファイルの先頭5行を表示します。
1717,2005-05-10 00:00:00.000,21251,3522,明豊ファシリティワークス(株)
1717,2005-05-11 00:00:00.000,21251,3522,明豊ファシリティワークス(株)
1717,2005-05-12 00:00:00.000,21251,3522,明豊ファシリティワークス(株)
1717,2005-05-13 00:00:00.000,21251,3522,明豊ファシリティワークス(株)
1717,2005-05-16 00:00:00.000,21251,3522,明豊ファシリティワークス(株)
企業名の『明豊ファシリティワークス(株)』が重複して余分なデータとなっています。実験1のコードではDBMSから言語側にこのように重複したデータが来ます。各実験で転送されるデータ量を見てみましょう。
結果データの転送量(CSVファイルベース)
実験1 | 約256MB |
実験2-A | 約256MB |
実験2-B | 約184MB |
実験3 | 約184MB |
実は、DBMSから言語側へ転送されるデータ量自体は、実験1より実験2-Bの方が少なくなります。そのような関係で、実験1より実験2の方が早くなっています。SQLの実行回数(実験1の方がよい)とデータ転送量(実験2の方がよい)になりますが、このあたりはハードウェアの環境やDBMSによって結果が変わってくるでしょう。
この2つのデータから実験3は、なるべく少ないSQLの実行回数で少ないデータ量を転送しているということが解るかと思います。
追記:コメント欄での指摘およびテスト再現性を考慮してテスト環境を整備して再度計測しています。
バージョンが0.5Xから0.6に変わりましたが、機能的には特に変わりません。
前回のリリースから継続的に実行性能の強化をしてまして、だいぶ速くなったので一旦リリースします。
どこまで速くなったかですが、ADP 0.60と各ブラウザとのフィボナッチ数列を求めるプログラムの実行時間の比較を行います。
ちなみに、JavaScriptのコードですが、
こちらにあるコードを使わせていただきました。
ADP側のコードは、以下のとおりです(高速化のためには元のソースも変更する必要がありましたので前とは若干変わっています)。
+fib(0,0),!;
+fib(1,1),!;
+fib($x,$y),fib($x - 1, $f1),fib($x - 2, $f2), $y == $f1 + $f2, !;
,fib(28).printn;
◆マシン
・CPU Core i7-980X
・メモリ 24GB(DDR3-1066 4GB × 6)
・OS Windows 7 Ulitimate (x64)
◆結果
28のフィボナッチ数列を求める時間
IE8(64ビット版) | 368ミリ秒 |
FireFox 3.6.13 | 167ミリ秒 |
Google Chrome 8.0.552.224 | 9ミリ秒 |
ADP 0.60 | 226ミリ秒 |
ADP 0.60の結果ですが、
IE8以上、FireFox3.6未満という結果になりました。個人的にはまだまだ不満足ですが、競争が激しくなったブラウザのJavaScriptとそう遜色がない結果になっているのでひとまず納得しておきます。
またGoogle Chromeの結果が突出していますが、これはJITコンパイラが利いているかと思います。この手のベンチマークの結果を誤解してほしくないので書いておきますとどんなプログラムも常にGoogle Chromeが突出して速いと言っているわけではござませんので結果を丸々鵜呑みにしないように注意してください
(実際の体感速度は皆様が使ってみて判断してください・・・)。
で、ここまでくると
『いったいどこまで速くなるのか?』
と疑問に思われるでしょう。というわけで、アセンブラ(正確にはインラインアセンブラ)のコードと実行結果を載せます。
#include <iostream>
using namespace std;
#if 0
int __fastcall fib(int f)
{
if ( f == 0 ) return f;
if ( f == 1 ) return f;
return fib(f-1) + fib(f-2);
}
#else
extern "C" {
int __declspec(naked) __fastcall fib(int f)
{
__asm push esi
__asm mov eax, ecx
__asm cmp ecx, 0
__asm je _return
__asm cmp ecx, 1
__asm je _return
__asm dec ecx
__asm call fib
__asm mov esi, eax
__asm dec ecx
__asm call fib
__asm add eax, esi
__asm add ecx, 2
_return:
__asm pop esi
__asm ret
}
}
#endif
int main(int argc, _TCHAR* argv[])
{
clock_t c = clock();
cout << "fib = " << fib(28) << endl;
cout << "Execute time is = " << (clock()-c)*1000.0/CLOCKS_PER_SEC << endl;
return 0;
}
プリプロセッサでアセンブラコードが動くようにしていますが、コードはC言語との比較もできるようにCのコードも掲載しています。Visual Studio 2008でコンパイル実行できます。実行時間は4m秒でした。ちなみに、フィボナッチ数例自体を高速に求める方法は他にあります。以前の例のようにADPキャッシュを使えば数ミリ秒になります。ここでは再起関数の呼び出し回数を変えないようにして各プログラミング言語自体が持つ基本的な速度について比較できるようにしています。
前回のリリース告知からはや2ヵ月半が過ぎやっと述語キャッシュ機能を付けました。
キャッシュ機能ですが、同一の関数(述語)の呼び出し(評価)を行う場合に、1回目の呼び出しの結果を保存しておき、2回目の呼び出しでは1回目の呼び出し結果を使います。同一の判定ですが、述語名と引数の値が同じならキャッシュします。変数に関しては、値があればその値が同じか比較し、値がなければ同一とみなします(変数名が違っていてもキャッシュが有効になります)。
なので、printn関数のように副作用がある場合やrand関数のように毎回異なる値になることを期待する述語の場合はキャッシュ機能を使うとバグリます。つまりprintnの場合は2回目の画面出力が行われなくなり、rand関数の場合は同じ値を返して乱数でなくなります。
以下、フィボナッチ数例の値を求めるサンプルを例に、使い方を示します。
まずはキャッシュなし版のコード(fib1.p)で、
+fib(0,0);
+fib(1,1);
+fib($x,$y),fib($x - 1, $f1),fib($x - 2, $f2), $y == $f 1+ $f2;
,fib(25).printn;
以下、私のマシン(Core i7-920)での実行結果です。
D:>adp -t fib1.p
75025
time is 1538ms.
以下、キャッシュあり版のコード(fib2.p)で、
+fib(0,0);
+fib(1,1);
+fib($x,$y),fib$($x - 1, $f1),fib$($x - 2, $f2), $y == $f 1+ $f2;
,fib(25).printn;
以下、私のマシン(Core i7-920)での実行結果です。
D:>adp -t fib2.p
75025
time is 3ms.
キャッシュあり版のコードですがぱっとみた感じ違いが解らないかもしれませんが、fib述語を呼び出している部分『fib$($x - 1, $f1)』で、fib$ と述語名の終わりに $ が付いています。つまり、
述語名の後に $ をつけるとキャッシュ機能が有効になります。
キャッシュあり版となし版で、速度を比較しますとあり版が激速になっていることが分かるかと思います。
fib1.pフィボナッチ数例のコードですが、良く見るコードだと思いますが、このサンプルは実行速度の面で問題があります。2回再帰関数を呼び出しているので実行時間は、入力する値が大きくなると指数関数的に増えて行きます。
fib2.pの例で、キャッシュ機能を使いますと、再帰呼び出しが事実上1回になりますので、入力値が大きくなっても実行時間が指数関数的には増えなくなります。
キャッシュは万能でないので、fib2.pの例では、再帰関数を使う別の問題(大きな数を計算させるとスタックがあふれる)は解決できませんが、ADPのキャッシュ機能を使えば、コードを若干修正するだけでそこそこの性能が得られるので適用対象によっては有効な武器となるでしょう。
(ちなみにですが、フィボナッチ数例を求める実用的なコードではもっと別の実装を選択します。)
通常プログラムを高速化したいときですが、プログラマは追い込まれており、出来るだけお手軽にキャッシュ機能をつけたいと思います。また、不具合が出たときに取り外すのも手軽にやりたいと思います。このようなコンセプトの元、
ADPでは、関数(述語)の呼び出しそのものをキャッシュする機能
としてキャッシュ機能を実装しています。これは、なかなか便利だと思います。
次回は、別の例でキャッシュ機能を紹介します。