Previous Page | Next Page

C++/STL 正規表現の置換-boost::Regex_replace

1年ぶりのC++/SLTネタです。

正規表現での置換ですが、ちょっとハマったのでメモ書きします。以下、srcstrで与えられたstringに対して,fstr(正規表現)の検索を行い、repstrで置換します。マッチする文字列全てを置換します。結果の文字列はdststrで返します。


void replace_regex_all(
    string &srcstr, const char *fstr, const char *repstr, string &dststr)
{
    string tmp;
    boost::regex fnd(fstr);
    ostringstream t(ios::out | ios::binary);
    ostream_iterator<char, char> oi(t);
    boost::regex_replace(
        oi,
        srcstr.begin(),
        srcstr.end(),
       fnd,
       repstr,
       boost::match_default | boost::format_all);
    dststr = t.str();
}

2010-06-11 | コメント:0件

SQLの実行パフォーマンスについて 2010


@IT エンジニアライフのコメンテータ(だった)生島さんのコラムhttp://el.jibun.atmarkit.co.jp/g1sys/2010/05/post-2d1b.htmlのコメント欄に参加しました。

生島さんのコラムですが、過去に度々炎上してきましたが、炎上するたびに、

『SQLはオブジェクト指向言語の数十倍の効率』

という、この手の話が出てきます。この手の呪文は他にも幾つかあるのですが、これを出せば議論が終結するというある種の必殺技みたいに使われます。
が、それどころか、毎回、毎回、明確な結論が出ずにさらにコメント欄が荒れます。
私としては、本来はどうでもよい話なのですが、いきがかり上、私も思わず、

「私は、過去にSQLが遅いのでSQLを崩して、C言語でJOINをやらせて高速化しました。OO言語ではないですが、今だったらC++を使うでしょう(なぜってハッシュクラス があるから)。」
と発言しました。恐らく、多くの方は、

『いやいや、いくらなんでも、それはウソでしょう。』
とか、
『売り言葉に買い言葉でしょうが、それは良くないでしょう。』
とか、
『幾らC++が好きって言ったって、原理的にDBMS内で処理が閉じるSQLの方が速いでしょう。』
とか思われたことでしょう。

私も、そういうツッコミが来ることは重々承知していたのですが、現実に私は10年以上前になりますが、上記のような最適化を行ったことがありました。
以来、別にわざわざSQLを崩してCでJOINなんて事はしませんでしたが、逆にその後、さまざまなプロジェクトを通して、DBMSの動作をみる限り上記の最終手段は、まだ有効だなというのも実感としてあったのですが、あまりの共感の得られ無さにものすごい孤独感に襲われ、また生島さんの煽りも受け、このあたりで白黒はっきり付けたいと思います。


では、どのように白黒つけるのかですが、やはりベンチマークテストを行ってみるしかないかと思います。

つまり、生島さんが件のコラムのコメント欄に書かれた

TABLE_A a
INNER JOIN TABLE_B b
    a.KEY = b.KEY

をもとにしたSQLをC++で書き実際に実行させてその実行時間をみてみましょう。


■実験の環境

 <追記>この記事のコメント欄の指摘(サーバーとクライアントマシンが分かれていない)および環境が変わったので再現できない関係で、新しく環境を構築してテストをやり直しました。
 今回実験したテスト環境を示します。

■実験するSQLとプログラムの概要


以下のSQLを実行させ、カンマをセパレーターとして標準出力へ出力させます。
CSVファイルへの出力を想定したSQL、JOINの部分は生島さんがコメント欄で指摘したSQLそのものになっています。このSQLをもとにJOIN部分をC++でやらせてみます。
SELECT Price.CODE, RDATE, OPEN, CLOSE, NAME
FROM Price INNER JOIN Company ON (Price.CODE = Company.CODE)

■実験1 素直にSQL側でJOINをさせたものを実行


 以下のコードのとおり、実験するSQLをそのまま実行してみました。

#include <iostream>
#include <time.h>
#include "../kz_odbc.h"

using namespace std;

int main(void)
{
    kz_odbc db("DSN=Trade",true);
    kz_stmt stmt(&db);

    time_t  t = time(NULL);

    // テーブルからデータの取得
    stmt, "SELECT Price.CODE, RDATE, OPEN, CLOSE, NAME "
          " FROM Price INNER JOIN Company ON (Price.CODE=Company.CODE)"
          , endsql;
    kz_string_array result = stmt.next();
    int             cnt = 0;
    while ( !result.empty() ) {
        cout << result[0] << "," << result[1] << ","
             << result[2] << ","  << result[3] << ","
             << result[4] << "\n";
        result = stmt.next();
        cnt++;
    }

    cerr << "Execute time is " << time(0) - t << "sec." << endl;
    cerr << "Record count is " << cnt << "." << endl;
    return 0;
}

コードですが、Wordpressに合わせて編集してますので、変なところで改行が入っていますが御勘弁を。
 若干ですが、コードの説明を、
 stmt, "SELECT ・・・・
 とか
 result.empty()
 stmt.next()
の部分が私が作成したライブラリになります。といってもODBC APIを呼び出しているだけになります。そう特異なものでもないかと思います。
 実行結果ですが、
Execute time is 131sec.
Record count is 4671568.  
 となりました。プログラムは標準出力に出力していますが、実行に際しては標準出力をファイルにリダイレクトしています。その方が実行速度は速くなります。
 比較元のデータが無いので何とも言えませんが、1秒間に約3万5千件のデータがCSVファイルへ落とされているのでマシンスペックを考えますとまずまずでしょう。
ちなみに、実行ブランを確認しましたが、CompanyテーブルへのアクセスのTYPEはeq_refでユニークキーによるJOIN(最速のテーブルアクセス)が実行されていることを確認しました。

■実験2 C++側でネステッドループでJOINさせてみる

 ループループといっていたものですが、いわゆるネステッドスープのことだと推測します。

#include <iostream>
#include <time.h>
#include "../kz_odbc.h"

using namespace std;

int main(void)
{
    kz_odbc db("DSN=Trade",true);
    kz_stmt stmt(&db);
    time_t  t = time(NULL);

    // テーブルからデータの取得
    stmt, "SELECT CODE,RDATE,OPEN,CLOSE FROM Price", endsql;

    kz_string_array result = stmt.next();
    int                cnt = 0;
    while ( !result.empty() ) {
        // JOINの実行(ネステッドループ)
        kz_stmt    stmt2(&db);
        stmt2, "SELECT NAME FROM Company WHERE CODE = ? "
             ,  result[0].c_str(), endsql;
        kz_string_array result2 = stmt2.next();

        cout << result[0] << "," << result[1]  << ","
             << result[2] << ","  << result[3] << ","
             << result2[0] << "\n";
        result = stmt.next();
        cnt++;
    }

    cerr << "Execute time is " << time(0) - t << "sec." << endl;
    cerr << "Record count is " << cnt << "." << endl;
    return 0;
}

実行結果は以下のとおりです。
Execute time is 1714sec.
Record count is 4671568.

 これはものすごく遅いですね。生島さんが、
『SQLにすると数十倍速くなる』
といっていたのは、実験1のコードと実験2のコードを比べて言っていたと思われます。
では、これ以上に速くさせる方法はないのでしょうか?
生島さんの言うとおり、OO言語はSQLと比べて何十倍も遅いのでしょうか?

■実験3 C++側でハッシュJOINさせてみる

 件のコメント欄で生島さんが難しいとおっしゃっていた、ハッシュJOINですが、実は特段、難しいものではありません。
 以下のようにすっきりと実装できます。
 ちなみにコード中に出てきますmapというのはバイナリサーチを行います。なので、正確にはハッシュJOINではありません。
 C++でハッシュ検索を行うには、Boost等のライブラリを使う必要があります。
 つまり今回のコードはある意味、最適化の余地を残しているのですが、ここではテストの再現性(環境設定)の手間を考えてmapを使います。

#include <iostream>
#include <time.h>
#include "../kz_odbc.h"

using namespace std;

int main(void)
{
    kz_odbc db("DSN=Trade",true);
    kz_stmt stmt(&db);

    time_t  t = time(NULL);

    // マスターの取得・マップの作成
    map< string, string>    company;
    stmt, "SELECT CODE, NAME FROM Company  ",  endsql;
    kz_string_array result = stmt.next();
    while ( !result.empty() ) {
        company.insert( pair< string, string>( result[0], result[1]) );
        result = stmt.next();
    }

    // テーブルからデータの取得
    stmt, "SELECT CODE,RDATE,OPEN,CLOSE FROM Price ", endsql;
    result = stmt.next();
    int                cnt = 0;
    while ( !result.empty() ) {
        cout << result[0] << "," << result[1] << ","
             << result[2] << ","  << result[3] << ","
             << company[ result[0] ] << "\n";
        result = stmt.next();
        cnt++;
    }

    cerr << "Execute time is " << time(0) - t << "sec." << endl;
    cerr << "Record count is " << cnt << "." << endl;
    return 0;
}


 結果ですが、以下のとおり、実験1のコードよりも早くなっております。
Execute time is 108sec.
Record count is 4671568.


■結果

実行結果を再度以下に掲載します。
 
実験1(SQL) 131秒
実験2(C++側でネステッドループ) 1714秒
実験3(C++側でハッシュ) 108秒


 明確に結果が出ているかと思います。こんなに単純なテストの結果からでも
 「SQLをばらしてJOINをC++で行えば速くなる場合がある」
ということは理解していただけれるかと思います。
また、
『SQLはオブジェクト指向言語の数十倍の効率』
というのは、単純に
 「OO言語側の最適化が不十分である可能性がある」
ということも言えるでしょう。

ただ、実験3では、高々十数%しか速くなっていません。
ということであれば、通常はやはり実験1のようなコードの方がトータル(開発効率と実行効率を考えると)としては良いと思われる。実験3のような事実はあくまでも知識としてしておきたいところです。

追記、コメント欄の議論を踏まえて再度記事をアップしました。
追記、コメント欄の指摘(ローカルマシンで動かしている)を受けまして再度環境を作成して実験しました。
追記、まとめ記事を作成しました。

2010-05-31 | コメント:14件

C++/STLで数値の3桁区切り

久しぶりのC++/STLネタです。今回は数値の3桁区切りをやってみました。
ネットを探すとCのサンプルはあるのですが、少々トリッキーなコーディングのものが多いので、C++でのベタな実装というとこで作ってみました。


/**********************************************************************
 数値の3桁区切り
 試したコンパイル環境
    VC++ .NET 2003 / WINDOWS XP Professional 64 bit edition.
    GCC C++ 3.3.6 / glibc 2.3.4 / Vine Linux 4.2
**********************************************************************/

#include <algorithm>
#include <iostream>
#include <iomanip>
#include <vector>
#include <string>
#include <sstream>

std::string formatNumber(int num)
{
    std::vector<int>    sepnum;
    int                 number = abs(num);
    int                 sgn = num >= 0 ? 1 : -1;

    while ( number / 1000 ) {
        sepnum.push_back(number % 1000);
        number /= 1000;
    }

    std::stringstream  ss;
    ss << number * sgn;
    for ( std::vector<int>::reverse_iterator i = sepnum.rbegin();
                                        i < sepnum.rend(); i++ ) {
        ss << "," << std::setfill('0') << std::setw(3) << *i;
    }
    return std::string(ss.str());
}

using namespace std;
int main(int argc, char* argv[])
{
    cout << formatNumber(0) << endl;
    cout << formatNumber(1) << endl;
    cout << formatNumber(12) << endl;
    cout << formatNumber(123) << endl;
    cout << formatNumber(1234) << endl;
    cout << formatNumber(12345) << endl;
    cout << formatNumber(123456) << endl;
    cout << formatNumber(1234567) << endl;
    cout << formatNumber(12345678) << endl;
    cout << formatNumber(123456789) << endl;
    cout << formatNumber(1234567890) << endl;
    cout << formatNumber(-1) << endl;
    cout << formatNumber(-12) << endl;
    cout << formatNumber(-123) << endl;
    cout << formatNumber(-1234) << endl;
    cout << formatNumber(-12345) << endl;
    cout << formatNumber(-123456) << endl;
    cout << formatNumber(-1234567) << endl;
    cout << formatNumber(-12345678) << endl;
    cout << formatNumber(-123456789) << endl;
    cout << formatNumber(-1234567890) << endl;

}

2009-02-16 | コメント:0件

C++/STLでCSVファイルの読み込み

久しぶりのC++/STLネタということで、CSVファイルの読み込みです。
もともと、Apacheが出力するアクセスログの読み込みの為に作ったのですが、せっかくなので、Excelが出力するCSVにも対応しました。
CSVファイルの読み込みは他の言語ならsplit一発でお茶を濁すのですが、カンマ(,)やダブルクオート(")、改行があるデータにも対応してます。

/**********************************************************************
 CSVファイルの読み込みサンプル
 試したコンパイル環境
    VC++ .NET 2003 / WINDOWS XP Professional 64 bit edition.
    GCC C++ 3.3.6 / glibc 2.3.4 / Vine Linux 4.2
**********************************************************************/

#include <functional>
#include <algorithm>
#include <iostream>
#include <fstream>
#include <vector>
#include <string>

/**********************************************************************
 クオート文字の定義構造体
**********************************************************************/
struct quote{
    char    start;  // 開始文字
    char    end;    // 終了文字
    char    escape; // エスケープ文字
    quote( char start_, char end_, char escape_) :
         start(start_), end(end_), escape(escape_) {}
};

struct cmpStartQuote : public std::binary_function<quote,char,bool> {
    bool operator()(const quote &l_s1, const char &l_s2) const {
        return l_s1.start == l_s2;
    }
};

/**********************************************************************
 CSVの読取
**********************************************************************/
bool readCSV(
    std::vector< std::string > &result, // 結果格納ベクター
    std::istreambuf_iterator<char> &i,  // 入力文字イテレータ
    std::string &separator,             // 区切り文字
    std::vector<quote> &quotes)         // クオート文字ベクター
{
    bool retval = false;
    result.clear();
    std::string item;
    char    bc = '\0';
    bool    first = true;
    std::vector<quote>::iterator    ff = quotes.end();

    while ( i != std::istreambuf_iterator<char>() &&
                    (ff != quotes.end() || *i != '\n') ) {
        if ( ff == quotes.end() &&
                            strchr( separator.c_str(), *i) != 0 ) {
            result.push_back(item);
            item.clear();
            first = true;
            retval = true;
        } else {
            if ( first &&
                (ff = find_if( quotes.begin(),
                            quotes.end(),
                            bind2nd( cmpStartQuote(), *i)))
                    != quotes.end() ) {
                first = false;
            } else if ( ff != quotes.end() && ff->end == *i ) {
                if ( ff->end == ff->escape ) {
                    bc = *i++;
                    if ( i == std::istreambuf_iterator<char>() )
                        break;
                    if ( *i == ff->end ) {
                        item.push_back(*i);
                    } else {
                        ff = quotes.end();
                        continue;
                    }
                } else {
                    if ( bc == ff->escape ) {
                        item.push_back(*i);
                    } else {
                        ff = quotes.end();
                    }
                }
            } else {
                item.push_back(*i);
                first = false;
            }
        }
        bc = *i++;
    }
    result.push_back(item);
    if ( i != std::istreambuf_iterator<char>() ) {
        retval = true;
        i++;
    }
    return retval;
}

using namespace std;


int main(int argc, char* argv[])
{
    basic_ifstream<char>        inputFile("access_log");
    istreambuf_iterator<char>   sin(inputFile);
    vector< string >            rec;

    // APACHEのアクセスログ
    string                      separator(" ");
    vector< quote >             quotes;

    // クオート文字(")の設定("を表示したいときは\"になる)
    quotes.push_back( quote( '"', '"', '\\') );
    // 時間部分のクオート文字([]で括る)
    quotes.push_back( quote( '[', ']', 0 ) );

    while ( readCSV( rec, sin, separator, quotes) ) {
        vector< string >::iterator i;
        for ( i = rec.begin(); i < rec.end(); i++ ) {
            cout << i - rec.begin() << ":" << *i << endl;
        }
        cout << endl;
    }

    return 0;
}
 
上記コードは、Apacheのログを読み込むサンプルで、CSVファイルの読み込みの場合は、
separatorとquotesの部分が、
 
    string                      separator(",");
    vector< quote >             quotes;

    quotes.push_back( quote( '"', '"', '"') );
 
になります。

2008-08-04 | コメント:0件

コンテナの要素の参照は危険

C++/STLで、ついついやってしまうミスに、コンテナの要素への参照を保存するがある。
最近、このミスをやってしまったので覚書にしておく。
要するに、コンテナに値を追加・削除するとイテレーターが無効になる、と同じことなのだが、参照だとついついやってしまいます。

ちなみに、このバグはコンテナの領域が再配置されない限り発生しないので、reserveメソッドを使用して適当な領域を確保するとデバッグが困難になるので、マジで注意が必要です。

以下、バグの再現コードです。


#include <iostream>
#include <vector>

using namespace std;

int main(void)
{
    vector<int> array1;
    array1.reserve(10);
    for ( int i = 0; i < array1.capacity(); i++ ) {
        array1.push_back(i);
    }

    int &last_value = array1.back();    // コンテナの要素の参照を得る
    cout << "last_value: " << last_value << endl;

    array1.push_back(10);               // ここでlast_valueは無効になる
    cout << "last_value: " << last_value << endl;

    return 0;
}

実行結果
VC++ .NET 2003 / WINDOWS XP Professional 64 bit edition(32ビット環境で作成・実行)


last_value: 9
last_value: -572662307
2008-05-21 | コメント:0件
Previous Page | Next Page