象と戯れ

 | 

2009-09-11

Executorリファクタリング案

16:35 | Executorリファクタリング案 - 象と戯れ を含むブックマーク はてなブックマーク - Executorリファクタリング案 - 象と戯れ

JIT on SQLには予想外にも多くの反応を頂き、ありがとうございます。多くの人が気になる話題なのかなと再考したり、int4の比較演算子を逆アセンブルしてどうやったらJITできるかなどを模索したりしていました。この辺の話題はまた今度。

で、現行のパフォーマンスの壁を破るためにこんなことをやっていました。

***************
*** 32,41 ****
--- 32,68 ----
  static void InitScanRelation(SeqScanState *node, EState *estate);
  static TupleTableSlot *SeqNext(SeqScanState *node);
  
+ HeapTuple *myTable;
+ uint32 myRownum;
+ Oid myTableId = InvalidOid;
+ 
  /* ----------------------------------------------------------------
   *						Scan Support
   * ----------------------------------------------------------------
   */
+ 
+ static TupleTableSlot *
+ mySeqNext(SeqScanState *node)
+ {
+ 	HeapTuple	tuple;
+ 	TupleTableSlot *slot;
+ 	uint32		processed;
+ 
+ 	slot = node->ss_ScanTupleSlot;
+ 	processed = node->ps.state->es_processed++;
+ 	if (processed < myRownum)
+ 	{
+ 		tuple = myTable[processed];
+ 		ExecStoreTuple(tuple, slot, InvalidBuffer, false);
+ 	}
+ 	else
+ 	{
+ 		ExecClearTuple(slot);
+ 	}
+ 
+ 	return slot;
+ }
+ 
  /* ----------------------------------------------------------------
   *		SeqNext
   *
***************
*** 125,130 ****
--- 152,161 ----
  TupleTableSlot *
  ExecSeqScan(SeqScanState *node)
  {
+ 	if (myTableId != InvalidOid && myTableId == RelationGetRelid(node->ss_currentRelation))
+ 	{
+ 		return ExecScan((ScanState *) node, (ExecScanAccessMtd) mySeqNext);
+ 	}
  	/*
  	 * use SeqNext as access method
  	 */

myTableには下記のような関数を通してあらかじめヒープメモリ上にテーブルをHeapTupleの配列として丸ごと展開しておきます。

Datum
mytable_alloc(PG_FUNCTION_ARGS)
{
	Oid	relid = PG_GETARG_OID(0);
	Relation	rel;
	HeapScanDesc	scandesc;
	int		tablesize = 0;
	int		rownum = 0;		
	HeapTuple	*table;
	
	rel = heap_open(relid, NoLock);
	scandesc = heap_beginscan(rel, SnapshotNow, 0, NULL);

	for (;;)
	{
		HeapTuple	tuple;

		tuple = heap_getnext(scandesc, true);
		if (!tuple)
		{
			break;
		}
		tablesize += HEAPTUPLESIZE + tuple->t_len;
		rownum++;
	}

	heap_endscan(scandesc);
	heap_close(rel, NoLock);

	table = (HeapTuple *) malloc(sizeof(HeapTuple) * rownum);
	memset(table, 0, sizeof(HeapTuple) * rownum);
elog(INFO, "rownum = %d, tablesize = %d", rownum, tablesize);

	rel = heap_open(relid, NoLock);
	scandesc = heap_beginscan(rel, SnapshotNow, 0, NULL);
	rownum = -1;

	for (;;)
	{
		HeapTuple	tuple;

		tuple = heap_getnext(scandesc, true);
		if (!tuple)
		{
			break;
		}

		rownum++;
		table[rownum] = myheap_copytuple(tuple);
	}

	heap_endscan(scandesc);
	heap_close(rel, NoLock);

	myTable = table;
	myRownum = rownum;
	myTableId = relid;
	PG_RETURN_INT32(0);
}

myTableにテーブルが保存済みであれば、SeqScanノードはRelationからではなくmyTableからデータを読み出すようにするわけです。こうすることにより、heap_getnext()以下の面倒なページ内データフェッチをすっ飛ばして効率的にアクセスできるかなと思ったわけです。物理メモリに余裕があって、更新処理がそれほど頻繁ではないような(更新が単一プロセスによってある程度コントロールできる)ときは、有効な戦略になり得ます。

結果はぼちぼち、、、VirtualBoxの貧弱な512MB搭載CentOS上で250万レコード240MB程度のテーブルを作って実験しているのですが、通常の順スキャンに比べて2倍ぐらいのパフォーマンスが出ています。最も、単一プロセスでpsqlで手動でテストしているだけなので正確な数値ではない点はあしからず。

加えてヒープメモリを共有メモリに変えて、更新の際にはLWLockをしたりして・・・という処理を経て同時実行したときのパフォーマンスはまた別の話になると思います。

ここで言いたいのは、Executorのフックが難しすぎること。もちろん、id:pgsqlさん他の尽力によりExecutorのトップはフックできるようになっています。

void
ExecutorRun(QueryDesc *queryDesc,
			ScanDirection direction, long count)
{
	if (ExecutorRun_hook)
		(*ExecutorRun_hook) (queryDesc, direction, count);
	else
		standard_ExecutorRun(queryDesc, direction, count);
}

が、これはExecutorの手前で何か処理をしたいときがユースケースであって、今回のように「Seqscan且つターゲットのテーブルが特定領域に保存済みならそっちを参照」みたいなカスタムノードを作ろうと思ったときには、ソースに手を入れるしかない。PostgreSQLのせっかくの拡張性もExecutorの前では形無しです。

思うに、Node構造体をもっとInterfaceな表現にしないといけないんですね。現在のところNodeはTagのみを持つシンプルな構造体ですが、ExecutorNodeは

struct ExecutableNode{
  Node base; /* abtract node */
  PlanState* (*ExecInit)(Plan *, EState *, int);
  TupleTableSlot* (*ExecProc)(ExecutableNode *);
  void (*ExecEndNode)(ExecutableNode *);
};

みたいなことにして、各フェーズの処理を全てノードのメンバ関数として呼び出すようにしておく。こうすれば、ExecutorStart_hookあたりでプランツリーを舐めて該当SeqscanノードのExecProcをカスタムに入れ替えたり、という動的拡張が可能になりそうです。

ちなみに現状はどうなっているかというと、

int
ExecCountSlotsNode(Plan *node)
{
	if (node == NULL)
		return 0;

	switch (nodeTag(node))
	{
			/*
			 * control nodes
			 */
		case T_Result:
			return ExecCountSlotsResult((Result *) node);

		case T_Append:
			return ExecCountSlotsAppend((Append *) node);

		case T_RecursiveUnion:
			return ExecCountSlotsRecursiveUnion((RecursiveUnion *) node);

		case T_BitmapAnd:
			return ExecCountSlotsBitmapAnd((BitmapAnd *) node);

		case T_BitmapOr:
			return ExecCountSlotsBitmapOr((BitmapOr *) node);
...(後略)

ということになっていて、switchである必要もなかろうというわけです。

今回の例はかなりニッチな要件ですが、考えようによってはnodeAggを分散処理したりそれこそSQL/MEDな実装を考える際もスムーズに入れると思うんですよね。ただ、変更が派手過ぎてアレルギー反応がでそう。

こういうことを考えていくと、C++とかJavaって偉大だな、と思ったりします(普段は思わないけど)。そしてMySQLのストレージエンジンとかも勉強した方が良さそうですね。

 |