view paper.tex @ 10:2c85b017727f

spell check
author Shinji KONO <kono@ie.u-ryukyu.ac.jp>
date Fri, 27 Aug 2010 14:58:43 +0900
parents 22ce0f379878
children
line wrap: on
line source

% Sample file for the use of compsoft style file. 
%
\documentclass{compsoft}
% \documentclass[L]{compsoft}
% \documentclass[S]{compsoft}
% \documentclass[S, L]{compsoft}
% \documentclass[K]{compsoft}
% \documentclass[K, L]{compsoft}
% \documentclass[U]{compsoft}
% \documentclass[U, L]{compsoft}

% Preamble
%
% 「コンピュータソフトウェア」誌に掲載される論文の場合,次で
% 巻数,号数,開始ページ,終了ページを指定する.
\volNoPp{16}{5}{78}{83}

% ワークショップによる推薦論文の場合,ワークショップ名を指定する.
% \suisen{ワークショップ名}

% 特集の場合,特集のタイトルを与える.
% \tokushu{特集のタイトル}

% 大会論文の場合,\taikai で開催年を指定する.ここで指定した年から
% 大会の回数は計算される.
% \taikai{2009}

% ここに,使用するパッケージを列挙する.
\usepackage[dvips]{graphics}

% ユーザが定義したマクロなどはここに置く.ただし学会誌のスタイルの
% 再定義は原則として避けること.

\begin{document}

% 論文のタイトル
\title{動的なコード生成を用いた正規表現評価器の実装}

% 著者
% 和文論文の場合,姓と名の間には半角スペースを入れ,
% 複数の著者の間は全角スペースで区切る
%
\author{新屋 良磨, \space\space 河野 真治
%
% ここにタイトル英訳 (英文の場合は和訳) を書く.
%
\ejtitle{Implementation of Regular Expression Engine with Dynamic Code Generation.}
%
% ここに著者英文表記 (英文の場合は和文表記) および
% 所属 (和文および英文) を書く.
% 複数著者の所属はまとめてよい.
%
\shozoku{Shinya RYOMA, Shinji KONO}{琉球大学工学部情報工学学科}%
{Dept. \ of Information Engineering, Ryukyu University}
%
% 出典情報は \shutten とすれば出力される.
\shutten
%
% 受付年月日,記事カテゴリなどは自動的に生成される.
\uketsuke{2010}{8}{25}
%
% その他,脚注に入れるものがあれば,\note に記述する.
%\note{脚注に入れる内容}
}

%
% 和文アブストラクト
\Jabstract{%
当研究室では, Continuation based C (CbC)という, 状態遷移記述に適した C の下位
言語を提案している. CbC は ステートメントより大きく, 関数よりも小さな
プログラミング単位, コードセグメントの継続に接続を基本要素としている. 
本研究では, 与えられた正規表現を等価な有限状態オートマトンに変換し, 
それを Continuation based Cによる継続, バイトコード、Cに変換する
正規表現コンパイラ を Python で実装し、速度やプログラミングのしやすさなどの比較を行なった. 
特にマルチバイト文字の正規表現検査の高速化は実用的にも重要であり、
現状で使われているgrepの評価の行なった。
% なお, ここで言うコンパイルは, 内部形式/中間表現への変換だけでなく, 実行時バイナリの生成までを指す. 
}
%
\maketitle

\section{はじめに}
コンパイラ理論の発展と共に, コンパイルにかかる時間はより短く, また得られ
るプログラムはアセンブラレベルで最適化が施され, より高速になってきている.

完全に静的なコンパイルが可能な対象として, 正規表現評価器に着目した.
現在,正規表現の評価器は, プログラミング言語の組み込み機能やライブラリ等,
さまざまな実装が存在するが, それらの殆どは仮想マシン方式を採用している\cite{R2}.
仮想マシンを採用いた実装でも, 正規表現を内部表現に変換する処理を行ってお
り, それらを ``コンパイル'' と呼ぶことが多い.本研究で実装した評価器の
``コンパイル''とは, 正規表現を内部形式に変換することではなく, 正規
表現 から実行バイナリーを生成することを指す(\ref{subsection:compile}節).
本研究では, 実行バイナリーの生成にはコンパイラ基盤であるLLVM, GCC を用い
ており,評価器全体の実装としてはPythonで実装した.

本論文では, まず正規表現のコンパイル方法について説明し, 実装した検査器の
性能調査のために, 正規表現を用いてテキストマッチ処理を行う grep と同等の
機能を実装し, GNU grep との比較を行う. 

\section{正規表現}
\subsection{正規表現によるテキストマッチ}
正規表現は与えられた文字列を受理するかどうかを判定できるパターンマッチン
グの機構であり, sed, grep, awk を始めとするテキスト処理ツールに広く利用
されている. 正規表現には定められた文法によって記述され, 例えば, 正規表現
``$a*b$''は''$a$''の0回以上の繰り返し直後, ``$b$'' で終わる文字列(``$b$'' , ``$ab$'', 
``$aaaab$'')を受理し, ``$a(b|c)$'' は ``$a$''で始まり, 直後が ``$b$'' または
``$c$''で終わる文字列(``$ab$'', ``$ac$'') を受理する. 

\subsection{正規表現の演算}\label{subsection:regex}

本論文では, 以下に定義された演算をサポートする表現を正規表現として扱う. 
\begin{enumerate}
\item {連接} 二つの言語$L$と$M$の連接($LM$)は, $L$に属する列を一つとり, そのあとに$M$に族する列を連
接することによってできる列全体から成る集合である. 
\item {集合和} 二つの言語$L$と$M$集合和($L|M$)は, $L$または$M$(もしくはその両方)に属する列全体からなる
集合である. 
\item {閉包} 言語$L$の閉包($L*$)とは, $L$の中から有限個の列を重複を許して取り出し, 
      それらを連接してできる列全体の集合である. 
\end{enumerate}

正規表現は, この3つの演算について閉じておリ, この3つの演算によって定義され
る表現は, 数学的には正則表現と定義されている. 
本論文では, 特に区別のない限り, 正則表現と正規表現を同じものとして扱う. 

\subsection{grep}
正規表現は, テキストのパターンをシンプルに記述できるという利点から, テキ
ストファイルから, 任意のパターンにマッチするテキストを検索するなどの用途
に使用される. 

GNU grep は, それを実現するソフトウェアの一つであり, 引数として与えられ
たファイルから, 与えられた正規表現にマッチするテキストを含む行を出力する
機能を持っている. 

``与えられた正規表現にマッチするテキストを含む''というのは, 行の先頭から
末尾まで正規表現によるマッチングを行い, 正規表現が受理状態になった時点で
``含む'' という解釈を行う. つまり, 正規表現 ''$(a|s)t$'' は, ''$at$''または''
$st$``を受理する正規表現であり, テキスト行''$math. $``の2~3文字目の''$at$''に一致す
るので grep は ``$math. $'' を出力する. また正規表現''$a*$''は, ``$a$''の0回以上の繰
り返しを受理する正規表現であり, 空文字も受理するので, grep は全ての行を
出力することになる. 

\subsection{マルチバイト文字を含む正規表現}

   なんか書いて

\section{正規表現検査器の実装}
正規表現は等価なNFAに, またNFAは等価なDFAに変換することが可能である\cite{U}. 以
下にその変換手方を説明する. 

\begin{figure}[b]
\begin{center}
\scalebox{0. 50}{\includegraphics{fig1. eps}}
\end{center}
\caption{``$A$''と``$B$''の連接}
\label{figure:concat}
\end{figure}
\begin{figure}[b]
\begin{center}
\scalebox{0. 50}{\includegraphics{fig2. eps}}
\end{center}
\caption{``$A$''と``$B$''の集合和}
\label{figure:union}
\end{figure}
\begin{figure}[b]
\begin{center}
\scalebox{0. 50}{\includegraphics{fig3. eps}}
\end{center}
\caption{``$A$''の閉包}
\label{figure:star}
\end{figure}

\subsection{正規表現からNFAへの変換}
NFA({\it Non-deterministic Finite Automaton}) は, 入力に対して複数の
遷移先持つ状態の集合であり, 遷移先が非決定的(Non-deterministic)である. 
ここでは, NFAを5個組$(Q, \Sigma, , \delta, q_0, F)$で定義する. ただし, 
\begin{enumerate}
\item $Q$は状態の有限集合. 
\item $\Sigma$は入力記号の有限集合. 
\item $q_0$は$Q$の要素で, 開始状態と呼ぶ. 
\item $F$は$Q$の部分集合で, 受理状態と呼ぶ. 
\item $\delta$は, 状態と入力記号に対して状態の集合を返す遷移関
      数. ($\varepsilon$遷移を許す)
\end{enumerate}
正規表現が, 等価なNFAに変換できるということを, \ref{subsection:regex}で定義
した3つの演算について対応するNFAに変換できることから示す. 
\begin{enumerate}
\item {連接} 図\ref{figure:concat}は正規表現 ``AB'' に対応するNFA. 
\item {集合和} 図\ref{figure:union}は正規表現 ``$A|B$''に対応するNFA. 
\item {閉包} 図\ref{figure:star}は正規表現 ``A*''に対応するNFA. 
\end{enumerate}

図\ref{figure:union}, \ref{figure:star}において, ラベルのない矢印は無条件
の遷移を現しており, $\varepsilon$遷移と呼ばれる. また, 二重丸で囲まれた
状態は受理状態を現しておリ, NFAにおいて入力が終了した時点で, 受理状態を保
持している場合に限り, その文字列を受理したことになる. なお, NFAは同時に
複数の遷移先をもつことがあるので, テキストのマッチング途中で複数の状態を
保持することがある. 

現在実装されている正規表現検査器の多くは, 正規表現を内部的にNFAに変換し
て評価を行っている\cite{R1}. NFAによる実装は, 後述する後方参照や最長一致
に対応しやすいが, 同時に遷移する可能性のある複数の状態を保持する必要があ
るため, 正規表現の複雑度に多じてマッチングの時間が多くなってしまう場合が
ある. 文献\cite{R1}では, ``$a?a?a?aaa$'' のような ``$a?^na^n$'' のように
表現(``$a?$''は''a``か空文字''``を認識する拡張された正規表現お一つ)の評
価において, NFAベースの正規表現検査器では遷移する状態の数が増えてしまう
でマッチングにかかる処理時間が$n$の指数的に増加する問題をベンチマーク
結果と共に指摘している. 文献\cite{K} では正規表現からNFAベースで効率的な
マッチング処理を行う検査器をIBM 7094 機械語で生成する例が紹介されている. 

\subsection{NFAからDFAへの変換}
非決定的な遷移を行うNFAから, 決定的な遷移を行うDFA({\it Deterministic
Finite Automaton})に変換する手法を説明する. なお, 遷移が決定的である
ということは, 1つの入力に対して, 遷移する状態がただ1つであるということを
指す. 
DFAは, NFAと同様な5個組で$(Q, \Sigma, , \delta, q_0, F)$定義できる. ただ
し, DFAにおいて$\delta$において$\varepsilon$遷移は認められず, また任意
の状態$q$と入力$\sigma$について, $\delta(q, \sigma) = q'$となる$q'$は$Q$
の要素となる. つまり, 遷移先が決定的であるということに他ならない. 

以下に$\varepsilon$遷移を許す$\varepsilon$-NFA $E = (Q_E, 
\Sigma, \delta_E, q_0, F_E)$ から等価なDFA $D = (Q_D, \Sigma, 
\delta_D, q_D, F_D)$を構成する手順を示す. 

\begin{enumerate}
\item $Q_D$は$Q_E$の部分集合全から成る集合であり, おの中で$D$において
      到達可能な状態は, $\varepsilon$遷移に関して閉じている$Q_E$の部分
      集合$S$に限られる. ここで, 状態$q$において$\varepsilon$遷移に関し
      て閉じている集合全体を$ECLOSE(q)$と表す. $ECLOSE$を使って$S$を定義
      すると, $S = \displaystyle\bigcup_{q\in{S}}ECLOSE(q)$を満たす$S$. 
\item $q_D = ECLOSE(q_0)$. すなわち, $E$の開始状態の$\varepsilon$閉包. 
\item $F_D$は$E$の状態の集合で, 受理状態を少なくとも一つ含むもの全体から
      なる集合である. すなわち, $F_D = \{S | S \in Q_D \wedge S \cap F_E \ne
      \emptyset\}$
\item $\delta_D(S, a)$は$Q_D$の要素$S$と$\Sigma$の要素$a$に対して次のよ
      うに計算される. 
      \begin{enumerate}
       \item $S = \{p_1, p_2, . . . , p_k\}$とする. 
       \item $\displaystyle\bigcup^{k}_{i=1}\delta(p_i, a)$を求め, その結
             果を$\{r_1, r_2, . . . , r_m\}$とする. 
       \item このとき, $\delta_D(S, a) = \displaystyle\bigcup^{m}_{j=1}ECLOSE(r_j)$
      \end{enumerate}
\end{enumerate}
この方法によって得られたDFA $D$はNFA $E$と同等の言語を認識し, またNFAの
元となる正規表現と同等である. 

\subsection{DFAから実行バイナリの生成}\label{subsection:compile}
DFAからの実行バイナリ生成には, 3種類の実装を行った.

\begin{enumerate}
\item DFA $\rightarrow$ Continuation based C $\rightarrow$ gccによるコンパ
      イル
\item DFA $\rightarrow$ C $\rightarrow$ gccによるコンパイル
\item DFA $\rightarrow$ LLVM中間表現 $\rightarrow$ LLVMによるコンパイル
\end{enumerate}
%

以下, Continuation based C, LLVMの説明と, それを利用したDFAからの
実行バイナリ生成の方法を説明する.

\subsubsection{Continuation based C}
Continuation based C(以下CbC)は, . . . 
本研究室での先行研究によりCbCコンパイラは, GNU C Compiler上で実装されて
いる\cite{Y}, 本研究ではgcc-4. 5上に実装されたCbCコンパイラを用いた. 

以下に, 正規表現 ``$(A|B)*C$''に対応するDFAと, DFAの各状態に対応するCbC
のコードセグメントの生成例を示す. 

\newpage
\begin{figure}[t]
\begin{center}
\scalebox{0. 60}{\includegraphics{fig5. eps}}
\caption{正規表現``$(A|B)*C$''に対応するDFA}
\label{figure:dfasmaple}
\end{center}
\end{figure}

\begin{center}
\begin{small}
\begin{verbatim}
__code state_0(unsigned char *s, unsigned
        char* cur, unsigned char* buf, FILE
        *f, char* filename) {
  switch(*s++) {
    case 65: /* match A */
      goto state_0(s, cur, buf, f, filename);
    case 66: /* match B */
      goto state_0(s, cur, buf, f, filename);
    case 67: /* match C */
      goto state_1(s, cur, buf, f, filename);
    default: goto reject(s, cur, buf, f, filename);
  }
}
__code state_1(unsigned char *s, unsigned
        char* cur, unsigned char* buf, FILE
         *f, char* filename) {
  goto accept(s, cur, buf, f, filename);
}
\end{verbatim}
\end{small}
{\bf 図\ref{figure:dfasmaple}のDFAに対応するCbCコードセグメント}
\end{center}
DFAの遷移とは直接関係のない引数(ファイル名やバッファへのポインタ等) が目
立が, CbCでは環境をコードセグメント間で引数として明示的に持ち運ぶ軽量継
続を基盤としたプログラミングスタイルが望ましい. 今回コンパイラによって生
成したCbCソースコードでは,大域変数は持たず,必要な変数は全て引数に載せて
いる.
CbCのstate\_1, state\_0から呼ばれているaccept, rejectはそれぞれ受理と非
受理を表す. accept ではテキスト行を出力して次の行へ, rejectでは次
の文字へと処理を移すコードセグメントへ継続を行う.

生成したCbCソースコードを, GCC上に実装したCbCコンパイラによってコンパイルす
ることで実行バイナリを得る. 

\subsubsection{C}
Cによる実装では, CbCのコードセグメントに代わり関数を用いてDFAを実装した.
DFAによる遷移を関数呼び出しで行っているため,実行時のスタックの使用領域や,ス
タック操作によるオーバーヘッドが予想される.

\subsubsection{LLVM}
LLVM(Low Level Virtual Machine) は, さまざまなコード最適化/分析機能を備
えた, モジュール単位で利用可能なコンパイラ基盤である\cite{L}.

