2012-09-25

percent-encoding

Qiitaに投稿したパーセントエンコーディングについてのTipsにも書いたんだけど、自分の知ってるライブラリはどうも帯に短し襷に長しといった感じで、

  • application/x-www-form-urlencodedも扱える(似ているものなので、同じライブラリで扱いたい)
  • ASCIIに収録されていない文字を任意のエンコーディングでオクテット列に変換し、パーセントエンコーディングできる(今なら大体UTF-8で済みそうだけど、できれば他のエンコーディングも扱いたい)

という条件のものは見つからず、色々試行錯誤していた。

そんな折に@snmstsさんが教えてくださったのがdo-urlencodeで、普通のパーセントエンコーディングもapplication/x-www-form-urlencodedも両方扱えるし、UTF-8決め打ちとはいえ自分はUTF-8くらいしか使わないだろうから良いか、ということで使ってみた。

それで、使ってみて気付いたんだけど、どうもメモリの消費がちょっと多い。そしてちょっと遅い。

(let ((str (map 'string (lambda (_)
                          (declare (ignore _))
                          (code-char (random 255)))
                (make-string (* 1024 1024)))))
  (null (time (puri::encode-escaped-encoding str puri::*reserved-characters* t)))
  (null (time (urlencode:urlencode str))))

;; CCL 1.8
;; (PURI::ENCODE-ESCAPED-ENCODING STR PURI::*RESERVED-CHARACTERS* T)
;; took 82,384 microseconds (0.082384 seconds) to run.
;; 13,839 microseconds (0.013839 seconds, 16.80%) of which was spent in GC.
;; During that period, and with 2 available CPU cores,
;; 64,990 microseconds (0.064990 seconds) were spent in user mode
;; 13,998 microseconds (0.013998 seconds) were spent in system mode
;; 18,647,856 bytes of memory allocated.
;; 40 minor page faults, 0 major page faults, 0 swaps.
;; (URLENCODE STR)
;; took 7,272,771 microseconds (7.272771 seconds) to run.
;; 1,337,318 microseconds (1.337318 seconds, 18.39%) of which was spent in GC.
;; During that period, and with 2 available CPU cores,
;; 7,079,924 microseconds (7.079924 seconds) were spent in user mode
;; 171,974 microseconds (0.171974 seconds) were spent in system mode
;; 1,178,938,384 bytes of memory allocated.
;; 264 minor page faults, 0 major page faults, 0 swaps.

;; SBCL 1.0.58
;; Evaluation took:
;; 0.053 seconds of real time
;; 0.052993 seconds of total run time (0.046993 user, 0.006000 system)
;; 100.00% CPU
;; 124,622,974 processor cycles
;; 18,647,216 bytes consed
;;
;; Evaluation took:
;; 3.412 seconds of real time
;; 3.395483 seconds of total run time (3.268503 user, 0.126980 system)
;; [ Run times consist of 0.404 seconds GC time, and 2.992 seconds non-GC time. ]
;; 99.50% CPU
;; 7,962,815,308 processor cycles
;; 770,256,208 bytes consed

これはpuriのエンコーダと比較した結果なんだけど、4MB分の文字列を扱うのにCCLで1GB超、SBCLで700MB超のメモリを使っている。ワーオ。

コードを読んだところ、どうもエスケープ一回ごとに文字列とベクタのアロケーションが起きるようになっていた。ここを何とかすればメモリ消費量や処理速度が改善できるんじゃないだろうかうへへ、ということで改造することに。

で、試行錯誤の結果、大幅にメモリ消費量と速度は改善した。したんだけど、どうも中身が完全に別物になってしまった。総書き換えといった感じで、これを作者に取り込むように主張しても、「お前これ同じなの関数のインターフェイスだけやん」と言われる未来が容易に想像できたので、仕方なく別のライブラリに仕立てることにした。「自分で書いてしまったりしがち」とかQiitaのTipsに書いておいてこれだから割と救いようがない。

そうと決めたらもう開き直って、UTF-8以外のエンコーディングのサポートとか、puriで見かけて良いと思った、どの文字をエスケープするか指定できる機能とかも付けてみた。その結果がこれGitHubのミラーもある。

普通に使う分には難しいことはなく、

(percent:encode str)

とすればエンコードできるし、

(percent:decode str)

とすればデコードできる。エンコードやデコードする文字を指定する場合はこんな感じ。

;; 特定の文字だけエンコードしたり
(percent:encode "/usr/bin/["
                :test (lambda (x)
                        (or (percent:unreservedp x)
                            (= x #x2f))))    ; '/'
;=> "/usr/bin/%5B"

;; 特定の文字だけデコードできる
(percent:decode (percent:encode "a:b/c" :test (constantly nil))
                :test #'percent:alphap)
;=> "a%3Ab%2Fc"

こんなことできて何が嬉しいの? という風に思うかもしれないけど、URIはそれぞれのコンポーネントで許される文字が違うので、そういうのに対応するときに便利。あとはする必要がない文字までエンコードしてるURIの正規化とか、規格に沿わない実装に対応するのにも使える。詳しい使い方はREADMEやユニットテストを参照のこと。

あんまり需要はないと思うけど、自分で使うために書いたので問題ない。将来的にもっと良さげなライブラリを見つけたらそっちを使うかも。というか、誰か書いてください。

0 件のコメント: