关于math.NaN() 的比较问题 (深度)

GuoYuefei · · 105 次点击 · 开始浏览    置顶
# 关于math.NaN() 的比较问题 ## 缘起 缘起于二刷map底层时看到的一个例子 ```go 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()) } ``` Output : ```go [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 ``` 由此可知, **NaN 是不等于 NaN 的**,然后我就在那边纠结这个**为啥不相等呢**? 我们都知道接口有动态类型和动态值相同的限制才算相等,**但是**关键注意 NaN 本质上将还是 float64 类型, 也就是说关于接口的比较法则是不适用于它。 于是我在那边查了半天资料,一无所获。(可能是我查资料的姿势不对吧)于是乎,决定自己动动手,找找看。 ## 直接搜寻 Go 的 源码(无结论) ```go // go version is 1.14.6 // runtime/alg.go func f64equal(p, q unsafe.Pointer) bool { return *(*float64)(p) == *(*float64)(q) } ``` 还是要解读下的,可能刚入门的gopher认为这个不就是直接比较嘛,其实这个函数只是为了统一接口,在这个文件下有很多和这个函数相同函数原型的函数,方便各变量调用相等操作。 可能我找的不够全面,反正从这边我无法找到关于 NaN 的特殊处理方法。 额,既然到了源码这部分,还是来认识下什么是 NaN 吧! ```go // go version is 1.14.6 // math/bits.go // NaN returns an IEEE 754 ``not-a-number'' value. func NaN() float64 { return Float64frombits(uvnan) } ``` Float64frombits 函数顾名思义就是从比特位层面看int64是代表什么样的float64 关键uvnan是什么, 其实就是一个常数 ```go // go version is 1.14.6 // math/bits.go const uvnan = 0x7FF8000000000001 ``` 其实看源码的时候你能看到很多常数,有一个很有趣的常数 <code>uvinf = 0x7FF0000000000000</code>,这个当符号位为正时,代表双精度的最大值,为负数时为最小值 根据 IEEE 754 标准呢,NaN 应该是指数部分全1, 小数部分非零, 但是只要小数部分有一个非0,就能区分出是 uvinf 还是 uvnan 了。go语言这边使用的是小数部分最低位为1其余小数部分为0的方式。 现在我们知道 NaN 到底是样什么东西了。 ## 通过汇编找问题所在 ```go // main.go package main import "fmt" //import "unsafe" import "math" func main() { a := math.NaN() b := xxxx(a) fmt.Println(b) } func xxxx(a float64) bool { r := a == a return r } ``` 以上是我的测试代码, 其实可以不用fmt, 通过-N方式关闭编译优化就行。 使用一个xxxx函数的原因是为了方便定位比较这块的汇编代码。 使用以下命令打印出汇编指令 ```go go tool compile -S main.go ``` 然后找到这块内容,这是我们需要的 ```assembly "".xxxx STEXT nosplit size=23 args=0x10 locals=0x0 0x0000 00000 (const.go:15) TEXT "".xxxx(SB), NOSPLIT|ABIInternal, $0-16 0x0000 00000 (const.go:15) PCDATA $0, $-2 0x0000 00000 (const.go:15) PCDATA $1, $-2 0x0000 00000 (const.go:15) FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB) 0x0000 00000 (const.go:15) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB) 0x0000 00000 (const.go:15) FUNCDATA $2, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB) 0x0000 00000 (const.go:16) PCDATA $0, $0 0x0000 00000 (const.go:16) PCDATA $1, $0 0x0000 00000 (const.go:16) MOVSD "".a+8(SP), X0 0x0006 00006 (const.go:16) UCOMISD X0, X0 0x000a 00010 (const.go:16) SETEQ CL 0x000d 00013 (const.go:16) SETPC AL 0x0010 00016 (const.go:16) ANDL AX, CX 0x0012 00018 (const.go:17) MOVB CL, "".~r1+16(SP) 0x0016 00022 (const.go:17) RET 0x0000 f2 0f 10 44 24 08 66 0f 2e c0 0f 94 c1 0f 9b c0 ...D$.f......... 0x0010 21 c1 88 4c 24 10 c3 !..L$.. ``` <code>MOVSD "".a+8(SP), X0 </code>这部分开始一直到 <code>RET</code>,都是我们所需要的。 ```assembly MOVSD "".a+8(SP), X0 ; 将函数的第一个参数放入X0中 双字长 UCOMISD X0, X0 ; 无序比较 就是这一步,等会慢慢讲 SETEQ CL ; CL寄存器中存入ZF标志位 SETPC AL ; AL寄存器中存入PF标志位 奇偶标志位 // 参照SETNP指令 后来发现其实不是取PF,而是取PF反后放入AL寄存器 ANDL AX, CX ; 按位与运算 MOVB CL, "".~r1+16(SP) ; 与运算结果放入返回值的地址上 RET ; 返回 ``` 比较的关键在<code>UCOMISD</code>指令的作用上。 无序比较操作符 uncomisd 的作用如下: > #### (V)UCOMISD (all versions)[ ¶](https://www.felixcloutier.com/x86/ucomisd#-v-ucomisd--all-versions-) > > ``` > RESULT← UnorderedCompare(DEST[63:0] <> SRC[63:0]) { > (* Set EFLAGS *) CASE (RESULT) OF > UNORDERED: ZF,PF,CF←111; > GREATER_THAN: ZF,PF,CF←000; > LESS_THAN: ZF,PF,CF←001; > EQUAL: ZF,PF,CF←100; > ESAC; > OF, AF, SF←0; } > ``` 由此可见,当为无序时,也就是出现一个比较操作符是NaN时, ZF, PF, CF都为1, ZF AND PF == 1。 其他情况,ZF AND PF == 0, 这就做出了区分. 将按位与结果CL放入放回值地址,return函数。ps. 为嘛用0代表true, 感觉不对啊。 而且普通数比较相等还是不相等也没区分啊???所以上面一定有遗漏。 ```go package main import "fmt" import "unsafe" func main() { a := true c := *(*byte)(unsafe.Pointer(&a)) fmt.Printf("%d", c) } ``` Ouput : ```go 1 ``` 事实证明true是在内存里为1的。 最后我把错误锁定在了对 setpc 指令的理解上。 SETPC ,也就是普通汇编中SETNP这个指令,是当PF寄存器为0时,取值才为1. 所以最后CL寄存器中的值应该为<code>ZF AND NOT PF</code>, 所以在无序比较中一旦有NaN存在,CL的值<code>1 and 0</code>就为0, 正常比较是不相等<code>0 and 1</code>c也是0, 只有相等时<code>1 and 1</code>才为1. ## 结语 NaN 是由 IEEE 754 标准定义的, 其比较是由底层汇编 UCOMISD 完成了。 该指令考虑了无序(存在NaN的情况)时的比较。 ## 参考文献: https://www.felixcloutier.com/x86/ucomisd https://quasilyte.dev/blog/post/go-asm-complementary-reference/ (GO 汇编和其他汇编的对照表) https://software.intel.com/sites/default/files/managed/39/c5/325462-sdm-vol-1-2abcd-3abcd.pdf (查找SETNP指令) ## 摘自 https://github.com/GuoYuefei/Blog/issues/12

有疑问加站长微信联系

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:812540095

105 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传