CbC/Cによる実装では,DFAからCbC/Cのソースコードに変換し,GCCによってコンパ
イルを行っている. LLVMによる実装では, LLVMの中間表現であるLLVM-IRを,提供
されているAPIを用いて直接操作を行い, コンパイルを経て実行バイナリを得る.
PythonからLLVM APIの利用は, LLVM APIのPythonラッパーであるllvm-pyを使用した.

\begin{figure}[t]
\begin{center}
\scalebox{0.60}{\includegraphics{fig7.eps}}
\caption{LLVMを用いた実装}
\label{figure:llvm-impl}
\end{center}
\end{figure}

LLVMによる実装でも, Cによる実装と同様に,DFAの状態遷移をswitch文と関数呼
び出しによって表現している.

\newpage

\section{評価}
\subsection{CbC/C/LLVM - 三つの実装の比較}
本実験で実装した正規表現評価器のCbC/C/LLVMによる三つの実装に対して,
コンパイル時間及びマッチングにかかる時間の比較を行った.

{\bf コンパイル時間}
n個の単語を正規表現のOR演算で繋げたパターンに対し, 各実装のコンパイル時
間の比較を行った.

{\bf マッチング時間}
...
本実験で実装した正規表現評価器を用いて grep に相当するプログラムを実装し,
実際にテキストファイルからのパターンマッチを行い, それぞれの評価を行った.

 \\
 \\
 \\
 \\
 \\
 \\
 \\
 \\
 \\
 \\
 \\
 \\
 \\
 \\
 \\

\subsection{grepとの比較}
...
実験環境及びテキストファイルとして
\begin{itemize}
\item CPU : Core i7 950 @3.0GHz
\item Memory : 16GB
\item Text : Wikipedia 日本語版全記事\\ (XML, UTF-8, 4.7GB, 8000万行)
\end{itemize}
を用いた. 表\ref{table:benchmark2}に結果を示す.

\begin{table}[h]
\caption{ベンチマーク:grep}
\label{table:benchmark2}
\begin{tabular}[t]{|l||l|l|l|}
\hline
テストケース & fixed-string & simple-regex & complex-regex \\
\hline
本実装 & 13.90[s]  & 15.45 & 14.83[s] \\
コンパイル時間 & 0.15[s] & 0.17[s] & 0.23[s]\\
\hline
GNU grep 2.6.3 & 2.93[s] & 5.65[s] & 16.86[s] \\
\hline
GNU grep 2.5.4 & 2.96[s] & 6.37[s] & 188.51[s] \\
\hline
\end{tabular}
\end{table}

以下に, それぞれのテストケースのパターン, grepにマッチした行数,
および考察を記す. なお,ここで扱う正規表現の ``複雑さ''とは, DFAに
変換した時点の状態数, 遷移規則の多さを基準としている.

\begin{description}
\item[fixed-string]固定文字列によるマッチング\\
パターン  : ``$Wikipedia$''\\
マッチ行数: 348936行\\
GNU grep では, 与えられたパターン内に, 確実に含まれる文字列(固定文字列)
が存在する場合は, Boyer-Moore法 等の高速な文字列探索アルゴリズムを用いて
フィルタをかけることで, DFAによるマッチングを減らし,高速化している. 本実
装の grep では, そのようなフィルタリングは施しておらず, どのようなパター
ンにたいしても先に述べたDFAベースのマッチングを行う. fixed-stringのテス
トケースではその差が出ている.

