[ADP開発日誌]0.74リリース マルチスレッド化の第一歩 & LLPlanets発表用リリース


ADP公開一周年記念記事がまだ途中ですが、 Ver0.74のリリースを行います。

Ver0.74は、Accessでの整数のインサート時のエラーの改修と、pipe述語の実装があります。
pipe述語というのは、以前話に出ました、マルチスレッド機能の1つでパイプライン処理を実現する述語になります。
ちなみに、本リリースにに基づき、LLPlanetsのライトニングトークで発表を行います。私を見かけた人は『ブログ見てます』と声を掛けていただければうれしかったりします。

では、pipe述語の使用例を見てみましょう。何回かやっていて最近ホットなSQLのパフォーマンスについての例になります。
関連記事1:[ADP開発日誌]SQL(JOIN)の実行パフォーマンスについて2011
関連記事2:SQLの実行パフォーマンスについて 2010

実験環境

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;
 
[ADP開発日誌]SQL(JOIN)の実行パフォーマンスについて2011の実験1と同じです。
実行時間も同じで、約119秒です。

実験2 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)
 ,pipe
 ,sql( $db,$company, [$rec[0]], $name)
  ,csv($rec,$name).prtn,next;
 
[ADP開発日誌]SQL(JOIN)の実行パフォーマンスについて2011の実験2-Bと同じコードになります。
実行時間ですが、約117秒となりました。実験1と比べて約1.6%程速くなっています。

実験3 ADP側でjoin(事前にマップ作成)

3つ目は、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;
 
[ADP開発日誌]SQL(JOIN)の実行パフォーマンスについて2011の実験3と同じコードです。
実行時間ですが、約111秒で実験1より7%ほど速くなっていることが解ります。


続いて、pipe述語を使って並行処理をさせてみます。

実験1-P 素直にSQL側でjoinをさせたものをpipe実行

実験1のコードにpipe述語を挿入しています。
,$db = "DSN=Trade"
,$str = "SELECT Price.CODE, RDATE, OPEN, CLOSE, NAME FROM Price "
        "INNER JOIN Company ON (Price.CODE = Company.CODE)"
,sql@($db,$str,[]).pipe.csv.prtn,next;
実験1のコードとの違いは4行目の
,sql@($db,$str,[]).pipe.csv.prtn,next;
のpipeという記述で、これがpipe述語になります。pipe述語で区切られたコードは並行で処理を行います。
つまり
,sql@($db,$str,[])
の部分(バックトラックの実行)と
.csv.prtn,next;
の部分は並行で動作します。
sqlの部分は、.csv.prtn,nextの実行中にバックトラックを行います。
next述語で、pipeまで戻りますと、sqlの実行を待ち(同期)データを受け取ります。
ややこしいかも知れませんが、図で示すとよくわかるかと思います。


図で、青の矢印の部分と赤の矢印の部分がそれぞれ別のスレッドになっており平行で動作しています。
pipe述語が無い場合の動作イメージは以下のとおりです。


比較してみますと分かりますが、sql述語~next述語まででループがありますが、それを2つに分けて実行するイメージになります。
UnixのシェルやWindowsのコマンドプロンプトで、|(パイプ)を使ってコマンドをつなげることがありますが、pipe述語の実行イメージはこれと同様になります。
シェルのパイプ(|)は20年以上前からあり、お手軽にマルチタスク処理を実現できるのですがプログラム言語レベルで使えるものがなく、マルチスレッドプログラムとなるとなぜかややこしくなります。
ADPではお手軽にマルチスレッドプログラムを体験して頂くため、その一つとしてパイプを実装しました。

実行時間は、約108秒で、約9%速くなっています。少しですが実験3よりも速くなっていることが解ります。

実験2-P ADP側でjoin(ネステッドループ&キャッシュ)でpipe実行

続いて、実験2のコードにpipe述語を挿入しています。
,$db = "DSN=Trade"
,$price = "SELECT CODE,RDATE,OPEN,CLOSE FROM Price"
,$company = "SELECT NAME FROM Company WHERE CODE = ?"
,sql( $db, $price, [], @rec)
 ,pipe
 ,sql$( $db,$company, [$rec[0]], $name)
  ,csv($rec,$name).prtn,next;

実行時間は、約89秒で実験2と比べて約24%速くなっています。
興味深いのは実験1-Pよりも速度向上が大きいです。pipe述語は半分に分割してそれぞれ実行するという方式をとっていますが、当然ですが常に半分になるとは限りません。上手く半分に分割できる場合もありますし、そうでない場合もあります。そのような関係でこのような逆転現象が発生します。一口にJOINのパフォーマンスといってもこのように様々な要因が絡んできますので、一概に『○○が効率的』といえないことを表す良い例となっています。

