文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

聊聊内存中的Slice操作

2024-12-02 23:28

关注

本文主要关注 slice 的相关操作:

  1. 元素赋值(修改)
  2. make
  3. copy
  4. make and copy
  5. append

环境

  1. OS : Ubuntu 20.04.2 LTS; x86_64 
  2. Go : go version go1.16.2 linux/amd64 

声明

操作系统、处理器架构、Golang版本不同,均有可能造成相同的源码编译后运行时内存地址、数据结构不同。

本文仅保证学习过程中的分析数据在当前环境下的准确有效性。

代码清单

  1. package main 
  2.  
  3. import "fmt" 
  4.  
  5. func main() { 
  6.     var src = []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} 
  7.     src[3] = 100 
  8.     //src[13] = 200 
  9.     dst := makeSlice() 
  10.     makeSliceCopy(src) 
  11.     growSlice(src) 
  12.     copySlice(dst, src) 
  13.     sliceStringCopy([]byte("hello world"), "hello slice"
  14.  
  15. //go:noinline 
  16. func sliceStringCopy(slice []byte, s string) { 
  17.     copy(slice, s) 
  18.     PrintInterface(string(slice)) 
  19.  
  20. //go:noinline 
  21. func copySlice(dst []int, src []int) { 
  22.     copy(dst, src) 
  23.     PrintInterface(dst) 
  24.  
  25. //go:noinline 
  26. func growSlice(slice []int) { 
  27.     slice = append(slice, 11) 
  28.     PrintInterface(slice) 
  29.  
  30. //go:noinline 
  31. func makeSliceCopy(array []int) { 
  32.     slice := make([]int, 5) 
  33.     copy(slice, array) 
  34.     PrintInterface(slice) 
  35.  
  36. //go:noinline 
  37. func makeSlice() []int { 
  38.     slice := make([]int, 5) 
  39.     //slice := make([]int, 5, 10) 
  40.     //slice := make([]int, 10, 5) // "len larger than cap in make(%v)" 
  41.     return slice 
  42.  
  43. //go:noinline 
  44. func PrintInterface(v interface{}) { 
  45.     fmt.Println("it =", v) 

深入内存

1. 元素赋值

该操作很简单,直接通过偏移量定位元素内存并赋值,对应一条机器指令:


如果如下元素索引超过runtime.slice.cap, 则会panic。

  1. src[13] = 200 

查看可执行程序,Golang编译器发现代码异常之后,直接使用runtime.panicIndex函数调用替换了元素赋值及之后的所有操作,退出程序。


这很令人好奇:明明编译时期发现了代码逻辑错误,但并没有终止编译过程,而是把它变成一个运行时异常。难道运行时异常更好吗?

针对这个问题暂时没有找到合理的答案,只能猜测这是编译器为了应对各种代码场景的一个通用编译处理逻辑,而不是仅仅为了处理本例中的情况。

2. make

使用make关键字动态创建 slice。编译之后make会变成什么指令,视情况而定。

代码清单中第42行的makeSlice函数编译之后,对应的机器指令如下:


可以看到,make关键字编译之后,变成了 runtime.makeslice 函数调用,其实现如下:

  1. func makeslice(et *_type, len, cap int) unsafe.Pointer { 
  2.     // 计算需要分配的内存字节数 
  3.     mem, overflow := math.MulUintptr(et.size, uintptr(cap)) 
  4.     if overflow || mem > maxAlloc || len < 0 || len > cap { 
  5.         mem, overflow := math.MulUintptr(et.size, uintptr(len)) 
  6.         if overflow || mem > maxAlloc || len < 0 { 
  7.             panicmakeslicelen() 
  8.         } 
  9.         panicmakeslicecap() 
  10.     } 
  11.  
  12.     // 直接分配内存 
  13.     return mallocgc(mem, et, true

以上代码非常简单,有几个判断条件稍微解释下:

(1)overflow表示元素大小和元素数量的乘积是否溢出,即是否大于64位无符号整数的最大值,肯定是不能大于的;

(2)maxAlloc的值为 0x1000000000000,实际上大多数64位处理器和操作系统的内存可寻址范围并不是64位,而是不超过48位,这是Golang一个内存分配和校验逻辑;

(3)len>cap时,Golang编译器会进行检查 ,编译失败。

另外,在Golang源码中,有个 runtime.makeslice64 函数,并没有出现在编译后的可执行程序中。在 Go 编译器代码中看到应该是和32位程序编译相关。我们更关心64位程序,所以不再深究。

3. copy

代码清单中第23行的copySlice函数编译之后,对应的机器指令如下:


将其翻译为Golang伪代码,大意如下:

  1. func copySlice(dst []int, src []int) { 
  2.     n := len(dst) 
  3.     if n  > len(src) { 
  4.         n = len(src) 
  5.     } 
  6.     if &dst[0] != &src[0] { 
  7.         runtime.memmove(&dst[0], &src[0], len(dst)*8) 
  8.     } 
  9.     PrintInterface(dst) 

仔细阅读以上指令代码,确定其逻辑与 runtime.slicecopy 函数相匹配,也就是说copy关键字编译之后变成了runtime.slicecopy函数调用。但是编译器对runtime.slicecopy函数进行了内联优化,所以最终并不能看到直接的runtime.slicecopy函数调用。

在Golang中,copy关键字可以用于把 string 对象拷贝到[]byte对象中;因为字符串类型还没有学习到,所以暂时搁置这种特殊情况。

4. make and copy

当make和copy两个关键字一起使用时,又发生了新变化。

代码清单中第35行的makeSliceCopy函数编译之后,对应的机器指令如下:


可以清楚的看到,当make和copy两个关键字一起使用时,被Golang编译器合并成了 runtime.makeslicecopy 函数调用。该函数源代码逻辑非常清晰,此处不再赘述。

5. append

代码清单中第29行的growSlice函数对已经满的 slice 进行 append 操作。

编译之后,对应的机器指令如下:


以上代码逻辑是:首先进行len(slice)+1和cap(slice)比较,对已经满的 slice 进行 append 操作时,将触发底层数组的长度扩增(分配新的数组),将其翻译为Golang伪代码,大意如下:

  1. func growSlice(slice []int) { 
  2.     if len(slice) + 1 > cap(slice) { 
  3.         slice = runtime.growslice(element_type_pointer, slice, 11) 
  4.     } 
  5.     // cap(slice) == 20 
  6.     slice[len(slice)] = 17 
  7.     PrintInterface(slice) 

runtime.growslice 函数的功能是:slice 进行append操作时,如果该slice已经满了,调用该函数重新分配底层数组进行扩容。

在本例中,

  1. 原 slice 的容量是10,调用runtime.growslice函数之后,容量变为20。
  2. slice元素是 int 类型(element_type_pointer),关于该类型的分析可以阅读内存中的整数 。

通过以上学习研究,对slice的各种操作有了本质上的了解,相信用起来更加得心应手。

本文转载自微信公众号「Golang In Memory」

 

来源:Golang In Memory内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