Skip to content

Commit

Permalink
Optimize Diff for frequent equality (#204)
Browse files Browse the repository at this point in the history
Diff is most often used in tests where the expected outcome
is equality (and thus no reported difference). Optimize for this
situation by performing a fast-pass equality check and only fall
back on constructing a report if inequal.
  • Loading branch information
dsnet committed May 26, 2020
1 parent d08c604 commit 4a83f56
Showing 1 changed file with 45 additions and 25 deletions.
70 changes: 45 additions & 25 deletions cmp/compare.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,30 +90,8 @@ import (
// If there is a cycle, then the pointed at values are considered equal
// only if both addresses were previously visited in the same path step.
func Equal(x, y interface{}, opts ...Option) bool {
vx := reflect.ValueOf(x)
vy := reflect.ValueOf(y)

// If the inputs are different types, auto-wrap them in an empty interface
// so that they have the same parent type.
var t reflect.Type
if !vx.IsValid() || !vy.IsValid() || vx.Type() != vy.Type() {
t = reflect.TypeOf((*interface{})(nil)).Elem()
if vx.IsValid() {
vvx := reflect.New(t).Elem()
vvx.Set(vx)
vx = vvx
}
if vy.IsValid() {
vvy := reflect.New(t).Elem()
vvy.Set(vy)
vy = vvy
}
} else {
t = vx.Type()
}

s := newState(opts)
s.compareAny(&pathStep{t, vx, vy})
s.compareAny(rootStep(x, y))
return s.result.Equal()
}

Expand All @@ -132,15 +110,57 @@ func Equal(x, y interface{}, opts ...Option) bool {
// Do not depend on this output being stable. If you need the ability to
// programmatically interpret the difference, consider using a custom Reporter.
func Diff(x, y interface{}, opts ...Option) string {
s := newState(opts)

// Optimization: If there are no other reporters, we can optimize for the
// common case where the result is equal (and thus no reported difference).
// This avoids the expensive construction of a difference tree.
if len(s.reporters) == 0 {
s.compareAny(rootStep(x, y))
if s.result.Equal() {
return ""
}
s.result = diff.Result{} // Reset results
}

r := new(defaultReporter)
eq := Equal(x, y, Options(opts), Reporter(r))
s.reporters = append(s.reporters, reporter{r})
s.compareAny(rootStep(x, y))
d := r.String()
if (d == "") != eq {
if (d == "") != s.result.Equal() {
panic("inconsistent difference and equality results")
}
return d
}

// rootStep constructs the first path step. If x and y have differing types,
// then they are stored within an empty interface type.
func rootStep(x, y interface{}) PathStep {
vx := reflect.ValueOf(x)
vy := reflect.ValueOf(y)

// If the inputs are different types, auto-wrap them in an empty interface
// so that they have the same parent type.
var t reflect.Type
if !vx.IsValid() || !vy.IsValid() || vx.Type() != vy.Type() {
t = reflect.TypeOf((*interface{})(nil)).Elem()
if vx.IsValid() {
vvx := reflect.New(t).Elem()
vvx.Set(vx)
vx = vvx
}
if vy.IsValid() {
vvy := reflect.New(t).Elem()
vvy.Set(vy)
vy = vvy
}
} else {
t = vx.Type()
}

return &pathStep{t, vx, vy}
}

type state struct {
// These fields represent the "comparison state".
// Calling statelessCompare must not result in observable changes to these.
Expand Down

0 comments on commit 4a83f56

Please sign in to comment.