UTF-8: バイト列→文字列変換
前々回に作成したURLデコード用の関数では、sb-ext:octets-to-string関数が処理のボトルネックとなっていた。
確かsbcl(1.0.28)はバイト列から文字列への変換には、UTF-8でもShift-JISでもEUC-JP(及びその他)でも出来るような汎用的な方法(枠組み?)*1を採用していたはずだが、(sbclでは)文字は内部的にはユニコード値として表現されているので、それを利用すれば(UTF-8に限れば)もっと効率的に変換できるはずだと思う。
今回はそれを試してみた。
以下がUTF-8バイト列をユニコード文字列に変換する関数。
入力のバイト列はsimple-arrayだと云うことが前提で、若干エラーチェックが不足している。
※ この関数はsbcl用に作られたものだが、文字の表現としてユニコードを採用している処理系なら一応動作するはず
;;;; 型定義および宣言 (deftype octet () '(unsigned-byte 8)) (deftype simple-octets () '(simple-array octet)) (declaim (optimize (speed 3) (debug 0) (safety 0) (compilation-speed 0)) (inline bit-off? bit-val utf8-octets-to-string)) ;;;; 補助関数定義 ;; 下からpos番目のbitが0ならtrue (defun bit-off? (pos digit) (not (ldb-test (byte 1 pos) digit))) ;; 下からlength番目までの値を取り出し、必要ならシフトさせる (defun bit-val (length digit &optional (shift 0)) (ash (ldb (byte length 0) digit) shift)) ;;;; 変換関数 ;; posから始まるUTF8バイト列(octets)を、ユニコード一文字に変換してstringに追加する ;; ※ バイト列はsimple-octetsである必要がある (defun utf8-to-ucs(octets pos string &aux (os octets)) (declare (simple-octets os) (fixnum pos)) (let* ((octet (aref os pos)) (code (cond ((bit-off? 7 octet) ; #b0xxxxxxx octet) ((bit-off? 6 octet) ; #b10xxxxxx (error "Illegal UTF-8 character starting at byte position ~D." pos)) ((bit-off? 5 octet) ; #b110xxxxx #b10xxxxxx (+ (bit-val 5 octet 6) (bit-val 6 (aref os (incf pos))))) ; XXX: 後続の値が適切かどうかのチェックは行っていない(以下同) ((bit-off? 4 octet) ; #b1110xxxx #b10xxxxxx #b10xxxxxx (+ (bit-val 4 octet 12) (bit-val 6 (aref os (incf pos)) 6) (bit-val 6 (aref os (incf pos))))) ((bit-off? 3 octet) ; #b11110xxx #b10xxxxxx #b10xxxxxx #b10xxxxxx (+ (bit-val 3 octet 18) (bit-val 6 (aref os (incf pos)) 12) (bit-val 6 (aref os (incf pos)) 6) (bit-val 6 (aref os (incf pos))))) ;; CHAR-CODE-LIMITの値が21bit以下であることが前提のコード (t (error "CHAR-CODE-LIMIT(~D) exceeded (position ~D)." char-code-limit pos))))) (vector-push (code-char code) string) (the fixnum (1+ pos)))) ; 次の文字が始まる位置を返す ;; UTF8バイト列をユニコード文字列に変換する ;; フィルポインタ付きのstringを返す (defun utf8-octets-to-fill-pointer-string (octets &aux (len (length octets))) (declare (simple-octets octets)) (let ((as (make-array len :element-type 'character :fill-pointer 0))) ; バッファを多めに確保しておく (do ((i 0 (utf8-to-ucs octets i as))) ((>= i len) as) (declare (fixnum i))))) ;; utf8-octets-to-fill-pointer-stringの戻り値をsimple-stringに変換する (defun utf8-octets-to-string (octets) (coerce (utf8-octets-to-fill-pointer-string octets) 'simple-string))
sb-ext:octets-to-string関数との比較結果。
予想通り、今回作成した関数の方が速かった(おおよそ二倍*2 )。
;;;;;;;;;;;;;;; ;;;; データ用意 ;; ファイル読込み関数 (defun read-file (path) (sb-ext:octets-to-string (with-open-file (in path :element-type '(unsigned-byte 8)) (let ((as (make-array (file-length in) :element-type '(unsigned-byte 8)))) (read-sequence as in) as)))) ;; 夏目漱石の『心』のUTF-8バイト列 (defvar *kokoro* (sb-ext:string-to-octets (read-file "/path/to/kokoro") :external-format :utf8)) ;;;;;;;;; ;;;; 計時 ;; sb-ext:octets-to-string関数 > (time (dotimes (i 10) (sb-ext:octets-to-string *kokoro* :external-format :utf8))) Evaluation took: 0.181 seconds of real time 0.164010 seconds of total run time (0.160010 user, 0.004000 system) 90.61% CPU 572,873,551 processor cycles 28,440,864 bytes consed ;; utf8-octets-to-string関数 > (time (dotimes (i 10) (utf8-octets-to-string *kokoro*))) Evaluation took: 0.095 seconds of real time 0.092007 seconds of total run time (0.084006 user, 0.008001 system) 96.84% CPU 303,277,449 processor cycles 29,676,960 bytes consed ;; ついでにutf8-octets-to-fill-pointer-string関数も > (time (dotimes (i 10) (utf8-octets-to-fill-pointer-string *kokoro*))) Evaluation took: 0.059 seconds of real time ; (coerce ... 'simple-string)が不要な分高速 0.056003 seconds of total run time (0.052003 user, 0.004000 system) [ Run times consist of 0.004 seconds GC time, and 0.053 seconds non-GC time. ] 94.92% CPU 187,361,375 processor cycles 22,189,040 bytes consed