PostgreSQL:EUC-SJIS変換処理の改善について

euc_jp_and_sjis.patch (840 bytes)
 encodingがEUCのデータベースに対して,WindowsなどのSJISクライアントから
多量のデータを検索したとき,文字コードの変換処理がボトルネックになることが
あります。
そこで,EUC->SJIS変換の効率を改善するパッチを作成してみました。
このパッチが新たなバグを作り出していないか?,もっと速くできないか?など,
ご意見お待ちします。

テスト方法:
(1)以下のようなSQLファイル(test.sql)を用意する。
set client_encoding to 'SJIS';
select * from accounts;
(accountsは,pg_bench -iで作成されるテーブルです)

(2)SQLを実行する。
$ psql -f test.sql -o /dev/null

(3)サーバプロセスのprofile結果
Each sample counts as 0.01 seconds.
  %   cumulative   self              self     total
 time   seconds   seconds    calls   s/call   s/call  name
 14.17      0.17     0.17  9088966     0.00     0.00  pg_mule_mblen
 10.00      0.29     0.12   400008     0.00     0.00  euc_jp2mic
  8.33      0.39     0.10  9088966     0.00     0.00  pg_mic_mblen
  5.83      0.46     0.07  1301673     0.00     0.00  AllocSetAlloc
  5.83      0.53     0.07   400009     0.00     0.00
perform_default_encoding_conversion
  5.83      0.60     0.07   400008     0.00     0.00  mic2sjis
  5.00      0.66     0.06  1300705     0.00     0.00  AllocSetFree

 pg_mule_mblen, euc_jp2mic, pg_mic_mblen, mic2sjis,
perform_default_encoding_conversionはEUC->SJIS変換で実行される関数です。
さらに,AllocSetAlloc, AllocSetFreeの実行回数のうち約2/3は,変換用の
一時的なバッファ確保のために使われています。
つまり今回のテストケースでは,実行時間の大半が文字コード変換に使われて
いることになります。

 特にpg_mule_mblenやpg_mic_mblenは,call回数が非常に多くなっています。
このパッチでは,これらの関数の実行回数を減らすようにしました。
パッチの内容は以下のとおりです。

(1)mic2sjisのループ内部で,pg_mic_mblen()を実行しないようにした
 オリジナルのコードでは,1文字ごとにpg_mic_mblenを実行して残りの文字の
長さを計算しています。さらに,pg_mic_mblenはpg_mule_mblenを実行します。
このため,pg_mic_mblen/pg_mule_mblenの実行回数が多くなっています。

    while (len >= 0 && (c1 = *mic))
    {
        len -= pg_mic_mblen(mic++);

 これを以下のようにすることで,ループ内部でpg_mic_mblenを実行する必要が
なくなります。

    unsigned char *mic_end = mic + len;

    while (mic <= mic_end && (c1 = *mic))
    {
        mic++;

(2)mic2sjisでSJISに変換できない文字が現れたとき,micポインタの指す位置が
正しくならないときがあるのではないか?

        else if (c1 > 0x7f)
        {
            /* cannot convert to SJIS! */
            *p++ = PGSJISALTCODE >> 8;
            *p++ = PGSJISALTCODE & 0xff;

 ここでは現在の文字のバイト数を考慮していないため,micが次の文字を指して
いることを保証できないと思います。
そこで,micの位置を調節するために以下のコードを追加しました。

            /*
             * Adjust a mic pointer. Because can't guarantee mic points
             * next char here.
             */
            mic--;
            mic += pg_mic_mblen(mic);


効果の測定(1): profile結果
Each sample counts as 0.01 seconds.
  %   cumulative   self              self     total
 time   seconds   seconds    calls  ms/call  ms/call  name
 11.46      0.11     0.11   400008     0.00     0.00  euc_jp2mic
 10.42      0.21     0.10  1301673     0.00     0.00  AllocSetAlloc
  6.25      0.27     0.06  1300705     0.00     0.00  AllocSetFree
  6.25      0.33     0.06   400000     0.00     0.00  FunctionCall3
  6.25      0.39     0.06   100000     0.00     0.00  slot_deform_tuple
  5.21      0.44     0.05   400008     0.00     0.00  euc_jp_to_sjis
  4.17      0.48     0.04   900051     0.00     0.00  appendBinaryStringInfo

 長い実行時間を占めていたpg_mule_mblenやpg_mic_mblenがいなくなり,
ボトルネックがeuc_jp2micへ移動したことが分かります。


効果の測定(2): 実行時間の測定
profileオプションを外して(-O2のみにする),postgresをリビルドしてから
以下のコマンドで実行時間を測定。

$ time psql -f test.sql -o /dev/null

-- patch適用前
real    0m2.964s
user    0m0.850s
sys     0m0.110s

-- patch適用後
real    0m2.639s
user    0m0.820s
sys     0m0.060s

実行時間(real)が約10%程,短縮できています。

メモ1:postgresのprofileの取り方 (linuxの場合)

(1)CFLAGSに -pg -DLINUX_PROFILEを設定する
 例えば以下のようにする
 CFLAGS="-O2 -pg -DLINUX_PROFILE"; export CFLAGS
 デバッグ情報も欲しいときは以下のようにする
 CFLAGS="-g -pg -DLINUX_PROFILE"; export CFLAGS

(2)postgresをビルドしてインストールする

(3)psqlでテストしたいSQLを実行して,psqlを終了する

(4)サーバプロセスのprofile結果(gmon.out)はデータファイルのディレクトリに
作成される(例:$PGDATA/base/17237/gmon.out)

(5)gprofでprofile結果を参照する
 gprof ${POSTGRES_HOME}/bin/postgres ${PGDATA}/base/17237/gmon.out | less

メモ2:文字コード変換のshared libraryをprofileする

 文字コード変換の処理は,src/backend/utils/mb/conversion_procs以下にあるが,
shared libraryになっているので,うまくprofileできなかった。

 以下の手順でstatic linkにして,profileできるか?

(1)backend/utils/mb/MakefileのOBJSに以下を追加(これでstatic linkするはず)
 conversion_procs/euc_jp_and_sjis/euc_jp_and_sjis.o

(2)include/catalog/pg_proc.hに以下を追加
 (*)OID(635,636)は,同ディレクトリにあるunused_oidsで探す

DATA(insert OID = 635 ( int4                   PGNSP PGUID 12 f f t f v 5  2278
"23 23 2275 2275 23" _null_ euc_jp_to_sjis - _null_ ));
DESCR("convert euc_jp to sjis");
DATA(insert OID = 636 ( int4                   PGNSP PGUID 12 f f t f v 5  2278
"23 23 2275 2275 23" _null_ sjis_to_euc_jp - _null_ ));
DESCR("convert sjis to euc_jp");

(3)postgresをリビルドする

(4)backend/utils/mb/conversion_procs/conversion_create.sqlを編集
 euc_jp_to_sjis, sjis_to_euc_jpのcreate functionをコメントアウトする

CREATE OR REPLACE FUNCTION euc_jp_to_sjis ... とかなっている行を,
-- CREATE OR REPLACE FUNCTION euc_jp_to_sjis ... にする。
sjis_to_euc_jpも同様。

(5)インストール
 make install

(6)データベースを作り直す(catalogが変わったため)

 これでprofile取れた。

メモ3:さらなる高速化を考える

(1)euc_jp_to_sjisのstrlenを省略できないか?
 euc_jp_to_sjisからmic2sjisを実行するときにmic文字列の長さを
 strlenで計算しているが、これは直前のeuc_jp2micの状態を使うことで
 strlenを実行せずに文字列の長さを取得できる

(2)palloc/pfreeも省略できないか?
 文字列が短いときはローカル変数のバッファを使うことで、euc_jp_to_sjisの
palloc/pfreeを省略できると思う。

(3)SJIS->EUCも同様の手法が使える