Commit 1aa677c8 authored by Kirill Smelkov's avatar Kirill Smelkov

xfmt: Addons to fmt and strconv with focus on formatting text without allocations

Std fmt works ok unless you need to do text formatting in hot codepaths.
There fmt becomes inappropriate as it is slow and does allocations on
every formatting.

strconv also does not have append routines for every needed case, e.g.
there is no strconv.AppendRune, no strconv.AppendHex etc.

So xfmt

1. provides append routines for builtin types lacking in strconv

2. introduces xfmt.Stringer interface which types can implement to hook
   into general formatting via xfmt.Append()

3. provides xfmt.Buffer which is []byte with syntatic sugar for
   formatting in a way similar to printf: For example if in fmt speak
   you have

	s := fmt.Sprintf("hello %q %d %x", "world", 1, []byte("data"))

   xfmt analog would be

	buf := xfmt.Buffer{}
	buf .S("hello ") .Q("world") .C(' ') .D(1) .C(' ') .Xb([]byte("data"))
	s := buf.Bytes()

   and xfmt.Buffer can be reused several times via Buffer.Reset() .

The above xfmt.Buffer usage is more uglier than fmt.Printf but much less uglier
than direct strconv.Append* and friends calls, and works faster and without
allocations compared to fmt.Printf:

	BenchmarkXFmt/%c(0x41)-4                20000000                65.4 ns/op             1 B/op          1 allocs/op
	BenchmarkXFmt/.Cb(0x41)-4               200000000                5.96 ns/op            0 B/op          0 allocs/op
	BenchmarkXFmt/%c(-1)-4                  20000000                70.1 ns/op             3 B/op          1 allocs/op
	BenchmarkXFmt/.C(-1)-4                  100000000               12.9 ns/op             0 B/op          0 allocs/op
	BenchmarkXFmt/%c(66)-4                  20000000                65.8 ns/op             1 B/op          1 allocs/op
	BenchmarkXFmt/.C(66)-4                  100000000               12.7 ns/op             0 B/op          0 allocs/op
	BenchmarkXFmt/%c(1080)-4                20000000                67.2 ns/op             2 B/op          1 allocs/op
	BenchmarkXFmt/.C(1080)-4                100000000               12.8 ns/op             0 B/op          0 allocs/op
	BenchmarkXFmt/%c(8364)-4                20000000                69.4 ns/op             3 B/op          1 allocs/op
	BenchmarkXFmt/.C(8364)-4                100000000               13.8 ns/op             0 B/op          0 allocs/op
	BenchmarkXFmt/%c(65537)-4               20000000                70.5 ns/op             4 B/op          1 allocs/op
	BenchmarkXFmt/.C(65537)-4               100000000               14.3 ns/op             0 B/op          0 allocs/op
	BenchmarkXFmt/%s("hello")-4             20000000                72.3 ns/op             5 B/op          1 allocs/op
	BenchmarkXFmt/.S("hello")-4             200000000                9.40 ns/op            0 B/op          0 allocs/op
	...
