Go Slice与String内存布局和实现

  上一篇提到的关于gc性能的问题,对比slice和map的结构可以看出为了存储数据map用了更多的内存空间,并且可能存在链表,链表的每个节点在gc时都做为一个小对象对待,增加了扫描的时间,因此gc时间相对更长。

  slice通过内部指针和相关属性引用数组片段,来实现变长方案。实现方式和数据结构都类似C++中的vector。它本身是结构体,作为参数传递时传递的是slice本身而不是它引用的底层数组。len()可获得slice长度,cap()可获得slice容量。

  

Go Slice与String内存布局和实现

  slice可以通过数组初始化,也可以直接make。make时直接使用cap作为new的长度来创建底层数组,返回的是slice结构体。如果通过new([]int)来初始化,它返回的是一个指向slice结构体的指针,不能直接对它进行下标操作。

  

Go Slice与String内存布局和实现

  遍历slice时经常用到range操作,range会复制range的对象。下面例子中在循环内部改变slice的属性,最终会作用到slice上导致最后输出[1 2 101]。但是并不会导致循环在第三次就结束,因为range s是从s的复本中读取i和n的。s的复本只复制了指针,底层元素仍指向同一片,因此可以在循环内改变slice元素的值并在循环期内可见。

  

Go Slice与String内存布局和实现

  reslice的增长规则:如果新的size是当前size的2倍以上,则大小增长为新size。如果新的size不到当前size的2倍,则按当前size的不同有不同操作。当前size不超过1024,按每次2倍增长,否则按当前大小的1/4增长。

  slice通过append元素使得元素达到cap,就会重新分配内存,复制内容并接着append,即便指向的数组还有空位。比如这个例子a初始化为长度和容量都是3的slice,再往a中append数据时a将在堆上重新分配空间并复制原始内容,因此这时原始数组的后几位已经看不到了。

  

Go Slice与String内存布局和实现

  如果slice作为函数的入参,通常希望对slice的操作可以影响到底层数据,但是如果在函数内部append数据超过了cap,导致重新分配底层数组,这时入参a指向的底层数组跟调用方实参指向的不再是同一个。如下面的例子这样因为扩容导致与代码实现原意相违背,因此通常不建议在函数内部对slice有append操作,若有需要则显示的返回这个slice。

  

Go Slice与String内存布局和实现

  string的结构和C++STL实现的string类似。都是由指向固定地址的str指针和len组成的结构体。对string的复制只是对指针和len的复制,作为函数参数时入参只不过是指向同一个底层数据的相同指针。

  通常string常量是编译器分配到只读段的(.rodata),对应的数据地址不可写入。fmt.Sprintf生成的字符串分配在堆上,对应数据地址可修改。

  

Go Slice与String内存布局和实现

  平常使用中经常将两者互相转化,每次相互转化时都会发生底层数据的复制。如果是动态生成的字符串可以通过以下对指针的操作来直接转化数据,而不需要拷贝,性能好接近4倍。

  

Go Slice与String内存布局和实现

上一篇:[20220116]P2254 [NOI2005] 瑰丽华尔兹


下一篇:js数组的常用方法