LoginSignup
54
54

More than 5 years have passed since last update.

メモ:golang で []byte と string の読み込みを透過的に扱う試行錯誤

Last updated at Posted at 2014-12-18

概要

string を byte スライスにしたり,またその逆だったりをおこなうのに,

string2byteslice
vec := []byte("hello")

なんてコードをよく書きます.

特にメソッドの引数が byte スライスでしか用意されてない場合などは,

cast2byteslice
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 はそもそも値を書き換えたりするものなので,値コピーして利用するしかないですよね.

NewBufferString
func NewBufferString(s string) *Buffer {
    return &Buffer{buf: []byte(s)}
}

bytes.Reader, strings.Reader を使う

次に目をつけたのは,bytes と strings のそれぞれに存在する Reader 型です.それぞれの new をみてみると,

bytes.NewReader
func NewReader(b []byte) *Reader { return &Reader{b, 0, -1} }
strings.NewReader
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()
read_only
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 みながら検討するのがいいと思われます.

もっといい方法があったら教えてください〜 :octocat:

54
54
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
54
54