当前位置: 首页 > 科技观察

这个围棋边界检查简直快把人逼疯了~

时间:2023-03-18 21:20:02 科技观察

1.什么是边界检查?边界检查,英文名称为BoundsCheckElimination,简称BCE。它是Go语言中的一种检查方法,用于防止数组和切片越界导致的内存不安全。如果校验下标越界,就会产生Panic。边界检查让我们的代码可以安全运行,但另一方面,它也让我们的代码运行效率略有下降。比如下面的代码会进行三次边界检查packagemainfuncf(s[]int){_=s[0]//第一次检查_=s[1]//第二次检查_=s[2]//检查第三次}funcmain(){}你可能想知道,三次?我怎么知道它会检查三遍。其实编译的时候只要加上参数就可以了,命令如下$gobuild-gcflags="-d=ssa/check_bce/debug=1"main.go#command-line-arguments./main.go:4:7:FoundIsInBounds./main.go:5:7:FoundIsInBounds./main.go:6:7:FoundIsInBounds2。边界检查的条件?并非所有对数组和切片的索引操作都需要边界检查。比如下面的例子,不需要进行边界检查,因为编译器已经根据上下文知道了slices的长度和你的终止索引,可以立即判断是否越界,所以不需要tocheck执行边界检查,因为在编译的时候就已经知道这个地方会不会panic了。packagemainfuncf(){s:=[]int{1,2,3,4}_=s[:9]//不需要边界检查}funcmain(){}所以可以断定,在执行过程中无法判断编译阶段索引操作是否会越界会需要进行边界检查,比如packagemainfuncf(s[]int){_=s[:9]//需要进行边界检查}funcmain(){}3.边界的特殊情况checking3.1Cases-在下面的示例代码中,由于索引2在前面已经检查过是否会越界,因此智能编译器可以推断出后面的索引0和1不需要再次检查packagemainfuncf(s[]int){_=s[2]//检查一次_=s[1]//不会检查_=s[0]//不会检查}funcmain(){}3.2情况2在下面的例子中,它从逻辑上可以保证不会越界的代码也不会进行越界检查。packagemainfuncf(s[]int){forindex,_:=ranges{_=s[index]_=s[:index+1]_=s[index:len(s)]}}funcmain(){}3.3案例3、在下面的示例代码中,虽然可以确定数组的长度和容量,但索引是通过rand.Intn()函数得到的随机数。从编译器的角度看,索引值是不确定的,有可能大于数组。长度也可以小于数组的长度。因此,需要进行第一次检查。第一次检查后,可以逻辑推断出第二个索引,所以不会进行边界检查。packagemainimport("math/rand")funcf(){s:=make([]int,3,3)index:=rand.Intn(3)_=s[:index]//先check_=s[index:]//不会检查}funcmain(){}但是如果上面的代码稍微改动一下,让slice的长度和容量不同,结果又会不一样。packagemainimport("math/rand")funcf(){s:=make([]int,3,5)index:=rand.Intn(3)_=s[:index]//先check_=s[index:]//第二次检查}funcmain(){}只有数组的长度和容量相等,且:index成立,才能确定index:也成立。在这种情况下,我们只需要检查一次数组的长度和容量不相等,那么索引在编译器眼中可能大于数组的长度,甚至大于数组的容量大批。我们假设通过index得到的随机数是4,那么它大于数组的长度。这时候虽然s[:index]可以成功,但是s[index:]会失败,所以需要进行第二次边界检查。你可能会说,index3不就是最大值吗?怎么可能是4?要知道编译器在编译的时候并不知道index的最大值是3。综上所述,当数组的长度和容量相等时,s[:index]的建立可以保证s[index:]的建立,因为它只需要检查一次。当数组的长度和容量不相等时,s[:index]的建立不能保证s[index:]也为真,因为需要检查两次才可以。3.4案例4有了上面的铺垫,我们再来看下面的例子。由于数组是调用者传入的参数,所以编译器在编译的时候是获取不到的。数组的长度和容量是否相等是已知的,所以我们只能确定并检查两者。packagemainimport("math/rand")funcf(s[]int,indexint){_=s[:index]//firstcheck_=s[index:]//secondcheck}funcmain(){}但如果顺序两个表达式颠倒了,只需要检查一次,原因我就不多说了。packagemainimport("math/rand")funcf(s[]int,indexint){_=s[index:]//先检查_=s[:index]//不检查}funcmain(){}5.积极消除边界检查虽然编译器已经非常努力地消除了一些应该消除的边界检查,但不可避免地会出现一些遗漏。这需要“警民合作”。对于那些编译器没有考虑到,但开发者又在为程序的效率而努力的场景,他可以通过一些技巧来给出一些提示,告诉编译器什么事情不应该做。边界检查。比如下面这个例子,从代码的逻辑来看,根本不需要做边界检查,只是编译器没有那么聪明。实际上,它需要为每个for循环做一次边界检查,这是一种性能上的浪费。packagemainfuncf(is[]int,bs[]byte){iflen(is)>=256{for_,n:=rangebs{_=is[n]//对每个循环进行边界检查}}}funcmain(){}你可以试试在for循环前加这么一句is=is[:256],告诉编译器新的is的长度是256,最大索引值是255,不会超过byte的最大值,因为is[n]从逻辑上讲一定不能越界。packagemainfuncf(is[]int,bs[]byte){iflen(is)>=256{is=is[:256]for_,n:=rangebs{_=is[n]//不需要做边界检查}}}funcmain(){}6.写在最后上面列举的例子并没有涵盖标准编译器支持的边界检查消除的所有情况。本文仅列出一些常见的场景。虽然标准编译器中的边界检查消除功能仍不是100%完美,但它确实适用于许多常见情况。自标准编译器支持以来,此功能随着每个版本更新而不断改进和增强。不用说,在未来的版本中,标准编译器会变得更加智能,因此上面第五个示例中提供给编译器的提示可能变得不必要。感谢Go语言开发团队的出色工作!7.参考文档https://gfw.go101.org/article/bounds-check-elimination.html大哥。转载本文请联系围棋编程时间公众号。