概要
string を byte スライスにしたり,またその逆だったりをおこなうのに,
vec := []byte("hello")
なんてコードをよく書きます.
特にメソッドの引数が byte スライスでしか用意されてない場合などは,
input := "some string..."
MyFind([]byte(input))
などと書いたりします.
string は読み込み専用のスライスみたいな物だという認識だったので,キャストしても,ポインタがコピーされるだけで,必要になったらコピーされるだろうぐらいに思ってたんですが,調べてみたらメモリがまるっとコピーされるので,パフォーマンスに影響しそうな箇所ではこの方法は避けるべきです.(その辺のお話はこちら)
[]byte と string を別個に扱おうと思うと,たとえば,
- func MyFindString(input string)
- func MyFindByteSlice(input []byte)
みたいな専用関数をそれぞれの方ごとに用意しなければいけなくなってしまいます.たぶんメソッドの中身も似たようなものになるわけですし,保守の観点からは専用メソッドを作らずに済ませたいところです.
string と []byte を透過的に扱うひとつの方法
まず,[]byte と string を透過的に扱うには,これらの値が変更されないということが前提となります.つまり読み込みしかしない場合です.
byte スライス関連のパッケージと言えば,bytes パッケージ.ここに,bytes.Buffer という型があります.この型の new には下記の2つがあります.
- func NewBuffer(buf []byte) *Buffer
- func NewBufferString(s string) *Buffer
お,string を引数に構築できる!と思いますが,ところがどっこい,こいつは内部で string を []byte にキャストしているので,値がまるっとコピーされてしまいます.まぁ,よく考えれば,Buffer はそもそも値を書き換えたりするものなので,値コピーして利用するしかないですよね.
func NewBufferString(s string) *Buffer {
return &Buffer{buf: []byte(s)}
}
bytes.Reader, strings.Reader を使う
次に目をつけたのは,bytes と strings のそれぞれに存在する Reader 型です.それぞれの new をみてみると,
func NewReader(b []byte) *Reader { return &Reader{b, 0, -1} }
func NewReader(s string) *Reader { return &Reader{s, 0, -1} }
どちらもコピーせずに保持するだけです!この型を利用すれば専用関数とか作らなくて済む...と思いきや,このまま単純に利用してしまうと,
- MyFindString(r strings.Reader)
- MyFindByteSlice(r bytes.Reader)
となって何も抽象化されません.
インターフェースを利用して抽象化
bytes と strings で提供される Reader 型のレシーバの一覧.
メソッド | 実装しているインターフェース | 備考 |
---|---|---|
Len() int | ||
Read(b []byte) (n int, err error) | io.Reader | |
ReadAt(b []byte, off int64) (n int, err error) | io.ReadAt | |
ReadByte() (b byte, err error) | io.ByteReader | |
(r *Reader) ReadRune() (ch rune, size int, err error) | io.RuneReader | |
Seek(offset int64, whence int) (int64, error) | io.Seeker | |
UnreadByte() error | io.ByteScanner | ByteScannerはByteReader+UnreadByte() |
UnreadRune() error | io.RuneScanner | RuneScannerはRuneReader+UnreadRune() |
WriteTo(w io.Writer) (n int64, err error) | io.WriteTo |
これで,bytes と strings で Reader にさえ変換してしまえば,適当なインターフェースを使って抽象化できます.
byte 単位で読めればいいなら,io.Reader でも使っておけばいいですかね.
ところで,[]byte と string 使うときの典型的なコードってこんな感じじゃありません?
s := "hello world"
for i := 0; i < len(s); i++ {
...
}
やっぱ,長さがわかってた方がいろいろやりやすいですよね.でも,Len()
を含んでいるような interface は見あたらないです.
そんなときは,新しいインターフェースを作りましょう.
type MyReader interface {
ReadByte() (byte, error)
Len() int
}
これで,
- MyFind(r MyReader)
とメソッドがまとめられました.
パフォーマンス
Double Array Trie の実装を,引数を Reader にしたときと,string にしたときでパフォーマンスを計測してみました.
CommonPrefixSearch で比較してます.
BenchmarkCommonPrefixSearchReader 1000000 1339 ns/op
BenchmarkCommonPrefixSearchString 1000000 1033 ns/op
インターフェースにすると,レシーバを呼び出すようになるので,直接値にアクセスできる string を利用した方が2割ほど速いですね.とはいえ,処理がものすごくパフォーマンスに影響するのでなければ,インターフェースを利用してなるべくコードを抽象化してしまいたいところ.とりあえず Reader インターフェースで作ってみて,pprof みながら検討というところでしょうか.
Reader は本当に欲しかったものだろうか?
Reader が目的の用途にぴったり合うときもあるとは思うんですが,前から順番に読んでくだけとはかぎらないですよね.byte スライスでも string でも,s[i]
のようにアドレスでアクセスしたい.本当に欲しかったのは,byte スライスと string にプリミティブに提供されている操作を抽象化した interface だったんじゃないでしょうか.
デフォルトの操作 | メソッドに抽象化したイメージ |
---|---|
s[i] | s.At(i) |
s[i:j] | s.Range(i, j) |
len(s) | s.Len() |
package ro
type String string
type ByteSlice []byte
func (s String) At(i int) byte {
return s[i]
}
func (s String) Len() int {
return len(s)
}
func (b String) Range(s, e int) Array {
return b[s:e]
}
func (b ByteSlice) At(i int) byte {
return b[i]
}
func (b ByteSlice) Len() int {
return len(b)
}
func (b ByteSlice) Range(i, j int) Array {
return b[s:e]
}
type Array interface {
At(i int) byte
Len() int
Range(s, e int) Array
}
ベンチマークを取ってみると,Reader 使ったときとほぼ変わらないですね.最初っからこれでよかったんだ orz.
BenchmarkCommonPrefixSearchArray 1000000 1397 ns/op
BenchmarkCommonPrefixSearchString 1000000 1097 ns/op
Type Switch ?
ここまでくると,[]byte でも string でも受け付けられるように, interface{}
で引き回わして,type switch したらいいんじゃないかと思えてきます.
ベンチマークとってみると,
BenchmarkCommonPrefixSearchTypeSwitch 1000000 2068 ns/op
BenchmarkCommonPrefixSearchString 2000000 1101 ns/op
なんと,2倍も遅くなってしまいました.これはダメ.
まとめ
読み込みしかしない []byte と string は, bytes, strings パッケージで提供される Reader を利用してインターフェースを構築すると透過的に扱うことが出来ます.最初から提供されているパッケージなので,Reader としての利用で間に合う場合は,これを使っちゃいましょう.添え字でのアクセスとか必要な場合は,Read Only の型を別に用意して,インターフェースを作るのが良さそうです.
どちらの場合も,若干パフォーマンスは落ちるので,ものすごくクリティカルな処理の時は気をつける.まずは Reader で作ってみて,pprof みながら検討するのがいいと思われます.
もっといい方法があったら教えてください〜