Mar 11, 2021

[Go][code dig] resource resurrection of using runtime.SetFinalizer

Reference:
https://tailscale.com/blog/netaddr-new-ip-type-for-go/

intern.go
https://github.com/go4org/intern/blob/main/intern.go


Side knowledge

Comparison operators

https://golang.org/ref/spec#Comparison_operators
  • Slice, map, and function values are not comparable. 
  • Interface values are comparable. Two interface values are equal if they have identical dynamic types and equal dynamic values or if both have value nil.
  • Struct values are comparable if all their fields are comparable. Two struct values are equal if their corresponding non-blank fields are equal.
  • A comparison of two interface values with identical dynamic types causes a run-time panic if values of that type are not comparable. This behavior applies not only to direct interface value comparisons but also when comparing arrays of interface values or structs with interface-valued fields.

For go4org/intern package take the fact that interface is comparable.

//go:nocheckptr  // func should not be instrumented by checkptr

Code dig

Types:

type Value struct {
	_      [0]func() // prevent people from accidentally using value type as comparable
	cmpVal interface{}
	resurrected bool
}

// not exposed, caller would never use 'key' but value only, i.e internal cache is encapsulated.
type key struct {
	s      string
	cmpVal interface{}
	// isString reports whether key contains a string.
	// Without it, the zero value of key is ambiguous.
	isString bool
}

Data structure:

var (
	// mu guards valMap, a weakref map of *Value by underlying value.
	// It also guards the resurrected field of all *Values.
	mu      sync.Mutex
	valMap  = map[key]uintptr{} // to uintptr(*Value)
	valSafe = map[key]*Value{}        // non-nil in safe+leaky mode, i.e map size grow is unbounded.
)

main logic:

//go:nocheckptr
func get(k key) *Value {
	mu.Lock()
	defer mu.Unlock()

	var v *Value
	if valSafe != nil {
		v = valSafe[k]
	} else if addr, ok := valMap[k]; ok {
		v = (*Value)((unsafe.Pointer)(addr)) // addr is uintptr, the address it holds will always be valid
		v.resurrected = true
	}
	if v != nil {
		return v
	}
	v = k.Value()
	if valSafe != nil {
		valSafe[k] = v
	} else {
		// SetFinalizer before uintptr conversion (theoretical concern;
		// see https://github.com/go4org/intern/issues/13)
		runtime.SetFinalizer(v, finalize)
		valMap[k] = uintptr(unsafe.Pointer(v))
	}
	return v
}

func finalize(v *Value) {
	mu.Lock()
	defer mu.Unlock()
	if v.resurrected {
		// We lost the race. Somebody resurrected it while we
		// were about to finalize it. Try again next round.
		v.resurrected = false
		runtime.SetFinalizer(v, finalize)
		return
	}
	delete(valMap, keyFor(v.cmpVal))
}

Fact:

runtime.SetFinalizer will always make the pointer resource referenced again, until next time GC runs which has the pointer resource's SetFinalizer to nil can it be truly garbage collected.

  • currently, if a struct size is larger than 16 bytes, for assigning value with that struct to a passing in pointer to struct would result in allot zero size struct first than memcpy to the pointer to struct.
  • Use struct size less equal to 16 bytes compiler is able to optimize to store the local created struct direct into passed in pointer to struct's heap memory.
type T struct {
	a, b, c, d int
}

func f(x *T) {
	t := T{}
	*x = t
}

type U struct {
	a, b, c, d, e int
}

func g(x *U) {
	u := U{}
	*x = u
}
f is compiled optimally, to:

	XORPS	X0, X0
	MOVQ	"".x+8(SP), AX
	MOVUPS	X0, (AX)
	MOVUPS	X0, 16(AX)
	RET
g is quite a bit worse:

	MOVQ	BP, 40(SP)
	LEAQ	40(SP), BP
	MOVQ	$0, "".u(SP)
	XORPS	X0, X0
	MOVUPS	X0, "".u+8(SP)
	MOVUPS	X0, "".u+24(SP)
	MOVQ	"".u(SP), AX
	MOVQ	"".x+56(SP), CX
	MOVQ	AX, (CX)
	LEAQ	8(CX), DI
	LEAQ	"".u+8(SP), SI
	DUFFCOPY	$868
	MOVQ	40(SP), BP
	ADDQ	$48, SP
	RET

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.