逐步学习Go-Slice(切片还可以多挖一下)

ops/2024/10/21 1:07:27/

特性

  1. 扩缩容:只有自动扩容,没有自动缩容
  2. 原地与非原地:切片有原地有非原地操作

自动扩容

Slice只有自动扩容,不会自动缩容,而且自动扩容会根据当前slice的容量来计算不同的扩容系数,具体容量和扩展因子关系如下表:

初始容量(Starting Cap)扩展因子(Growth Factor)
2562.0
5121.63
10241.44
20481.35
40961.30

Go 对Slice这么做的原因是:内存和性能之间的权衡。

  1. Go 设定 256 为一个阈值,这个阈值的设定是基于减少添加元素到最终形成非常大的切片时的总内存的重新分配次数。
  2. 当切片长度小于或等于 256 时,每次添加元素导致切片容量不足时,Go会选择按照2倍增加切片容量,以减少内存重分配的次数。
  3. 当切片长度大于 256 但还小于等于 1024 时,每次容量不足时扩大的容量会小于翻倍,这是为了更加接近实际需求,减少内存的浪费。
  4. 当切片长度超过 1024 时,每次扩容的容量比例更大,这样能尽可能减少因为添加元素导致的大切片内存重分配次数。

原地与非原地操作

什么是原地和非原地?

原地算法(in-place algorithm,也称就地算法)是不需要额外的数据结构就能变换输入数据的算法。不过,分配少量空间给部分辅助变量是被允许的。

不是原地就是非原地,非原地就是要分配额外的内存空间来存储数据。

原地与非原地区别

原地操作和非原地操作的主要区别在于,原地操作在原有的内存空间上进行修改,不需要额外分配内存;而非原地操作由于需要创建新的空间来保存数据,因此会产生额外的内存开销。

slice中的原地与非原地操作

原地操作:

  1. 索引操作
  2. 获取子切片
  3. append操作(当slice容量足够时,也就是不扩容时)

非原地操作:

  1. append操作(当slice容量不足时,进行扩容)
  2. copy操作

原地操作示例

索引操作

以下代码获取切片的某个元素

    s := make([]int, 5)s[0] = 1s[1] = 2s[2] = 3s[3] = 4s[4] = 5

COPY

子切片

子切片就是原来切片的子级。子切片引用原来切片的元素,所以是原址。子切片并没有申请内存空间把元素拷贝到新空间中。


s1 := s[:1]
fmt.Printf("s1 slice addr: %p\n", &s1)
println("s1 addr: ", unsafe.Pointer(&s1[0]))

COPY

append操作

append操作在容量足够的情况下是原址操作,比如:我们创建一个slice,容量为5,然后调用append添加第6个元素之前都是原址操作。


s := make([]int, 10)
s = append(s, 1, 2, 3, 4, 5)

COPY

如何确定自己代码是否是原址?看汇编

TEXT    main.createSlice(SB), ABIInternal, $32-8CMPQ    SP, 16(R14)PCDATA  $0, $-2JLS     51PCDATA  $0, $-1PUSHQ   BPMOVQ    SP, BPSUBQ    $24, SPFUNCDATA        $0, FUNCDATA        $1, FUNCDATA        $5, FUNCDATA        $6, PCDATA  $3, $1MOVQ    AX, main.cap+40(SP)PCDATA  $3, $-1MOVQ    AX, BXMOVQ    BX, CXLEAQ    type:int(SB), AXPCDATA  $1, $0CALL    runtime.makeslice(SB)MOVQ    main.cap+40(SP), BXMOVQ    BX, CXADDQ    $24, SPPOPQ    BPRETNOPPCDATA  $1, $-1PCDATA  $0, $-2MOVQ    AX, 8(SP)CALL    runtime.morestack_noctxt(SB)PCDATA  $0, $-1MOVQ    8(SP), AXJMP     0

COPY

非原地操作示例

append操作

你make了一个容量为5的slice,然后再append 5个元素,这会自动扩容。

s := make([]int, 5)s[0] = 1s[1] = 2s[2] = 3s[3] = 4s[4] = 5s = append(s, 1, 2, 3, 4, 5)fmt.Printf("s slice addr: %p\n", &s)

COPY

汇编代码:


...
CALL    runtime.makeslice(SB)
...CALL    runtime.growslice(SB)
...

COPY

copy操作

copy操作是深拷贝。

scopy := make([]int, 10)copy(s, scopy)println("scopy 1st element addr: ", unsafe.Pointer(&scopy[0]))

COPY

slice源码

下面的源码我进行了部分删除,为了更好的聚焦核心内容。

创建makeslice


func makeslice(et *_type, len, cap int) unsafe.Pointer {// 根据slice的类型和容量来计算slice总共需要多少个字节的内存mem, overflow := math.MulUintptr(et.Size_, uintptr(cap))// 分配内存// mem为字节长度return mallocgc(mem, et, true)
}

COPY

自动扩容growslice

首先调用nextslicecap计算出新的容量,然后再使用Switch-case来计算出新容量对应的字节容量,最后申请内存并把老的Slice元素指针复制到新的Slice元素指针。


func growslice(oldPtr unsafe.Pointer, newLen, oldCap, num int, et *_type) slice {oldLen := newLen - num// 根据上文中提到的容量大小和扩展因子来计算新的容量newcap := nextslicecap(newLen, oldCap)switch {// 如果是一个字节的类型(byte,bool, int8系列),那么新的切片元素指针et内存大小=newcapcase et.Size_ == 1:lenmem = uintptr(oldLen)newlenmem = uintptr(newLen)capmem = roundupsize(uintptr(newcap), noscan)overflow = uintptr(newcap) > maxAllocnewcap = int(capmem)// 如果是类型正好是系统架构的字宽,那么容量需要乘以这个字宽: newcap * et.Size——case et.Size_ == goarch.PtrSize:lenmem = uintptr(oldLen) * goarch.PtrSizenewlenmem = uintptr(newLen) * goarch.PtrSizecapmem = roundupsize(uintptr(newcap)*goarch.PtrSize, noscan)overflow = uintptr(newcap) > maxAlloc/goarch.PtrSizenewcap = int(capmem / goarch.PtrSize)// 如果类型是2的倍数:2,4,8, 16...,使用位运算来计算内存大小case isPowerOfTwo(et.Size_):var shift uintptrif goarch.PtrSize == 8 {// Mask shift for better code generation.shift = uintptr(sys.TrailingZeros64(uint64(et.Size_))) & 63} else {shift = uintptr(sys.TrailingZeros32(uint32(et.Size_))) & 31}lenmem = uintptr(oldLen) << shiftnewlenmem = uintptr(newLen) << shiftcapmem = roundupsize(uintptr(newcap)<<shift, noscan)overflow = uintptr(newcap) > (maxAlloc >> shift)newcap = int(capmem >> shift)capmem = uintptr(newcap) << shift// 其他情况就直接newcap * et.Size_default:lenmem = uintptr(oldLen) * et.Size_newlenmem = uintptr(newLen) * et.Size_capmem, overflow = math.MulUintptr(et.Size_, uintptr(newcap))capmem = roundupsize(capmem, noscan)newcap = int(capmem / et.Size_)capmem = uintptr(newcap) * et.Size_}if overflow || capmem > maxAlloc {panic(errorString("growslice: len out of range"))}var p unsafe.Pointerif et.PtrBytes == 0 {p = mallocgc(capmem, nil, false)// The append() that calls growslice is going to overwrite from oldLen to newLen.// Only clear the part that will not be overwritten.// The reflect_growslice() that calls growslice will manually clear// the region not cleared here.memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)} else {// Note: can't use rawmem (which avoids zeroing of memory), because then GC can scan uninitialized memory.p = mallocgc(capmem, et, true)if lenmem > 0 && writeBarrier.enabled {// Only shade the pointers in oldPtr since we know the destination slice p// only contains nil pointers because it has been cleared during alloc.//// It's safe to pass a type to this function as an optimization because// from and to only ever refer to memory representing whole values of// type et. See the comment on bulkBarrierPreWrite.bulkBarrierPreWriteSrcOnly(uintptr(p), uintptr(oldPtr), lenmem-et.Size_+et.PtrBytes, et)}}memmove(p, oldPtr, lenmem)return slice{p, newLen, newcap}
}// nextslicecap computes the next appropriate slice length.
func nextslicecap(newLen, oldCap int) int {newcap := oldCapdoublecap := newcap + newcapif newLen > doublecap {return newLen}const threshold = 256if oldCap < threshold {return doublecap}for {// Transition from growing 2x for small slices// to growing 1.25x for large slices. This formula// gives a smooth-ish transition between the two.newcap += (newcap + 3*threshold) >> 2// We need to check `newcap >= newLen` and whether `newcap` overflowed.// newLen is guaranteed to be larger than zero, hence// when newcap overflows then `uint(newcap) > uint(newLen)`.// This allows to check for both with the same comparison.if uint(newcap) >= uint(newLen) {break}}// Set newcap to the requested cap when// the newcap calculation overflowed.if newcap <= 0 {return newLen}return newcap
}

COPY

可以留心一下

  1. 子切片中的元素是原来切片元素的子集是引用关系,所以有一个子切片就不会释放原来切片的内存,像是内存泄露
  2. 尽量避免切片自动扩容,原因主要是切片耗时并且不会自动缩容
  3. 大切片只有一个很小的子切片,这时候建议copy一个小切片,让切片尽快释放内存
  4. 切片循环引用问题

http://www.ppmy.cn/ops/4049.html

相关文章

多线程8

void 取款(){int oldBalance balance if(!CAS(balance,oldBalance,oldBalance-500)){}} 在这个线程中如果变成了这样 void 取款(){int oldBalance balancevoid 取款(){int oldBalance balance if(!CAS(balance,oldBalance,oldBalance-500)){}有人转账发生了500->1000。} i…

量子密钥分发系统设计与实现(一):系统基本架构讨论

经过一段时间讨论&#xff0c;我们了解到量子密钥分发设备是当前量子保密通信系统的基础。从本文开始&#xff0c;我将开启量子密钥分发系统设计与实现系列&#xff0c;详细讨论量子密钥分发设备如何从0到1的搭建。 1.QKD系统总体讨论 QKD系统的核心功能就是为通信双方提供理论…

Windows下载使用nc(netcat)命令

‘nc’ 不是内部或外部命令&#xff0c;也不是可运行的程序&#xff1f; 点击链接地址&#xff0c;下载压缩包。 完成后解压 使用方式&#xff08;三种&#xff09;&#xff1a; 1、直接双击exe使用 2、把这个exe放到cmd启动的默认路径下 放到默认路径下&#xff0c;使用nc&a…

Linux引导过程与服务控制

Linux操作系统引导过程 排除启动类故障 服务控制及切换运行级别 优化启动过程 Linux引导过程 引导过程总览&#xff1a; 简化来说就是由开机自检 MBA引导 GRUB菜单 加载内核&#xff08;kernel&#xff09; init进程初始化等组成 Linux 操作系统的引导过程&…

DAY31-贪心算法| 455.分发饼干,376.摆动序列,53. 最大子序和

文章目录 455.分发饼干376.摆动序列53.最大子序和 455.分发饼干 文字讲解&#xff1a;分发饼干 视频讲解&#xff1a;分发饼干 状态&#xff1a;这题ok 思路&#xff1a; 代码&#xff1a; class Solution {public int findContentChildren(int[] g, int[] s) {if (s.length0…

vue中使用水印

1. 在utils下创建watermark.js const watermark {}/**** param {要设置的水印的内容} str* param {需要设置水印的容器} container* param {需要设置水印的每一块的宽度} canWidth* param {需要设置水印的每一块的高度} canHeight* param {需要设置水印的字体} canFont* para…

墨子web3时事周报

蚂蚁集团Web3研发进展与布局 国内Web3赛道的领军企业——蚂蚁集团&#xff0c;凭借其在前沿科技领域的深耕不辍&#xff0c;已在Web3技术研发疆域缔造了卓越战绩。特别是在引领行业革新的关键时刻&#xff0c;集团于今年四月末震撼推出了颠覆性的Web3全套解决方案&#xff0c…

移植speexdsp到OpenHarmony标准系统②

在linux上生成speexdsp的so动态链接库和.a静态链接库 make和make install后会生成speexdsp的.so动态链接库和.a静态链接库 make make install其中build/lib目录下&#xff1a; ├── libspeexdsp.a /*静态库*/ ├── libspeexdsp.la …