Skip to content

Commit

Permalink
opt: skip whole for empty struct
Browse files Browse the repository at this point in the history
  • Loading branch information
AsterDY committed Aug 26, 2024
1 parent 5069157 commit fb0fdb9
Show file tree
Hide file tree
Showing 4 changed files with 103 additions and 62 deletions.
71 changes: 46 additions & 25 deletions decoder/decoder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ package decoder

import (
"encoding/json"
"fmt"
"runtime"
"runtime/debug"
"strings"
Expand Down Expand Up @@ -86,48 +85,70 @@ func init() {
_ = json.Unmarshal([]byte(TwitterJson), &_BindingValue)
}

func TestFastSkip(t *testing.T) {
func BenchmarkSkipValidate(b *testing.B) {
type skiptype struct {
A int `json:"a"` // mismatched
B string `json:"-"` // ommited
C [1]float64 `json:"c"` // fast int
C [1]int `json:"c"` // fast int
D struct {} `json:"d"` // empty struct
E map[string]int `json:"e"` // mismatched elem
// Unknonwn
}
type C struct {
name string
json string
expTime float64
}
var compt = `[+`+TwitterJson+`,`+TwitterJson+`,`+TwitterJson+`,`+TwitterJson+`,`+TwitterJson+`,`+TwitterJson+`,`+TwitterJson+`,`+TwitterJson+`+]`
var sam = map[int]interface{}{}
for i := 0; i < 10; i++ {
sam[i] = _BindingValue
}
comptd, err := json.Marshal(sam)
if err != nil {
b.Fatal("invalid json")
}
compt := string(comptd)
var cases = []C{
{"mismatched", `{"a":`+compt+`}`, 2},
{"ommited", `{"b":`+compt+`}`, 2},
// {"fast int", `{"c":[`+strings.Repeat("-1.23456e-19,", 2000)+`1]}`, 1.2},
// {"ommited", `{"b":`+compt+`}`, 2},
{"fast int", `{"c":[`+strings.Repeat("-1.23456e-19,", 2000)+`1]}`, 1.2},
{"unknown", `{"unknown":`+compt+`}`, 2},
{"empty", `{"d":`+compt+`}`, 2},
{"mismatched elem", `{"e":`+compt+`}`, 2},
}
_ = NewDecoder(`{}`).Decode(&skiptype{})
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
var obj1, obj2 = &skiptype{}, &skiptype{}
// validate skip
d := NewDecoder(c.json)
t1 := time.Now()
err1 := d.Decode(obj1)
d1 := time.Since(t1)
// fask skip
d = NewDecoder(c.json)
d.SetOptions(OptionNoValidateJSON)
t2 := time.Now()
err2 := d.Decode(obj2)
d2 := time.Since(t2)

require.Equal(t, err1 == nil, err2 == nil)
require.Equal(t, obj1, obj2)
// fast skip must be 5x faster
println(d1, d2)
require.True(t, float64(d1)/float64(d2) > c.expTime, fmt.Sprintf("%v/%v=%v", d1, d2, float64(d1)/float64(d2)))
b.Run(c.name, func(b *testing.B) {
b.Run("validate", func(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
var obj1 = &skiptype{}
// validate skip
d := NewDecoder(c.json)
// t1 := time.Now()
err1 := d.Decode(obj1)
_ = err1
}
})
b.Run("fast", func(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
var obj2 = &skiptype{}
// d1 := time.Since(t1)
// fask skip
d := NewDecoder(c.json)
d.SetOptions(OptionNoValidateJSON)
// t2 := time.Now()
err2 := d.Decode(obj2)
// d2 := time.Since(t2)
_ = err2
}
})
// require.Equal(t, err1 == nil, err2 == nil)
// require.Equal(t, obj1, obj2)
// // fast skip must be 5x faster
// println(d1, d2)
// require.True(t, float64(d1)/float64(d2) > c.expTime, fmt.Sprintf("%v/%v=%v", d1, d2, float64(d1)/float64(d2)))
})
}
// var data = `{"a":`+TwitterJson+`,"b":`+TwitterJson+`,"c":[`+strings.Repeat("1,", 1024)+`1], "d":`+TwitterJson+`, "UNKNOWN":`+TwitterJson+`}`
Expand Down
57 changes: 37 additions & 20 deletions internal/decoder/jitdec/assembler_regabi_amd64.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
//go:build go1.17 && !go1.24
// +build go1.17,!go1.24

/*
Expand All @@ -19,18 +20,19 @@
package jitdec

import (
`encoding/json`
`fmt`
`math`
`reflect`
`unsafe`

`github.com/bytedance/sonic/internal/caching`
`github.com/bytedance/sonic/internal/jit`
`github.com/bytedance/sonic/internal/native`
`github.com/bytedance/sonic/internal/native/types`
`github.com/bytedance/sonic/internal/rt`
`github.com/twitchyliquid64/golang-asm/obj`
"encoding/json"
"fmt"
"math"
"reflect"
"strings"
"unsafe"

"github.com/bytedance/sonic/internal/caching"
"github.com/bytedance/sonic/internal/jit"
"github.com/bytedance/sonic/internal/native"
"github.com/bytedance/sonic/internal/native/types"
"github.com/bytedance/sonic/internal/rt"
"github.com/twitchyliquid64/golang-asm/obj"
)

/** Register Allocations
Expand Down Expand Up @@ -292,7 +294,6 @@ var _OpFuncTab = [256]func(*_Assembler, *_Instr) {
_OP_array_clear_p : (*_Assembler)._asm_OP_array_clear_p,
_OP_slice_init : (*_Assembler)._asm_OP_slice_init,
_OP_slice_append : (*_Assembler)._asm_OP_slice_append,
_OP_object_skip : (*_Assembler)._asm_OP_object_skip,
_OP_object_next : (*_Assembler)._asm_OP_object_next,
_OP_struct_field : (*_Assembler)._asm_OP_struct_field,
_OP_unmarshal : (*_Assembler)._asm_OP_unmarshal,
Expand All @@ -312,6 +313,7 @@ var _OpFuncTab = [256]func(*_Assembler, *_Instr) {
_OP_check_char_0 : (*_Assembler)._asm_OP_check_char_0,
_OP_dismatch_err : (*_Assembler)._asm_OP_dismatch_err,
_OP_go_skip : (*_Assembler)._asm_OP_go_skip,
_OP_skip_emtpy : (*_Assembler)._asm_OP_skip_empty,
_OP_add : (*_Assembler)._asm_OP_add,
_OP_check_empty : (*_Assembler)._asm_OP_check_empty,
_OP_debug : (*_Assembler)._asm_OP_debug,
Expand Down Expand Up @@ -600,6 +602,28 @@ func (self *_Assembler) _asm_OP_go_skip(p *_Instr) {
self.Sjmp("JMP" , _LB_skip_one) // JMP _skip_one
}

var _F_IndexByte = jit.Func(strings.IndexByte)

func (self *_Assembler) _asm_OP_skip_empty(p *_Instr) {
// self.Byte(0xcc)
self.call_sf(_F_skip_one) // CALL_SF skip_one
// self.Byte(0xcc)
self.Emit("TESTQ", _AX, _AX) // TESTQ AX, AX
self.Sjmp("JS" , _LB_parsing_error_v) // JS _parse_error_v
self.Emit("BTQ", jit.Imm(_F_disable_unknown), _ARG_fv)
self.Xjmp("JNC", p.vi())
self.Emit("LEAQ", jit.Sib(_IC, _AX, 1, 0), _BX)
self.Emit("MOVQ", _BX, _ARG_sv_n)
self.Emit("LEAQ", jit.Sib(_IP, _AX, 1, 0), _AX)
self.Emit("MOVQ", _AX, _ARG_sv_p)
self.Emit("MOVQ", jit.Imm(':'), _CX)
self.call_go(_F_IndexByte)
// self.Byte(0xcc)
self.Emit("TESTQ", _AX, _AX)
// disallow unknown field
self.Sjmp("JNS", _LB_field_error)
}

func (self *_Assembler) skip_one() {
self.Link(_LB_skip_one) // _skip:
self.Emit("MOVQ", _VAR_ic, _IC) // MOVQ _VAR_ic, IC
Expand Down Expand Up @@ -1059,7 +1083,6 @@ func (self *_Assembler) mapassign_utext(t reflect.Type, addressable bool) {
var (
_F_skip_one = jit.Imm(int64(native.S_skip_one))
_F_skip_array = jit.Imm(int64(native.S_skip_array))
_F_skip_object = jit.Imm(int64(native.S_skip_object))
_F_skip_number = jit.Imm(int64(native.S_skip_number))
)

Expand Down Expand Up @@ -1709,12 +1732,6 @@ func (self *_Assembler) _asm_OP_slice_append(p *_Instr) {
self.Link("_append_slice_end_{n}")
}

func (self *_Assembler) _asm_OP_object_skip(_ *_Instr) {
self.call_sf(_F_skip_object) // CALL_SF skip_object
self.Emit("TESTQ", _AX, _AX) // TESTQ AX, AX
self.Sjmp("JS" , _LB_parsing_error_v) // JS _parse_error_v
}

func (self *_Assembler) _asm_OP_object_next(_ *_Instr) {
self.call_sf(_F_skip_one) // CALL_SF skip_one
self.Emit("TESTQ", _AX, _AX) // TESTQ AX, AX
Expand Down
9 changes: 1 addition & 8 deletions internal/decoder/jitdec/assembler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -528,14 +528,7 @@ func TestAssembler_OpCode(t *testing.T) {
src: "",
exp: []int{123, 0},
val: &[]int{123},
}, {
key: "_OP_object_skip",
ins: []_Instr{newInsOp(_OP_object_skip)},
src: `{"zxcv":[1,2.0],"asdf":[true,false,null,"asdf",{"qwer":345}]}`,
pos: 1,
vfn: func(i int, _ interface{}) { assert.Equal(t, 61, i) },
val: nil,
}, {
},{
key: "_OP_object_next",
ins: []_Instr{newInsOp(_OP_object_next)},
src: `{"asdf":[1,2.0,true,false,null,"asdf",{"qwer":345}]}`,
Expand Down
28 changes: 19 additions & 9 deletions internal/decoder/jitdec/compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,6 @@ const (
_OP_array_clear_p
_OP_slice_init
_OP_slice_append
_OP_object_skip
_OP_object_next
_OP_struct_field
_OP_unmarshal
Expand All @@ -97,6 +96,7 @@ const (
_OP_check_char_0
_OP_dismatch_err
_OP_go_skip
_OP_skip_emtpy
_OP_add
_OP_check_empty
_OP_debug
Expand Down Expand Up @@ -155,7 +155,6 @@ var _OpNames = [256]string {
_OP_array_skip : "array_skip",
_OP_slice_init : "slice_init",
_OP_slice_append : "slice_append",
_OP_object_skip : "object_skip",
_OP_object_next : "object_next",
_OP_struct_field : "struct_field",
_OP_unmarshal : "unmarshal",
Expand Down Expand Up @@ -902,7 +901,24 @@ func (self *_Compiler) compileStructBody(p *_Program, sp int, vt reflect.Type) {
n := p.pc()
p.add(_OP_is_null)

skip := self.checkIfSkip(p, vt, '{')
j := p.pc()
p.chr(_OP_check_char_0, '{')
p.rtt(_OP_dismatch_err, vt)

/* special case for empty object */
if len(fv) == 0 {
p.pin(j)
s := p.pc()
p.add(_OP_skip_emtpy)
p.pin(s)
p.pin(n)
return
}

skip := p.pc()
p.add(_OP_go_skip)
p.pin(j)
p.int(_OP_add, 1)

p.add(_OP_save)
p.add(_OP_lspace)
Expand All @@ -920,11 +936,6 @@ func (self *_Compiler) compileStructBody(p *_Program, sp int, vt reflect.Type) {
p.chr(_OP_check_char, '}')
p.chr(_OP_match_char, ',')

/* special case of an empty struct */
if len(fv) == 0 {
p.add(_OP_object_skip)
goto end_of_object
}

/* match the remaining fields */
p.add(_OP_lspace)
Expand Down Expand Up @@ -960,7 +971,6 @@ func (self *_Compiler) compileStructBody(p *_Program, sp int, vt reflect.Type) {
p.int(_OP_goto, y0)
}

end_of_object:
p.pin(x)
p.pin(y1)
p.add(_OP_drop)
Expand Down

0 comments on commit fb0fdb9

Please sign in to comment.