GO Language Interview Essence - Can the float type be used as the key of a map?

From a grammatical point of view, it is possible. In Go language, any comparable type can be used as a key. Except for slice, map, and functions types, other types are OK. Specifically include: Boolean values, numbers, strings, pointers, channels, interface types, structures, and arrays containing only the above types. The common feature of these types is that they support the ==and !=operators, k1 == k2when k1 and k2 can be considered to be the same key. If it is a structure, only the hashed values ​​and literal values ​​are equal, are they considered to be the same key. For many literal values ​​that are equal, the hashed values ​​may not be equal, such as references.

By the way, any type can be used as value, including map types.

Let’s see an example:

func main() {
	m := make(map[float64]int)
	m[1.4] = 1
	m[2.4] = 2
	m[math.NaN()] = 3
	m[math.NaN()] = 3

	for k, v := range m {
		fmt.Printf("[%v, %d] ", k, v)
	}

	fmt.Printf("\nk: %v, v: %d\n", math.NaN(), m[math.NaN()])
	fmt.Printf("k: %v, v: %d\n", 2.400000000001, m[2.400000000001])
	fmt.Printf("k: %v, v: %d\n", 2.4000000000000000000000001, m[2.4000000000000000000000001])

	fmt.Println(math.NaN() == math.NaN())
}

Program output:

[2.4, 2] [NaN, 3] [NaN, 3] [1.4, 1] 
k: NaN, v: 0
k: 2.400000000001, v: 0
k: 2.4, v: 2
false

In the example, a map whose key type is float is defined, and four keys are inserted into it: 1.4, 2.4, NAN, NAN.

When printing, 4 keys are also printed. If you know NAN != NAN, it is not surprising. Because the results of their comparison are not equal, naturally, in the view of the map, they are two different keys.

Then, we queried several keys and found that NAN did not exist, nor did 2.400000000001, but 2.4000000000000000000000001 did.

Kind of weird, isn't it?

Then, I discovered the following facts through assembly:

When using float64 as the key, you must first convert it to uint64 type and then insert it into the key.

Specifically, this is done through Float64frombitsthe function:

// Float64frombits returns the floating point number corresponding
// the IEEE 754 binary representation b.
func Float64frombits(b uint64) float64 { return *(*float64)(unsafe.Pointer(&b)) }

That is, floating point numbers are expressed in the format specified by IEEE 754. Such as assignment statement:

0x00bd 00189 (test18.go:9)      LEAQ    "".statictmp_0(SB), DX
0x00c4 00196 (test18.go:9)      MOVQ    DX, 16(SP)
0x00c9 00201 (test18.go:9)      PCDATA  $0, $2
0x00c9 00201 (test18.go:9)      CALL    runtime.mapassign(SB)

"".statictmp_0(SB)The variables are like this:

"".statictmp_0 SRODATA size=8
        0x0000 33 33 33 33 33 33 03 40
"".statictmp_1 SRODATA size=8
        0x0000 ff 3b 33 33 33 33 03 40
"".statictmp_2 SRODATA size=8
        0x0000 33 33 33 33 33 33 03 40

Let’s output something again:

package main

import (
	"fmt"
	"math"
)

func main() {
	m := make(map[float64]int)
	m[2.4] = 2

    fmt.Println(math.Float64bits(2.4))
	fmt.Println(math.Float64bits(2.400000000001))
	fmt.Println(math.Float64bits(2.4000000000000000000000001))
}
4612586738352862003
4612586738352864255
4612586738352862003

Converted to hexadecimal:

0x4003333333333333
0x4003333333333BFF
0x4003333333333333

Compare it with the previous one "".statictmp_0, it's very clear. 2.4The result after conversion 2.4000000000000000000000001by math.Float64bits()function is the same. Naturally, from the perspective of map, the two have the same key.

Let’s take a look at NAN (not a number):

// NaN returns an IEEE 754 ``not-a-number'' value.
func NaN() float64 { return Float64frombits(uvnan) }

uvnan is defined as:

uvnan    = 0x7FF8000000000001

Call NAN() directly Float64frombits, pass in the hard-coded const variable 0x7FF8000000000001, and get the NAN value. Since NAN is parsed from a constant, why is it considered a different key when inserted into the map?

This is determined by the hash function of the type. For example, for a 64-bit floating point number, its hash function is as follows:

func f64hash(p unsafe.Pointer, h uintptr) uintptr {
	f := *(*float64)(p)
	switch {
	case f == 0:
		return c1 * (c0 ^ h) // +0, -0
	case f != f:
		return c1 * (c0 ^ h ^ uintptr(fastrand())) // any kind of NaN
	default:
		return memhash(p, h, 8)
	}
}

The second case f != fis for this purpose NAN. A random number will be added here.

In this way, all the puzzles are solved.

Due to the characteristics of NAN:

NAN != NAN
hash(NAN) != hash(NAN)

Therefore, when the key searched for in the map is NAN, nothing can be found; if 4 NANs are added to it, 4 NANs will be obtained through traversal.

Finally, the conclusion: the float type can be used as a key, but due to accuracy issues, it will cause some weird problems, so use it with caution.


Regarding when the key is a reference type, to determine whether two keys are equal, the hashed values ​​must be equal and the literals of the keys must be equal. Example added by @WuMingyu:

func TestT(t *testing.T) {
    
    
	type S struct {
    
    
		ID	int
	}
	s1 := S{
    
    ID: 1}
	s2 := S{
    
    ID: 1}

	var h = map[*S]int {
    
    }
	h[&s1] = 1
	t.Log(h[&s1])
	t.Log(h[&s2])
	t.Log(s1 == s2)
}

test output:

=== RUN   TestT
--- PASS: TestT (0.00s)
    endpoint_test.go:74: 1
    endpoint_test.go:75: 0
    endpoint_test.go:76: true
PASS

Process finished with exit code 0

Guess you like

Origin blog.csdn.net/zy_dreamer/article/details/132799925