\item[simple-regex]単純な正規表現でのマッチング\\
パターン  : ``\verb|^\*+ \[\[|''\\
マッチ行数: 3439028行\\
このパターンは行頭から'*'の1回以上の繰り返しの後に文字列 ``\verb| \[\]|''
が続く行にマッチする(Wikipediaにおける ``関連項目''). このパターンにも, 確実に含まれる文字列が存在するの
で, GNU grep ではBoyer-Moore法でフィルタリングを行った後マッチングを行う
ことで本実装と比べて高速な結果となっている.

\item[complex-regex]複雑な正規表現でのマッチング\\
パターン  :``$(Python|Perl|Pascall|Prolog|\\
PHP|Ruby|Haskell|Lisp|Scheme)$''\\
マッチ行数: 1503行\\
このパターンは,9個の単語をORで並べたもので,確実に含まれる文字列は存在せ
ず,先の二つに比べてGNU grep は遅い結果となっている.

GNU grep 2.5.4 は188秒と, 本実装及びGNU grep 2.6.3に対して非常に遅い結
果となっているが, これは後述する mbrtowc(3) によるマルチバイト文字の変換
処理によるオーバーヘッドによるものである.
\end{description}

3つのテストケースの結果を見てみると, 本実装はそれぞれ実行時間にあまり差
がなく, またコンパイル時間はマッチングにかかる時間と比べて無視できるなっ
ている. また, complex-regexのテストケースではGNU grepよりも高速な結果が
出ており, 本実装は(フィルタリングを用いない)純粋なDFAマッチングにおいて
パターンに比べて検索対テキストファイルが十分に大きな場合の検索にコンパイ
ルの効果が発揮されていることが分かる.

\subsection{特徴}
本実験によって実装された正規表現評価器の特徴を, GNU grep との比較をはさ
みながら説明する.

{\bf 正規表現からの動的なコード生成}\\
本実装による一番の特徴として, 正規表現から変換を行うことで得られる等価な
DFAを, C/CbC/LLVM に変換しコンパイルすることで正規表現に応じた実行バイナ
リを生成することが挙げられる. またコンパイラ理論におけるさまざまな最適化
が期待される.
grepによる検索のような, 与えられるパターン(正規表現)に対してマッチ対象の
テキストファイルが十分に大きい場合, 正規表現のコンパイルにかかる時間はマッ
チングにかかる時間によって隠される.

GNU grep では, 本実装と同様にDFAベースのマッチングを行うが, DFAの各状態
は構造体よって表され, 状態遷移は各状態毎に保持している遷移先ポインタによ
る配列を,1Byte単位でテーブルルックアップを行うことで実装されている.

{\bf UTF-8対応}\\
本実装は, マルチバイト文字の代表的な符号化方式であるUTF-8に対応しており,
正規表現の演算は1Byte単位ではなく,1文字単位で適用される.
マルチバイト文字を含む正規表現のサンプルとして, ``(あ$|$い)*う''をDFAに
変換した図\ref{figure:multibyte-dfasample}を載せる. 図における
\verb|'\x'|に始まる文字は16進数表記で, \verb|'\x82'|は16進数82を表す.

GNU grep 2.5.x では, マルチバイト文字に対応しているものの, プログラム内
部でlibc mbrtowc(3)を用いて固定サイズであるワイド文字に変換して処理を行っ
ており, テストケース complex-regex ではそのオーバーヘッドが顕著に現れて
いる.
2010年3月にリリースされた GNU grep 2.6 から, UTF-8に対して本実装と同様に
内部的に対応することで, mbrtowc(3)による変換コストが無くなっている.
\begin{figure*}[H]
\begin{center}
\scalebox{0.80}{\includegraphics{fig6.eps}}
\caption{正規表現``(あ$|$い)*う''に対応するDFA}
\label{figure:multibyte-dfamaple}
\end{center}
\end{figure*}

{\bf 柔軟な実装}\\
本実験で実装した正規表現評価器は, Pythonによって実装されており,全体
で3000行程度の軽量なプログラムとなっている. 比較対象のGNU grep は,
C言語によって実装されており, プログラム全体は10000行を超える.

さらに,本実装から動的に生成されるコードも,コードセグメント/関数とswitch
を基準としたシンプルな記述で高い可読性を持ちつつ, 細かい最適化をGCC/LLVMの最適
化技術を利用することで実行速度も非常に高速なものとなっていることが実験結
果から分かる.

\section{まとめと今後の課題}
本実験では, 正規表現をコードセグメント/関数による状態遷移を行うコードに
変換し, GNU grep との比較を行い良好な結果を得た.

\bibitem{U} Hopcroft, J, E. Motowani, R. Ullman, J : オートマトン言
        語理論計算論I (第二版), pp.~39--90.

\bibitem{L} Lattner, Chris. Adve, Vikram : The LLVM Compiler Framework
        and Infrastructure, 2004

\bibitem{K} Thompson, K : Regular Expression Search Algorithm, 1968

\bibitem{H} 長谷川 勇 : 国際正規表現ライブラリの開発

\bibitem{Y} 与儀 健人 : 組込み向け言語Continuation based CのGCC上の実装,
        2010

\end{thebibliography}
\end{adjustvboxheight} % needed only when Appendix follows

\end{document}