当前位置: 首页 > article >正文

Go 语言的slice是如何扩容的?

Go 语言中的 slice 是一种灵活、动态的视图,是对底层数组的抽象。当对 slice 进行追加元素等操作导致其长度超过容量时,就会发生扩容。

一、扩容的基本原理

当 slice 需要扩容时,Go 语言会根据当前的容量来确定新的容量。一般来说,新的容量通常是原容量的 2 倍。例如,如果一个 slice 的容量是 10,那么在扩容后,新的容量会变成 20。这种扩容策略使得 slice 的容量能够快速增长,以满足不断添加元素的需求。

但是,当 slice 的容量超过 1024 时,扩容的策略会有所改变。新的容量会增加原容量的一半。比如,原容量是 2048,扩容后的新容量会是 2048 + 2048 / 2 = 3072。这种策略可以避免在容量很大时,一次性分配过多的内存,从而减少内存浪费。

上面这种扩容机制是一种很常见的说法, 但是有时候, 我们还需要内存对齐, 所以上面这个扩容机制应该变成大于1.25倍和2倍

// 注意返回的是一个新的切片
func growslice(et * _type, old slice, cap int) slice { // ......
    newcap: = old.cap
    doublecap: = newcap + newcap
    if cap > doublecap {
        newcap = cap
    } else {
    	//小于1024
        if old.len < 1024 {
            // 扩容两倍
            newcap = doublecap
        } else {
        	// 如果小于newcap
            for 0 < newcap && newcap < cap {
                newcap += newcap / 4
            }
            if newcap <= 0 {
                newcap = cap
            }
        }
    }
    // 内存对齐
    capmem = roundupsize(capmem)
    newcap = int(capmem / et.size) // ......
}

内存分配和数据复制

在确定了新的容量后,Go 语言会分配一块新的内存空间来存储底层数组。这块新内存的大小是根据新的容量来确定的,其元素类型与原 slice 底层数组的元素类型相同。

然后,会将原底层数组中的数据复制到新的内存空间中。这个复制过程是逐个元素进行的,确保数据的完整性和顺序不变。例如,原 slice 中有 [1, 2, 3, 4] 这些元素,扩容后,这些元素会被复制到新的底层数组中,仍然保持 [1, 2, 3, 4] 的顺序。

最后,slice 的指针会指向新的底层数组,长度和容量等属性也会相应更新。原来的底层数组所占用的内存可能会被垃圾回收机制回收,除非还有其他的变量引用它。

我们会写一个具体的例子来看:

package main

import "fmt"

func main() {
    s: = [] int {
        1, 2
    }
    s = append(s, 4, 5, 6)
    fmt.Printf("len=%d, cap=%d", len(s), cap(s))
}

通过答应上面这个的结果cap 的结果是6, 如果按照原来的说法应该是这样的:

  1. 小于1024, 添加第一个元素4, cap 扩容两倍, 实际是2*2=4
  2. 添加5的时候, 不需要进行扩容,
  3. 添加6的时候, 扩容两倍变成8, 但是结果却是6

但是这种计算过程是错误的:

// 其中cap当前是5
growslice(et * _type, old slice, cap int) 

cap > doublecap =old.cap * 2 = 4 所以目前的cap 是为5

然后会执行对应的内存对齐函数:

capmem = roundupsize(uintptr(newcap) * sys.PtrSize) // sys.PtrSize 大小为 8。
class_to_size[size_to_class8[(size+smallSizeDiv-1)/smallSizeDiv]]

其中smallsizediv 的大小是8, 然后size=40 最终得到的结果是对应的6值

二、扩容的性能影响

时间开销

扩容操作涉及到内存分配和数据复制,这会消耗一定的时间。内存分配的时间开销取决于操作系统和当前的内存状况。在内存充足的情况下,分配内存的速度相对较快。但是,如果系统内存紧张,分配内存可能会需要等待,从而增加时间开销。
数据复制的时间开销与原底层数组的大小成正比。如果原数组很大,复制数据就需要更多的时间。例如,对于一个包含数百万个元素的 slice,扩容时的数据复制操作可能会导致程序在短时间内出现明显的延迟。

空间开销

每次扩容后,都会多出一部分未使用的内存空间。这是为了后续添加元素预留的。虽然这种策略可以减少频繁扩容的次数,但在某些情况下,如果 slice 的最终大小并没有达到预期,就会造成内存浪费。例如,一个 slice 只需要添加 10 个元素,但由于扩容策略,它的容量可能被扩展到 20 或更大,这就多占用了一部分内存。

三、优化扩容的建议

预先分配足够的容量

如果在使用 slice 之前能够预估大致的元素数量,可以在创建 slice 时就预先分配足够的容量。例如,如果知道要存储 1000 个元素,可以使用 make([]int, 0, 1000) 来创建一个长度为 0、容量为 1000 的 slice。这样就可以避免多次扩容,提高程序的性能。

使用 Append 操作的优化形式

当需要向 slice 中添加多个元素时,可以尽量减少对 append 函数的调用次数。例如,如果有多个元素要添加,可以先将它们存储在一个临时的 slice 中,然后一次性使用 append 将临时 slice 添加到目标 slice 中。这样可以减少扩容的次数,因为一次性添加多个元素可能会直接触发一次较大规模的扩容,而不是多次小规模的扩容。


http://www.kler.cn/a/509764.html

相关文章:

  • openwrt下oaf插件编译安装,实现上网行为监控
  • 挖掘机检测数据集,准确识别率91.0%,4327张原始图片,支持YOLO,COCO JSON,PASICAL VOC XML等多种格式标注
  • JEL分类号
  • 《汽车维修技师》是什么级别的期刊?是正规期刊吗?能评职称吗?
  • 将图像输入批次扁平化为CNN
  • 【机器学习实战入门】使用 Pandas 和 OpenCV 进行颜色检测
  • 电子应用设计方案95:智能AI热水瓶系统设计
  • 技术洞察:C++在后端开发中的前沿趋势与社会影响
  • 关于我的博客建站经历
  • 每打开一个chrome页面都会【自动打开F12开发者模式】,原因是 使用HBuilderX会影响谷歌浏览器的浏览模式
  • vue编写一个可拖动的模块,并可以和任何其他组件组合使用
  • 【2024年华为OD机试】 (B卷,100分)- 流水线(Java JS PythonC/C++)
  • 算法之 二叉树
  • 深入理解 SQL 中的 DATEDIFF 函数
  • Python基于Vue+Django网上商城的设计与实现【附源码】
  • “大数据+技校”:VR虚拟仿真实训室的发展前景
  • Windows图形界面(GUI)-QT-C/C++ - Qt Tree Widget详解与应用
  • 【机器学习实战入门】使用OpenCV进行性别和年龄检测
  • list转tensor很慢
  • SpringMVC——原理简介
  • openharmony标准系统芯片移植指导
  • 360AI平台资源可视化建设
  • Java开发提效秘籍:巧用Apache Commons IO工具库
  • 【力扣Hot 100】子串
  • 力扣动态规划-3【算法学习day.97】
  • React 中hooks之useLayoutEffect 用法总结以及与useEffect的区别