Skip to content

Commit

Permalink
Speed up unmarshaling.
Browse files Browse the repository at this point in the history
Use bufio.Reader internally to avoid allocating extra slices.

Deprecates the bencode.Reader type.

The public API that previously used bencode.Reader,

    Unmarshal(r Reader, val interface{}) (err error)

 is now

    Unmarshal(r io.Reader, val interface{}) (err error)

Which is compatible, since any bencode.Reader is also an io.Reader.

BenchmarkJackpalBencodeMarshal-8     	  200000	      5525 ns/op	    2128 B/op	      56 allocs/op
BenchmarkJackpalBencodeUnmarshal-8   	  500000	      3417 ns/op	    2593 B/op	      72 allocs/op
  • Loading branch information
jackpal committed Aug 12, 2018
1 parent f8d9db2 commit b5110c4
Show file tree
Hide file tree
Showing 2 changed files with 65 additions and 27 deletions.
88 changes: 63 additions & 25 deletions parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,39 +53,46 @@ type builder interface {
Flush()
}

// Deprecated: This type is currently unused. It is exposed for backwards
// compatability. The public API that previously used this type,
//
// Unmarshal(r Reader, val interface{}) (err error)
//
// is now
//
// Unmarshal(r io.Reader, val interface{}) (err error)
//
// Which is compatible, since any Reader is also an io.Reader.
// Clients should drop their use of this type. It may be removed in the future.
type Reader interface {
io.Reader
io.ByteScanner
}

func collectInt(r Reader, delim byte) (buf []byte, err error) {
for {
var c byte
c, err = r.ReadByte()
if err != nil {
return
}
if c == delim {
return
}
if !(c == '-' || c == '.' || c == '+' || c == 'E' || (c >= '0' && c <= '9')) {
err = errors.New("Unexpected character in Integer")
return
}
buf = append(buf, c)
func decodeInt64(r *bufio.Reader, delim byte) (data int64, err error) {
buf, err := readSlice(r, delim)
if err != nil {
return
}
data, err = strconv.ParseInt(string(buf), 10, 64)
return
}

func decodeInt64(r Reader, delim byte) (data int64, err error) {
buf, err := collectInt(r, delim)
if err != nil {
// Read bytes up until delim, return slice without delimiter byte.
func readSlice(r *bufio.Reader, delim byte) (data []byte, err error) {
if data, err = r.ReadSlice(delim); err != nil {
return
}
data, err = strconv.ParseInt(string(buf), 10, 64)
lenData := len(data)
if lenData > 0 {
data = data[:lenData-1]
} else {
panic("bad r.ReadSlice() length")
}
return
}

func decodeString(r Reader) (data string, err error) {
func decodeString(r *bufio.Reader) (data string, err error) {
length, err := decodeInt64(r, ':')
if err != nil {
return
Expand All @@ -94,16 +101,47 @@ func decodeString(r Reader) (data string, err error) {
err = errors.New("Bad string length")
return
}

// Can we peek that much data out of r?
if peekBuf, peekErr := r.Peek(int(length)); peekErr == nil {
data = string(peekBuf)
_, err = r.Discard(int(length))
return
}

var buf = make([]byte, length)
_, err = io.ReadFull(r, buf)
_, err = readFull(r, buf)
if err != nil {
return
}
data = string(buf)
return
}

func parseFromReader(r Reader, build builder) (err error) {
// Like io.ReadFull, but takes a bufio.Reader.
func readFull(r *bufio.Reader, buf []byte) (n int, err error) {
return readAtLeast(r, buf, len(buf))
}

// Like io.ReadAtLeast, but takes a bufio.Reader.
func readAtLeast(r *bufio.Reader, buf []byte, min int) (n int, err error) {
if len(buf) < min {
return 0, io.ErrShortBuffer
}
for n < min && err == nil {
var nn int
nn, err = r.Read(buf[n:])
n += nn
}
if n >= min {
err = nil
} else if n > 0 && err == io.EOF {
err = io.ErrUnexpectedEOF
}
return
}

func parseFromReader(r *bufio.Reader, build builder) (err error) {
c, err := r.ReadByte()
if err != nil {
goto exit
Expand Down Expand Up @@ -152,7 +190,7 @@ func parseFromReader(r Reader, build builder) (err error) {

case c == 'i':
var buf []byte
buf, err = collectInt(r, 'e')
buf, err = readSlice(r, 'e')
if err != nil {
goto exit
}
Expand Down Expand Up @@ -205,9 +243,9 @@ exit:
// Parse parses the bencode stream and makes calls to
// the builder to construct a parsed representation.
func parse(reader io.Reader, builder builder) (err error) {
// Check to see if the reader already fulfills the bencode.Reader interface.
// Check to see if the reader already fulfills the bufio.Reader interface.
// Wrap it in a bufio.Reader if it doesn't.
r, ok := reader.(Reader)
r, ok := reader.(*bufio.Reader)
if !ok {
r = newBufioReader(reader)
defer bufioReaderPool.Put(r)
Expand Down
4 changes: 2 additions & 2 deletions struct.go
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ func (b *structBuilder) Key(k string) builder {
// To unmarshal a top-level bencode array, pass in a pointer to an empty
// slice of the correct type.
//
func Unmarshal(r Reader, val interface{}) (err error) {
func Unmarshal(r io.Reader, val interface{}) (err error) {
// If e represents a value, the answer won't get back to the
// caller. Make sure it's a pointer.
if reflect.TypeOf(val).Kind() != reflect.Ptr {
Expand All @@ -292,7 +292,7 @@ func Unmarshal(r Reader, val interface{}) (err error) {
return
}

func unmarshalValue(r Reader, v reflect.Value) (err error) {
func unmarshalValue(r io.Reader, v reflect.Value) (err error) {
var b *structBuilder

// XXX: Decide if the extra codnitions are needed. Affect map?
Expand Down

0 comments on commit b5110c4

Please sign in to comment.