実験2-PP ADP側でjoin(ネステッドループ&キャッシュ)でpipe実行2

実験2-Pのコードにさらにpipe述語を挿入しています。pipe述語は1つだけでなく複数入れることもできます。
,$db = "DSN=Trade"
,$price = "SELECT CODE,RDATE,OPEN,CLOSE FROM Price"
,$company = "SELECT NAME FROM Company WHERE CODE = ?"
,sql( $db, $price, [], @rec)
 ,pipe
 ,sql$( $db,$company, [$rec[0]], $name)
  ,pipe
  ,csv($rec,$name).prtn,next;

実行時間は、約112秒で実験2-PPと比べて逆に遅くなっています。このように闇雲にマルチスレッドを行っても必ずしも速くならない場合がある(もちろん速くなる場合もある)のが面白いところです。pipe述語を2つ使うと3つスレッドが動作しますが、実験環境ではCPUコアが2つしかないので足の引っ張り合いのようなことになったようです。

実験3-P ADP側でjoin(事前にマップ作成)でpipe実行

続いて、実験3のコードにpipe述語を挿入しています。
,$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;

実行時間は、約91秒で、実験3と比べて約18%速くなっています。

ちなみに実験3-Pからさらにpipeを挿入しても良いのですが、実験2-Pの時と同様にあまり速くならないので省略します。

結論


各実験結果を示します。
pipe述語の効果
実験実行時間(秒)
実験1119
実験2117
実験3111
実験1-P108
実験2-P89
実験2-PP112
実験3-P91

実験1~3どの場合でも、pipe述語が有効だということが分かります。これは、
・DBMSからデータを取得する
・ファイルへ書き出す
という2つのIO処理があり、pipe述語によって、それらを同時に実行することが出来る為です。
また実験2-PPと実験2-Pを比べても分かりますとおり闇雲にマルチスレッド化しても高速化が図れない場合もあります。
パフォーマンスアップは様々な要素が関わってきますので実験により確認しながらということが必要になります。
pipe述語はお手軽にマルチスレッドを実現でき、また取り外しも楽なので簡単に実験や試行錯誤が出来ます。
ADPのpipe述語はキャッシュ機能と同様に便利な道具として利用できるかと思います。

また、実験1-P、2-P、3-Pを比較しますとどれをとってもパフォーマンスにあまり差がないことがわかるでしょう。ADPの開発にあたりプログラマの自由度を高めるということも考慮しています。つまり、『○○でなければダメ』ではなく、どのアルゴリズムを採用するかはプログラマーの判断で、いか様にも選択できるような言語を目指しています。

追記:コメント欄での指摘およびテスト再現性を考慮してテスト環境を整備して再度計測しています。

2011-08-19 | コメント:0件

OpenBlockS 600

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上では動作していません。
2011-02-07 | コメント:0件

[ADP開発日誌]SQL(JOIN)の実行パフォーマンスについて2011


以前に書いたこの記事に関してコメントをもらいちょうど記事にしようかと思っていたところでしたので、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の実行回数
実験11回
実験2-A約470万回(Priceテーブルの行数+1)
実験2-B約2000回(Companyテーブルの行数+1)
実験32回
 
になります。実験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の実行回数で少ないデータ量を転送しているということが解るかと思います。

追記:コメント欄での指摘およびテスト再現性を考慮してテスト環境を整備して再度計測しています。
2011-02-01 | コメント:0件

半導体微細化の物理的限界を読んで

9月に入りブログの更新がWeeklyになってしまいましたが、微妙なプレシャーを感じながら、ぼちぼち更新しますです。
 
サーバーのセットアップがてらブログネタを探していたら以下の記事が目にとまった。
 
半導体微細化の物理的限界  
現在の半導体のチャネル長(トランジスタの大きさ)は、Intelの最新鋭のCPUで32nmとか45nmとかになっていますが、2022年には4.5nmになっているとの予想があるらしい。
 
ちなみに、10年前は180nm(PentiumIIIの頃)で10年かけて概ね1/4から1/5になった計算になるので、2022年に4.5nmはちょっと行きすぎなような気もしないことはないですが、4.5nmで作られたCPUを想像しますと、クロックスピードは恐らく20GHzを超えているかと思いますし、コア数も128とかになっているのではないでしょうか?まぁCPUオタクとしてはそんなCPUの登場は楽しみです。
 
記事にも書いてありますが、微細化といっても単に小さくすれば良いのではなく、色々な問題が出て来て、その都度ブレークスルーがあったらしいですが、それでも微細化の苦労が我々の耳にも届くことがあり、最近ではリーク電流の増大が記憶に新しいかと思います。
今から6年程前に、プロセスルールが90nmで登場した、Pentium4(Prescott)でしたが発熱が半端でなく、インテルは高クロック路線から転換しました。
以下、は『後藤弘茂のWeekly海外ニュース』の2003年2月27日の記事ですがその時は、2010年にはCPUのクロックは15~20GHzになるとのintel社の方の見通しでした。
 
Prescott/Tejasは5GHz台、65nmのNehalemは10GHz以上に  
ちなみに、同時期(と言っても2003年7月4日)の記事でメモリのクロックを2010年では1.6GHzが最高としていますが、こちらはほぼその通りになっているところが面白いです。
 
高速化するDRAM、次々世代のDDR3は最高1.6GHzへ
2010-09-16 | コメント:0件

C++/STL 1GBのintのソートにかかる時間 2010

未踏の説明会の続きですが、説明会の中に技術的なセッションもありまして、グーグル株式会社のソフトウェアエンジニア 鵜飼さんの講演が面白かったのですが、その中で、『1GBのintのソートにかかる時間は、封筒の裏計算で、30秒』というのがありました。

パフォーマンスには一家言ある私ですが、さすがに1GBのintのソート時間にはピンと来ませんでした。
という訳で、ホントかどうかやってみました。


#include <vector>
#include <algorithm>
#include <iostream>
#include <time.h>

using namespace std;

int main(void)
{
    vector<int>        values;

    srand(time(0));
    // vectorに適当な値を入れる
    for ( int i = 0; i < 1024*1024*1024 / sizeof(int); i++ ) {
        values.push_back((int)(rand()*rand()-i));
    }

    // ソートする
    clock_t        t = clock();
    sort( values.begin(), values.end());
    cout << "Time(sort) is "
         << (double)(clock() - t) / CLOCKS_PER_SEC << "sec." << endl;

    return 0;
}


実行時間(Core i7-920 Windows7 コンパイルVC++2008 リリースモード 64ビットモード)は以下になります。上記のプログラムですが、32ビットモードでは動作しません。32ビットプロセスはリニアに1GBのメモリは確保できないです。

Time(sort) is 43.895sec.

なるほど、確かに30秒からそう離れていません。
ちなみに、この手の封筒の裏計算ですが、桁が違わなければOKと考えてよいでしょう。なので、細かい値の違いが問題になる場合は、実アプリでキチンとベンチマークをとるのがよいでしょう。
この手の結果の受け止め方ですが、おそらく一般の業務アプリを作成する人にとっては『理論的限界値』程度に思っていた方がよいでしょう。つまり

 1秒間に数百万個のint型のソートができる。数千万個になったら要注意。

と思っておけばよろしいかと思います。実際に私の経験でも行数が数百万件のソートをSQLで行うのはあまり問題になることはなかったです。(もちろんメモリが十分にあればの話ですが)。

実行時間の詳細ですが、説明では以下のとおりでした。
 ・要素を比較する回数(ソートのオーダnlogn)から、
  2^28 * log(2^28) → 2^28 * 28 → 2^28 * 2^5 → 2^33(2の33乗)回
 ・比較に際してのL1キャッシュのアクセス時間 0.5ns / 回
 ・比較に際してのブランチペナルティ 2.5ns / 回(2回に1回ペナルティがあると仮定する)

 実行時間 2^33 * (0.5 + 2.5)nsec = 25.76sec 約30秒

ただ、上記の計算ですが、ブランチペナルティが全体の速度を決定しているというのはいささか疑問があります。上記の場合、メモリのアクセス回数から計算した方が良いのでは?と思います。
つまり、
 ・要素を比較する回数(ソートのオーダnlogn)から、
  2^28 * log(2^28) → 2^28 * 28 → 2^28 * 2^5 → 2^33(2の33乗)回
 ・ 比較に際してのメモリアクセス回数 2回(リード&ライト) 2*4バイト
 ・キャッシュライン 32バイト

 ・メインメモリへのアクセス回数 2^33 * 2 * 4 / 32 = 2^31 回
 ・メインメモリアクセス性能 1回のアクセス 10nsec(DDR3のレイテンシーから)

 実行時間 2^31 * 10nsec = 21.47sec

うーん、数値的には似たり寄ったりであまり変わらないか・・・・
2010-07-09 | コメント:0件
Previous Page | Next Page