• go slice切片的详细知识(包含底层扩容)——2


    目录

    例子

    例3:使用append逐个添加元素和一次性添加多个元素的区别

    例4:order[low:high:max]

    例5:当容量大于1024的时候,每次扩容真的是1.25倍吗?


    本文是对上一篇文章的补充:

    go slice切片的详细知识(包含底层扩容)-CSDN博客


    例子

    例3:使用append逐个添加元素和一次性添加多个元素的区别

    一次性添加多个元素:slice = append(slice, 1, 2, 3)

    • 性能:可能更高效,因为只进行了一次内存分配和复制操作。
    • 代码简洁:代码更简洁明了。

    逐个添加元素:slice = append(slice, 1)、slice = append(slice, 2)、slice = append(slice, 3) 

    • 性能:可能较低,因为每次添加元素时,切片可能需要多次进行内存分配和复制操作(取决于底层数组的容量)。
    • 内存分配:可能会多次触发内存分配操作。
    • 代码冗长:代码较为冗长,不如一次性添加多个元素的方式简洁。
    1. // 示例 1: 一次性添加多个元素
    2. var s1 []int
    3. s1 = append(s1, 1, 2, 3)
    4. fmt.Printf("%p %v %d %d\n", s1, s1, len(s1), cap(s1)) // 0xc000020090 [1 2 3] 3 3
    5. var slice1 []int = []int{1, 2}
    6. fmt.Printf("%p %v %d %d\n", slice1, slice1, len(slice1), cap(slice1)) // 0xc000128010 [1 2] 2 2
    7. slice1 = append(slice1, 3, 4, 5)
    8. fmt.Printf("%p %v %d %d\n", slice1, slice1, len(slice1), cap(slice1)) // 0xc000120060 [1 2 3 4 5] 5 6
    9. // 示例 2: 逐个添加元素
    10. var slice2 []int = []int{1, 2}
    11. fmt.Printf("%p %v %d %d\n", slice2, slice2, len(slice2), cap(slice2)) // 0xc000128060 [1 2] 2 2
    12. slice2 = append(slice2, 3)
    13. fmt.Printf("%p %v %d %d\n", slice2, slice2, len(slice2), cap(slice2)) // 0xc00012c020 [1 2 3] 3 4
    14. slice2 = append(slice2, 4)
    15. fmt.Printf("%p %v %d %d\n", slice2, slice2, len(slice2), cap(slice2)) // 0xc00012c020 [1 2 3 4] 4 4
    16. slice2 = append(slice2, 5)
    17. fmt.Printf("%p %v %d %d\n", slice2, slice2, len(slice2), cap(slice2)) // 0xc00012e000 [1 2 3 4 5] 5 8

    内容最终相同,但它们的内存分配情况可能不同。多次调用 append 可能会导致多次内存分配,而一次性添加多个元素则可能只需要进行一次内存分配和复制。

    例4:order[low:high:max]

    切片在被截取时的另一个特点是,被截取后的数组仍然指向原始切片的底层数据。

    如:bar 执行了 append 函数之后,最终也修改了 foo 的最后一个元素,这是一个在实践中非常常见的陷阱。

    1. foo := []int{0, 0, 0, 42, 100}
    2. bar := foo[1:4]
    3. fmt.Println(len(foo), cap(foo), foo) // 5 5 [0 0 0 42 100]
    4. fmt.Println(len(bar), cap(bar), bar) // 3 4 [0 0 42]
    5. fmt.Printf("%p %p\n", foo, bar) // 0xc00001c1b0 0xc00001c1b8 虽然地址不同(切片结构体还有长度和容量属性,所以切片结构体地址不同),但是指向的底层数组是同一个,因为没有扩容
    6. bar = append(bar, 99)
    7. fmt.Println(len(foo), cap(foo), foo) // 5 5 [0 0 0 42 99]
    8. fmt.Println(len(bar), cap(bar), bar) // 4 4 [0 0 42 99]

    如果要解决这样的问题,其实可以在截取时指定容量:order[low:high:max]

    1. foo := []int{0, 0, 0, 42, 100}
    2. bar := foo[1:4:4]
    3. // bar := foo[1:4:3] // 报错:Invalid index values, must be low <= high <= max
    4. fmt.Println(len(foo), cap(foo), foo) // 5 5 [0 0 0 42 100]
    5. fmt.Println(len(bar), cap(bar), bar) // 3 3 [0 0 42]
    6. fmt.Printf("%p %p\n", foo, bar) // 0xc00001c1b0 0xc00001c1b8
    7. bar = append(bar, 99)
    8. fmt.Println(len(foo), cap(foo), foo) // 5 5 [0 0 0 42 100]
    9. fmt.Println(len(bar), cap(bar), bar) // 4 6 [0 0 42 99]

    解释foo[1:4:4]:

    • low 是 1,表示新切片从 foo 的索引 1 开始(包含这个元素)。
    • high 是 4,表示新切片到 foo 的索引 4 结束(不包含这个元素)。
    • max 是 4,表示新切片的容量是从 low 开始到 max 结束的长度。

    练习:

    1. sliceA := make([]int, 5, 10)
    2. sliceB := sliceA[0:5]
    3. sliceC := sliceA[0:5:5]
    4. fmt.Println(len(sliceA), cap(sliceA)) // 5 10
    5. fmt.Println(len(sliceB), cap(sliceB)) // 5 10
    6. fmt.Println(len(sliceC), cap(sliceC)) // 5 5
    1. orderLen := 5
    2. order := make([]uint16, 2*orderLen)
    3. pollorder := order[:orderLen:orderLen]
    4. lockorder := order[orderLen:][:orderLen:orderLen]
    5. // pollorder切片指的是order的前半部分切片,lockorder指的是order的后半部分切片,即原order分成了两段。所以,pollorder和lockerorder的长度和容量都是5。
    6. fmt.Println(len(pollorder), cap(pollorder)) // 5 5
    7. fmt.Println(len(lockorder), cap(lockorder)) // 5 5
    1. sli := make([]int, 0)
    2. sli = append(sli, []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}...)
    3. s := sli[5:][:5] // sli 和 s 共享同一个底层数组
    4. // sli[5:]:[6, 7, 8, 9, 10]
    5. // [:5]:从上述新切片中再取前5个元素,结果是[6, 7, 8, 9, 10]
    6. fmt.Println(s) // [6 7 8 9 10]
    7. s[0] = 111
    8. fmt.Println(s, sli) // [111 7 8 9 10] [1 2 3 4 5 111 7 8 9 10]

    例5:当容量大于1024的时候,每次扩容真的是1.25倍吗?

    1024 直接扩容到 1536,不是1.25倍,而是1.5倍,这是为什么?(对于容量大于等于 1024 的切片,在扩容时 Go 并不是简单地按照1.25倍扩容,而是使用了一种更复杂的策略。)

    1. s2 := make([]int, 1024)
    2. fmt.Printf("s2: len: %d, cap: %d\n", len(s2), cap(s2)) // s2: len: 1024, cap: 1024
    3. s2 = append(s2, 1)
    4. fmt.Printf("s2: len: %d, cap: %d\n", len(s2), cap(s2)) // s2: len: 1025, cap: 1536

    向 slice 追加元素的时候,若容量不够,会调用 growslice 函数:

    1. func growslice(et *_type, old slice, cap int) slice {
    2. // ……
    3. for 0 < newcap && newcap < cap {
    4. // Transition from growing 2x for small slices
    5. // to growing 1.25x for large slices. This formula
    6. // gives a smooth-ish transition between the two.
    7. newcap += (newcap + 3*threshold) / 4
    8. }
    9. // ……
    10. capmem = roundupsize(uintptr(newcap) * ptrSize)
    11. newcap = int(capmem / ptrSize)
    12. }

    for循环:会不断循环直到 newcap 超过所需的容量。最终的结果可能会比严格的1.25倍略大一些,具体取决于当前的容量和内存分配的优化策略。

    最后两行代码:对 newcap 作了一个内存对齐,这个和内存分配策略相关,所以最终结果不一定是 1.25的整数倍(有时候扩容和元素类型的字节数有关系)。

    Go 语言的切片扩容机制是相当复杂的,它考虑了多种因素来确定新的容量,以便在性能和内存使用之间找到平衡。Go 语言的 runtime 库在执行切片扩容时,有时会为了减少频繁的内存分配而使用稍大的倍数。这种优化主要是为了减少内存分配次数,提高性能。

    Go 语言中切片扩容的策略为:

    • 如果新申请容量(cap)大于旧容量(old.cap)的两倍,则最终容量(newcap)是新申请的容量(cap);
    • 如果旧切片的长度小于 1024,则最终容量是旧容量的 2 倍,即“newcap=doublecap”;
    • 如果旧切片的长度大于或等于 1024,则最终容量从旧容量开始循环增加原来的 1/4,直到最终容量大于或等于新申请的容量为止;
    • 如果最终容量计算值溢出,即超过了 int 的最大范围,则最终容量就是新申请容量。

  • 相关阅读:
    Python闯LeetCode--第2题:两数相加
    umich cv-2-2
    无损剪切音视频文件的跨平台工具: LosslessCut | 开源日报 0908
    教你注册chrome开发者账号,并发布chrome浏览器插件。
    1-Redis架构设计到使用场景-四种部署运行模式(上)
    Maven中央仓库地址大全
    2023全国大学生软件测试大赛开发者测试练习题99分答案(ScapegoatTree2023)
    C语言 每日一题 PTA 10.21-10.24日 day3
    使用easyexcel模板导出的两个坑(Map空数据列错乱和不支持嵌套对象)
    【QT】QAbstractItemView的选择模式(SelectionMode)
  • 原文地址:https://blog.csdn.net/m0_72874409/article/details/139395602