内容简介:我在思考编译器是如何保护我们写的代码。无效的内存访问检查是编译器添加到代码中的一种安全检查。我们可能会认为这种“额外的代码”会损耗程序的性能,甚至可能需要数十亿的迭代操作。但是,这些检查可以防止代码对正在运行的系统造成损害。编译器本质上是指出和查找错误,使我们编写的代码在运行时更安全。基于上述考虑,同时 Go 语言想要达成更快的编译速度,如果硬件可以解决这些问题,那么 Go 语言编译器就会使用硬件来解决问题。其中一种情况是检测无效的内存访问。有时编译器会在代码中添加 nil 指针检查,而有时不会。在这篇博
简介
我在思考编译器是如何保护我们写的代码。无效的内存访问检查是编译器添加到代码中的一种安全检查。我们可能会认为这种“额外的代码”会损耗程序的性能,甚至可能需要数十亿的迭代操作。但是,这些检查可以防止代码对正在运行的系统造成损害。编译器本质上是指出和查找错误,使我们编写的代码在运行时更安全。
基于上述考虑,同时 Go 语言想要达成更快的编译速度,如果硬件可以解决这些问题,那么 Go 语言编译器就会使用硬件来解决问题。其中一种情况是检测无效的内存访问。有时编译器会在代码中添加 nil 指针检查,而有时不会。在这篇博客中,我们将探讨一种情况,即编译器在什么情况下让硬件来检测无效的内存访问,以及在什么情况下会添加 nil 指针检查。
硬件只作检查
当编译器依赖于硬件来检查并指出无效的内存访问时,编译器可以生成更少的代码,以提高程序性能。如果我们的代码尝试读取或写入地址 0x0,则硬件将抛出一个异常,该异常将被 Go 运行时捕获并以 panic
的形式反馈给我们的程序。如果 panic
没有恢复,则产生堆栈跟踪信息。
如下是一个尝试对 0x0 内存地址进行写入数据示例,以及产生的 panic
和堆栈跟踪信息:
01 package main 02 03 func main() { 04 var p *int // 声明一个 nil 值指针 05 *p = 10 // 将 10 写入地址 0x0 06 } panic: runtime error: invalid memory address or nil pointer dereference [signal 0xb code=0x1 addr=0x0 pc=0x2007] goroutine 16 [running]: runtime.panic(0x28600, 0x51744) /go/src/pkg/runtime/panic.c:279 +0xf5 main.main() /Go/Projects/src/github.com/goinaction/code/temp/main.go:5 +0x7 goroutine 17 [runnable]: runtime.MHeap_Scavenger() /go/src/pkg/runtime/mheap.c:507 runtime.goexit() /go/src/pkg/runtime/proc.c:1445
让我们看看由 6g 编译器在 darwin/amd64 机器上的生成的汇编代码(译者注:6g 是 Golang 在 amd64 架构机器上的编译器,由于 Go 语言的版本关系,1.5 之后的版本中 6g/8g 已被取代,应该统一使用 go tool compile -S main.go
):
go tool 6g -S main.go 04 var p *int 0x0004 00004 (main.go:4) MOVQ $0,AX 05 *p = 10 0x0007 00007 (main.go:5) NOP , 0x0007 00007 (main.go:5) MOVQ $10,(AX)
在上面的汇编代码片段中,我们看到 0 的值被分配给 AX 寄存器,然后代码尝试将值 10 写入 AX 寄存器指向的存储器。这会产生 panic 以及相应的堆栈跟踪信息。上述汇编代码中没有显示 nil 指针检查,因为编译器已将其交给硬件进行检测和报告。
编译器检查
让我们看一下编译器生成 nil 指针检查的示例:
01 package main 02 03 type S struct { 04 b [4096]byte 05 i int 06 } 07 08 func main() { 09 s := new(S) 10 s.i++ 11 }
在第 3 行到第 6 行,我们定义了 S 的结构类型,这个类型有两个字段。第一个字段是 4096 字节的数组,第二个字段是一个整数。然后在第 9 行的 main 函数中,我们创建一个 S 类型的值,并将该值的地址分配给名为 s 的指针变量。最后在第 10 行,我们将 s 实例的 i 字段的值递增 1。
让我们看看由 6g 编译器在 darwin/amd64 机器上生成的第 9 行、第 10 行的汇编代码(译者注:同上译注):
go tool 6g -S main.go 09 s := new(S) 0x0036 00054 (main.go:9) LEAQ "".autotmp_0001+0(SP),DI 0x003a 00058 (main.go:9) MOVL $0,AX 0x003c 00060 (main.go:9) MOVQ $513,CX 0x0043 00067 (main.go:9) REP , 0x0044 00068 (main.go:9) STOSQ , 0x0046 00070 (main.go:9) LEAQ "".autotmp_0001+0(SP),BX 10 s.i++ 0x004a 00074 (main.go:10) CMPQ BX,$0 0x004e 00078 (main.go:10) JEQ $1,105 0x0050 00080 (main.go:10) MOVQ 4096(BX),BP 0x0057 00087 (main.go:10) NOP , 0x0057 00087 (main.go:10) INCQ ,BP 0x005a 00090 (main.go:10) MOVQ BP,4096(BX) 0x0061 00097 (main.go:10) NOP ,
在我们的示例中,第 10 行代码需要下面的指针运算才能使其工作 :
10 s.i++ 0x004a 00074 (main.go:10) CMPQ BX,$0 0x004e 00078 (main.go:10) JEQ $1,105 0x0050 00080 (main.go:10) MOVQ 4096(BX),BP 0x0057 00087 (main.go:10) NOP , 0x0057 00087 (main.go:10) INCQ ,BP 0x005a 00090 (main.go:10) MOVQ BP,4096(BX)
i 字段内存地址位于 S 类型的值内的 4096 字节偏移量。上面汇编代码中,BP 寄存器被分配到 BX (s 值 ) 的内存位置的值加上 4096 的偏移量(译者注:基地址加上偏移量)。之后 BP 的值增加 1,新值分配在 BX + 4096 的内存地址中。(译者注:0x0050 0008 至 0x005a 00090 代码片段)
在这个例子中,Go 编译器在自增代码之前添加一个 nil 指针检查:
10 s.i++ 0x004a 00074 (main.go:10) CMPQ BX,$0 0x004e 00078 (main.go:10) JEQ $1,105 0x0050 00080 (main.go:10) MOVQ 4096(BX),BP 0x0057 00087 (main.go:10) NOP , 0x0057 00087 (main.go:10) INCQ ,BP 0x005a 00090 (main.go:10) MOVQ BP,4096(BX)
上述高亮代码(译者注:0x004a 00074 至 0x004e 00078 代码片段,详见原文)代码显示了编译器添加的 nil 指针检查。将 BX 的值与 0 的值进行比较,如果它们相等,代码不会执行下面的自增代码片段。
问题是,为什么 Go 在这个例子中添加了一个 nil 指针检查,而第一个例子中却没有添加?
添加 nil 检查的情形
让我们对先前的例子做一点小修改:
01 package main 02 03 type S struct { 04 i int // 交换字段的顺序 05 b [4096]byte 06 } 07 08 func main() { 09 s := new(S) 10 s.i++ 11 }
我所做的只是交换结构中的字段顺序。这次 int 类型的 i 字段在 4096 个元素的字节数组类型的 b 字段之前。现在让我们生成汇编代码,看看 nil 指针检查是否仍然存在:
09 s := new(S) 0x0036 00054 (main.go:9) LEAQ "".autotmp_0001+0(SP),DI 0x003a 00058 (main.go:9) MOVL $0,AX 0x003c 00060 (main.go:9) MOVQ $513,CX 0x0043 00067 (main.go:9) REP , 0x0044 00068 (main.go:9) STOSQ , 0x0046 00070 (main.go:9) LEAQ "".autotmp_0001+0(SP),BX 10 s.i++ 0x004a 00074 (main.go:10) NOP , 0x004a 00074 (main.go:10) MOVQ (BX),BP 0x004d 00077 (main.go:10) NOP , 0x004d 00077 (main.go:10) INCQ ,BP 0x0050 00080 (main.go:10) MOVQ BP,(BX) 0x0053 00083 (main.go:10) NOP ,
如果将第 10 行的未修改的示例汇编代码与刚才修改了的(交换字段后的)示例进行比较,你会发现有很大的区别:
First Example | Second Example CMPQ BX,$0 | NOP JEQ $1,105 | MOVQ (BX),BP MOVQ 4096(BX),BP | NOP , NOP , | INCQ ,BP INCQ ,BP | MOVQ BP,(BX) MOVQ BP,4096(BX) | NOP ,
当我们将整数字段移动到结构中的第一位,nil 指针检查就消失了。现在 Go 再次让硬件来完成 nil 指针检查,并且报告无效的内存访问。
原因是 Go 可以信任硬件来识别和报告来自可能存在于 0x0 和 0xFFF 地址间的无效内存访问。编译器不会在这些情况下添加检查。但是当代码如果处理超出 4k 范围的地址,nil 指针检查就会被加入到汇编代码中。
在之前的例子中,当字节数组首先出现在结构体中时,整数字段的内存会分配超过 4k 边界的偏移量。这会导致编译器为整数字段添加 nil 指针检查。当整数字段是第一个时,编译器将其留给硬件进行检测,因为(内存)地址在 4k 范围内。
总结
我不相信编写专注于性能的代码。通常我们编写惯用、简洁的代码,然后对程序进行基准测试,并根据需要进行性能调整。正如我们所看到的,结构体的设计会对程序的性能有一定的影响。访问结构体中的字段需要进行指针运算,对于大于 4K 的结构体,你组织字段的方式可能会有所不同。
需要注意的是,我们看到的是编译器实现细节,而不是 Go 规范的一部分。这可能会随着 Go 的新版本发布而发生改变,或者 5g、6g 或 8g 编译器之间存在差异。在对实现细节进行性能调优时要小心,每一个新版本都需要重新检查这些细节。有趣的是,当编译器可以信任硬件时,它将如何使我们的代码更安全。
扩展材料
如果你想在标准库中阅读一些关于重新 排序 结构以消除字段的 nil 指针检查的示例,请看 Nigel Tao 的代码更改:https://codereview.appspot.com/6625058
此外还有 Russ Cox 于 2013 年 7 月撰写的一份文档 ,"Go 1.2 字段选择器和 nil 检查 ":http://golang.org/s/go12nil
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- NULL 指针、零指针、野指针
- 将数组和矩阵传递给函数,作为C中指针的指针和指针
- C语言指针数组和数组指针
- python(函数指针和类函数指针)
- C++ 基类指针和派生类指针之间的转换
- LLVM接受NVIDIA的“f18” Fortran编译器作为官方Fortran编译器
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Masterminds of Programming
Federico Biancuzzi、Chromatic / O'Reilly Media / 2009-03-27 / USD 39.99
Description Masterminds of Programming features exclusive interviews with the creators of several historic and highly influential programming languages. Think along with Adin D. Falkoff (APL), Jame......一起来看看 《Masterminds of Programming》 这本书的介绍吧!
图片转BASE64编码
在线图片转Base64编码工具
html转js在线工具
html转js在线工具