parent 64936209
// Copyright (C) 2017 Nexedi SA and Contributors.
// Kirill Smelkov <kirr@nexedi.com>
//
// This program is free software: you can Use, Study, Modify and Redistribute
// it under the terms of the GNU General Public License version 3, or (at your
// option) any later version, as published by the Free Software Foundation.
//
// You can also Link and Combine this program with other software covered by
// the terms of any of the Open Source Initiative approved licenses and Convey
// the resulting work. Corresponding source of such a combination shall include
// the source code for all other software used.
//
// This program is distributed WITHOUT ANY WARRANTY; without even the implied
// warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
//
// See COPYING file for full licensing terms.
// Package xfmt provides addons to std fmt and strconv packages with focus on
// formatting text without allocations.
//
// For example if in fmt speak you have
//
// s := fmt.Sprintf("hello %q %d %x", "world", 1, []byte("data"))
//
// xfmt analog would be
//
// buf := xfmt.Buffer{}
// buf .S("hello ") .Q("world") .C(' ') .D(1) .C(' ') .Xb([]byte("data"))
// s := buf.Bytes()
//
// xfmt.Buffer can be reused several times via Buffer.Reset() .
package xfmt
import (
"encoding/hex"
"strconv"
"unicode/utf8"
"lab.nexedi.com/kirr/go123/mem"
"lab.nexedi.com/kirr/go123/xbytes"
)
const (
hexdigits = "0123456789abcdef"
)
// Stringer is interface for natively formatting a value representation via xfmt
type Stringer interface {
// XFmtString method is used to append formatted value to destination buffer
// The grown buffer have to be returned
XFmtString(b []byte) []byte
}
// Buffer provides syntactic sugar for formatting mimicking fmt.Printf style
// XXX combine with bytes.Buffer ?
type Buffer []byte
// Reset empties the buffer keeping underlying storage for future formattings
func (b *Buffer) Reset() {
*b = (*b)[:0]
}
// Bytes returns buffer storage as []byte
func (b Buffer) Bytes() []byte {
return []byte(b)
}
// Append appends to b formatted x
//
// NOTE sadly since x is interface it makes real value substituted to it
// escape to heap (not so a problem since usually they already are) but then also
// if x has non-pointer receiver convT2I _allocates_ memory for the value copy.
//
// -> always pass to append &object, even if object has non-pointer XFmtString receiver.
func Append(b []byte, x Stringer) []byte {
return x.XFmtString(b)
}
// V, similarly to %v, adds x formatted by default rules
func (b *Buffer) V(x Stringer) *Buffer {
*b = Append(*b, x)
return b
}
// S appends string formatted by %s
func (b *Buffer) S(s string) *Buffer {
*b = append(*b, s...)
return b
}
// Sb appends []byte formatted by %s
func (b *Buffer) Sb(x []byte) *Buffer {
*b = append(*b, x...)
return b
}
// Q appends string formatted by %q
func (b *Buffer) Q(s string) *Buffer {
*b = strconv.AppendQuote(*b, s)
return b
}
// Qb appends []byte formatted by %q
func (b *Buffer) Qb(s []byte) *Buffer {
*b = strconv.AppendQuote(*b, mem.String(s))
return b
}
// Qcb appends byte formatted by %q
func (b *Buffer) Qcb(c byte) *Buffer {
return b.Qc(rune(c))
}
// Qc appends rune formatted by %q
func (b *Buffer) Qc(c rune) *Buffer {
*b = strconv.AppendQuoteRune(*b, c)
return b
}
// Cb appends byte formatted by %c
func (b *Buffer) Cb(c byte) *Buffer {
*b = append(*b, c)
return b
}
// AppendRune appends to b UTF-8 encoding of r
func AppendRune(b []byte, r rune) []byte {
l := len(b)
b = xbytes.Grow(b, utf8.UTFMax)
n := utf8.EncodeRune(b[l:], r)
return b[:l+n]
}
// C appends rune formatted by %c
func (b *Buffer) C(r rune) *Buffer {
*b = AppendRune(*b, r)
return b
}
// D appends int formatted by %d
func (b *Buffer) D(i int) *Buffer {
*b = strconv.AppendInt(*b, int64(i), 10)
return b
}
// D64 appends int64 formatted by %d
func (b *Buffer) D64(i int64) *Buffer {
*b = strconv.AppendInt(*b, i, 10)
return b
}
// X appends int formatted by %x
func (b *Buffer) X(i int) *Buffer {
*b = strconv.AppendInt(*b, int64(i), 16)
return b
}
// AppendHex appends to b hex representation of x
func AppendHex(b []byte, x []byte) []byte {
lx := hex.EncodedLen(len(x))
lb := len(b)
b = xbytes.Grow(b, lx)
hex.Encode(b[lb:], x)
return b
}
// Xb appends []byte formatted by %x
func (b *Buffer) Xb(x []byte) *Buffer {
*b = AppendHex(*b, x)
return b
}
// Xs appends string formatted by %x
func (b *Buffer) Xs(x string) *Buffer {
return b.Xb(mem.Bytes(x))
}
// TODO XX = %X
// AppendHex016 appends to b x formatted 16-character hex string
func AppendHex016(b []byte, x uint64) []byte {
// like sprintf("%016x") but faster and less allocations
l := len(b)
b = xbytes.Grow(b, 16)
bb := b[l:]
for i := 15; i >= 0; i-- {
bb[i] = hexdigits[x & 0xf]
x >>= 4
}
return b
}
// X016, similarly to %016x, adds hex representation of uint64 x
func (b *Buffer) X016(x uint64) *Buffer {
*b = AppendHex016(*b, x)
return b
}
// Copyright (C) 2017 Nexedi SA and Contributors.
// Kirill Smelkov <kirr@nexedi.com>
//
// This program is free software: you can Use, Study, Modify and Redistribute
// it under the terms of the GNU General Public License version 3, or (at your
// option) any later version, as published by the Free Software Foundation.
//
// You can also Link and Combine this program with other software covered by
// the terms of any of the Open Source Initiative approved licenses and Convey
// the resulting work. Corresponding source of such a combination shall include
// the source code for all other software used.
//
// This program is distributed WITHOUT ANY WARRANTY; without even the implied
// warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
//
// See COPYING file for full licensing terms.
package xfmt
import (
"fmt"
"reflect"
"testing"
)
var testv = []struct {format, xformatMeth string; value interface{}} {
{"%c", "Cb", byte('A')},
{"%c", "C", rune(-1)},
{"%c", "C", 'B'}, // 1-byte encoded
{"%c", "C", 'и'}, // 2-bytes encoded
{"%c", "C", '\u20ac'}, // 3-bytes encoded
{"%c", "C", '\U00010001'}, // 4-bytes encoded
{"%s", "S", "hello"},
{"%s", "Sb", []byte("world")},
{"%q", "Q", "alpha"},
{"%q", "Qb", []byte("beta")},
{"%q", "Qcb", byte('D')},
{"%q", "Qc", 'B'}, // 1-byte encoded
{"%q", "Qc", 'и'}, // 2-bytes encoded
{"%q", "Qc", '\u20ac'}, // 3-bytes encoded
{"%q", "Qc", '\U00010001'}, // 4-bytes encoded
{"%x", "Xb", []byte("hexstring")},
{"%x", "Xs", "stringhex"},
{"%d", "D", 12765},
{"%d", "D64", int64(12764)},
{"%x", "X", 12789},
{"%016x", "X016", uint64(124)},
{"%v", "V", &stringerTest{}},
}
type stringerTest struct {
}
func (s *stringerTest) String() string {
return string(s.XFmtString(nil))
}
func (s *stringerTest) XFmtString(b []byte) []byte {
return append(b, `stringer test`...)
}
// verify formatting result is the same in between std fmt and xfmt
func TestXFmt(t *testing.T) {
buf := &Buffer{}
xbuf := reflect.ValueOf(buf)
for _, tt := range testv {
// result via fmt
resFmt := fmt.Sprintf(tt.format, tt.value)
// result via xfmt (via reflect.Call)
buf.Reset()
xmeth := xbuf.MethodByName(tt.xformatMeth)
if !xmeth.IsValid() {
t.Errorf(".%v: no such method", tt.xformatMeth)
continue
}
xargv := []reflect.Value{reflect.ValueOf(tt.value)}
xretv := []reflect.Value{}
callOk := false
func () {
defer func() {
if r := recover(); r != nil {
t.Errorf("%v: panic: %v", tt, r)
}
}()
xretv = xmeth.Call(xargv)
callOk = true
}()
if !callOk {
continue
}
// check all formatters return pointer to the same buf
// (this way it is handy to do .S("hello ") .X016(123) .V(zzz) ...
if !(len(xretv) == 1 && xretv[0].Interface() == buf) {
t.Errorf(".%v: returned %#v ; want %#v", tt.xformatMeth, xretv[0].Interface(), buf)
continue
}
resXFmt := string(*buf)
// results must be the same
if resFmt != resXFmt {
t.Errorf(".%v(%v) -> %q != printf(%q) -> %q",
tt.xformatMeth, tt.value, resXFmt, tt.format, resFmt)
}
}
}
func BenchmarkXFmt(b *testing.B) {
buf := &Buffer{}
for _, tt := range testv {
b.Run(fmt.Sprintf("%s(%#v)", tt.format, tt.value), func (b *testing.B) {
for i := 0; i < b.N; i++ {
fmt.Sprintf(tt.format, tt.value)
}
})
// construct methProxy for natively calling (not via reflect) method associated with tt.xformatMeth
// (calling via reflect allocates a lot and is slow)
// NOTE because of proxies the call is a bit slower than e.g. directly calling buf.S("...")
var methProxy func(buf *Buffer, v interface{})
xmeth, ok := reflect.TypeOf(buf).MethodByName(tt.xformatMeth)
if !ok {
b.Errorf(".%v: no such method", tt.xformatMeth)
continue
}
// XXX a bit ugly -> use code generation instead?
meth := xmeth.Func.Interface()
switch tt.value.(type) {
case byte: methProxy = func(buf *Buffer, v interface{}) { meth.(func (*Buffer, byte) *Buffer)(buf, v.(byte)) }
case rune: methProxy = func(buf *Buffer, v interface{}) { meth.(func (*Buffer, rune) *Buffer)(buf, v.(rune)) }
case string: methProxy = func(buf *Buffer, v interface{}) { meth.(func (*Buffer, string) *Buffer)(buf, v.(string)) }
case []byte: methProxy = func(buf *Buffer, v interface{}) { meth.(func (*Buffer, []byte) *Buffer)(buf, v.([]byte)) }
case int: methProxy = func(buf *Buffer, v interface{}) { meth.(func (*Buffer, int) *Buffer)(buf, v.(int)) }
case int64: methProxy = func(buf *Buffer, v interface{}) { meth.(func (*Buffer, int64) *Buffer)(buf, v.(int64)) }
case uint64: methProxy = func(buf *Buffer, v interface{}) { meth.(func (*Buffer, uint64) *Buffer)(buf, v.(uint64)) }
case *stringerTest: methProxy = func(buf *Buffer, v interface{}) { meth.(func (*Buffer, Stringer) *Buffer)(buf, v.(Stringer)) }
default:
b.Fatalf("TODO add support for %T", tt.value)
}
b.Run(fmt.Sprintf(".%s(%#v)", tt.xformatMeth, tt.value), func (b *testing.B) {
for i := 0; i < b.N; i++ {
buf.Reset()
methProxy(buf, tt.value)
}
})
}
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment