mirror of https://github.com/tailscale/tailscale/
util/deephash: use unsafe.Pointer instead of reflect.Value (#5459)
Use of reflect.Value.SetXXX panics if the provided argument was obtained from an unexported struct field. Instead, pass an unsafe.Pointer around and convert to a reflect.Value when necessary (i.e., for maps and interfaces). Converting from unsafe.Pointer to reflect.Value guarantees that none of the read-only bits will be populated. When running in race mode, we attach type information to the pointer so that we can type check every pointer operation. This also type-checks that direct memory hashing is within the valid range of a struct value. We add test cases that previously caused deephash to panic, but now pass. Performance: name old time/op new time/op delta Hash 14.1µs ± 1% 14.1µs ± 1% ~ (p=0.590 n=10+9) HashPacketFilter 2.53µs ± 2% 2.44µs ± 1% -3.79% (p=0.000 n=9+10) TailcfgNode 1.45µs ± 1% 1.43µs ± 0% -1.36% (p=0.000 n=9+9) HashArray 318ns ± 2% 318ns ± 2% ~ (p=0.541 n=10+10) HashMapAcyclic 32.9µs ± 1% 31.6µs ± 1% -4.16% (p=0.000 n=10+9) There is a slight performance gain due to the use of unsafe.Pointer over reflect.Value methods. Also, passing an unsafe.Pointer (1 word) on the stack is cheaper than passing a reflect.Value (3 words). Performance gains are diminishing since SHA-256 hashing now dominates the runtime. Signed-off-by: Joe Tsai <joetsai@digital-static.net>pull/5463/head
parent
e0c5ac1f02
commit
31bf3874d6
@ -0,0 +1,115 @@
|
|||||||
|
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package deephash
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/netip"
|
||||||
|
"reflect"
|
||||||
|
"time"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
// unsafePointer is an untyped pointer.
|
||||||
|
// It is the caller's responsibility to call operations on the correct type.
|
||||||
|
//
|
||||||
|
// This pointer only ever points to a small set of kinds or types:
|
||||||
|
// time.Time, netip.Addr, string, array, slice, struct, map, pointer, interface,
|
||||||
|
// or a pointer to memory that is directly hashable.
|
||||||
|
//
|
||||||
|
// Arrays are represented as pointers to the first element.
|
||||||
|
// Structs are represented as pointers to the first field.
|
||||||
|
// Slices are represented as pointers to a slice header.
|
||||||
|
// Pointers are represented as pointers to a pointer.
|
||||||
|
//
|
||||||
|
// We do not support direct operations on maps and interfaces, and instead
|
||||||
|
// rely on pointer.asValue to convert the pointer back to a reflect.Value.
|
||||||
|
// Conversion of an unsafe.Pointer to reflect.Value guarantees that the
|
||||||
|
// read-only flag in the reflect.Value is unpopulated, avoiding panics that may
|
||||||
|
// othewise have occurred since the value was obtained from an unexported field.
|
||||||
|
type unsafePointer struct{ p unsafe.Pointer }
|
||||||
|
|
||||||
|
func unsafePointerOf(v reflect.Value) unsafePointer {
|
||||||
|
return unsafePointer{v.UnsafePointer()}
|
||||||
|
}
|
||||||
|
func (p unsafePointer) isNil() bool {
|
||||||
|
return p.p == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// pointerElem dereferences a pointer.
|
||||||
|
// p must point to a pointer.
|
||||||
|
func (p unsafePointer) pointerElem() unsafePointer {
|
||||||
|
return unsafePointer{*(*unsafe.Pointer)(p.p)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// sliceLen returns the slice length.
|
||||||
|
// p must point to a slice.
|
||||||
|
func (p unsafePointer) sliceLen() int {
|
||||||
|
return (*reflect.SliceHeader)(p.p).Len
|
||||||
|
}
|
||||||
|
|
||||||
|
// sliceArray returns a pointer to the underlying slice array.
|
||||||
|
// p must point to a slice.
|
||||||
|
func (p unsafePointer) sliceArray() unsafePointer {
|
||||||
|
return unsafePointer{unsafe.Pointer((*reflect.SliceHeader)(p.p).Data)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// arrayIndex returns a pointer to an element in the array.
|
||||||
|
// p must point to an array.
|
||||||
|
func (p unsafePointer) arrayIndex(index int, size uintptr) unsafePointer {
|
||||||
|
return unsafePointer{unsafe.Add(p.p, uintptr(index)*size)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// structField returns a pointer to a field in a struct.
|
||||||
|
// p must pointer to a struct.
|
||||||
|
func (p unsafePointer) structField(index int, offset, size uintptr) unsafePointer {
|
||||||
|
return unsafePointer{unsafe.Add(p.p, offset)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// asString casts p as a *string.
|
||||||
|
func (p unsafePointer) asString() *string {
|
||||||
|
return (*string)(p.p)
|
||||||
|
}
|
||||||
|
|
||||||
|
// asTime casts p as a *time.Time.
|
||||||
|
func (p unsafePointer) asTime() *time.Time {
|
||||||
|
return (*time.Time)(p.p)
|
||||||
|
}
|
||||||
|
|
||||||
|
// asAddr casts p as a *netip.Addr.
|
||||||
|
func (p unsafePointer) asAddr() *netip.Addr {
|
||||||
|
return (*netip.Addr)(p.p)
|
||||||
|
}
|
||||||
|
|
||||||
|
// asValue casts p as a reflect.Value containing a pointer to value of t.
|
||||||
|
func (p unsafePointer) asValue(typ reflect.Type) reflect.Value {
|
||||||
|
return reflect.NewAt(typ, p.p)
|
||||||
|
}
|
||||||
|
|
||||||
|
// asMemory returns the memory pointer at by p for a specified size.
|
||||||
|
func (p unsafePointer) asMemory(size uintptr) []byte {
|
||||||
|
return unsafe.Slice((*byte)(p.p), size)
|
||||||
|
}
|
||||||
|
|
||||||
|
// visitStack is a stack of pointers visited.
|
||||||
|
// Pointers are pushed onto the stack when visited, and popped when leaving.
|
||||||
|
// The integer value is the depth at which the pointer was visited.
|
||||||
|
// The length of this stack should be zero after every hashing operation.
|
||||||
|
type visitStack map[unsafe.Pointer]int
|
||||||
|
|
||||||
|
func (v visitStack) seen(p unsafe.Pointer) (int, bool) {
|
||||||
|
idx, ok := v[p]
|
||||||
|
return idx, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *visitStack) push(p unsafe.Pointer) {
|
||||||
|
if *v == nil {
|
||||||
|
*v = make(map[unsafe.Pointer]int)
|
||||||
|
}
|
||||||
|
(*v)[p] = len(*v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v visitStack) pop(p unsafe.Pointer) {
|
||||||
|
delete(v, p)
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
//go:build !race
|
||||||
|
|
||||||
|
package deephash
|
||||||
|
|
||||||
|
import "reflect"
|
||||||
|
|
||||||
|
type pointer = unsafePointer
|
||||||
|
|
||||||
|
// pointerOf returns a pointer from v, which must be a reflect.Pointer.
|
||||||
|
func pointerOf(v reflect.Value) pointer { return unsafePointerOf(v) }
|
@ -0,0 +1,100 @@
|
|||||||
|
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
//go:build race
|
||||||
|
|
||||||
|
package deephash
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/netip"
|
||||||
|
"reflect"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// pointer is a typed pointer that performs safety checks for every operation.
|
||||||
|
type pointer struct {
|
||||||
|
unsafePointer
|
||||||
|
t reflect.Type // type of pointed-at value; may be nil
|
||||||
|
n uintptr // size of valid memory after p
|
||||||
|
}
|
||||||
|
|
||||||
|
// pointerOf returns a pointer from v, which must be a reflect.Pointer.
|
||||||
|
func pointerOf(v reflect.Value) pointer {
|
||||||
|
assert(v.Kind() == reflect.Pointer, "got %v, want pointer", v.Kind())
|
||||||
|
te := v.Type().Elem()
|
||||||
|
return pointer{unsafePointerOf(v), te, te.Size()}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p pointer) pointerElem() pointer {
|
||||||
|
assert(p.t.Kind() == reflect.Pointer, "got %v, want pointer", p.t.Kind())
|
||||||
|
te := p.t.Elem()
|
||||||
|
return pointer{p.unsafePointer.pointerElem(), te, te.Size()}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p pointer) sliceLen() int {
|
||||||
|
assert(p.t.Kind() == reflect.Slice, "got %v, want slice", p.t.Kind())
|
||||||
|
return p.unsafePointer.sliceLen()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p pointer) sliceArray() pointer {
|
||||||
|
assert(p.t.Kind() == reflect.Slice, "got %v, want slice", p.t.Kind())
|
||||||
|
n := p.sliceLen()
|
||||||
|
assert(n >= 0, "got negative slice length %d", n)
|
||||||
|
ta := reflect.ArrayOf(n, p.t.Elem())
|
||||||
|
return pointer{p.unsafePointer.sliceArray(), ta, ta.Size()}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p pointer) arrayIndex(index int, size uintptr) pointer {
|
||||||
|
assert(p.t.Kind() == reflect.Array, "got %v, want array", p.t.Kind())
|
||||||
|
assert(0 <= index && index < p.t.Len(), "got array of size %d, want to access element %d", p.t.Len(), index)
|
||||||
|
assert(p.t.Elem().Size() == size, "got element size of %d, want %d", p.t.Elem().Size(), size)
|
||||||
|
te := p.t.Elem()
|
||||||
|
return pointer{p.unsafePointer.arrayIndex(index, size), te, te.Size()}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p pointer) structField(index int, offset, size uintptr) pointer {
|
||||||
|
assert(p.t.Kind() == reflect.Struct, "got %v, want struct", p.t.Kind())
|
||||||
|
assert(p.n >= offset, "got size of %d, want excessive start offset of %d", p.n, offset)
|
||||||
|
assert(p.n >= offset+size, "got size of %d, want excessive end offset of %d", p.n, offset+size)
|
||||||
|
if index < 0 {
|
||||||
|
return pointer{p.unsafePointer.structField(index, offset, size), nil, size}
|
||||||
|
}
|
||||||
|
sf := p.t.Field(index)
|
||||||
|
t := sf.Type
|
||||||
|
assert(sf.Offset == offset, "got offset of %d, want offset %d", sf.Offset, offset)
|
||||||
|
assert(t.Size() == size, "got size of %d, want size %d", t.Size(), size)
|
||||||
|
return pointer{p.unsafePointer.structField(index, offset, size), t, t.Size()}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p pointer) asString() *string {
|
||||||
|
assert(p.t.Kind() == reflect.String, "got %v, want string", p.t)
|
||||||
|
return p.unsafePointer.asString()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p pointer) asTime() *time.Time {
|
||||||
|
assert(p.t == timeTimeType, "got %v, want %v", p.t, timeTimeType)
|
||||||
|
return p.unsafePointer.asTime()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p pointer) asAddr() *netip.Addr {
|
||||||
|
assert(p.t == netipAddrType, "got %v, want %v", p.t, netipAddrType)
|
||||||
|
return p.unsafePointer.asAddr()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p pointer) asValue(typ reflect.Type) reflect.Value {
|
||||||
|
assert(p.t == typ, "got %v, want %v", p.t, typ)
|
||||||
|
return p.unsafePointer.asValue(typ)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p pointer) asMemory(size uintptr) []byte {
|
||||||
|
assert(p.n >= size, "got size of %d, want excessive size of %d", p.n, size)
|
||||||
|
return p.unsafePointer.asMemory(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
func assert(b bool, f string, a ...any) {
|
||||||
|
if !b {
|
||||||
|
panic(fmt.Sprintf(f, a...))
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue