geektutu / blog

极客兔兔的博客,Coding Coding 创建有趣的开源项目。

Home Page:https://geektutu.com

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

切片(slice)性能及陷阱 | Go 语言高性能编程 | 极客兔兔

geektutu opened this issue · comments

https://geektutu.com/post/hpg-slice.html

Go 语言/golang 高性能编程,Go 语言进阶教程,Go 语言高性能编程(high performance go)。详细介绍了切片(slice) 常用的几种操作 append、copy 等的性能及原理。并且介绍了切片的陷阱,即什么情况下会产生大量内存被占用,而没法释放的情况。

牛批!

@chocolateszz 笔芯 ღ( ´・ᴗ・` ) 😋

大佬爱你,上班看得停不下来

大佬爱你,上班看得停不下来

@bestgopher 大E了啊,没有闪,武林要和为贵。

commented

我跟兔兔可以成为好朋友!🥰

例如表达式 s[n] 访问数组的第 n 个元素 这段应该是 访问数组中下标为 n 的元素

总结:

  1. GO 中的数组变量属于值类型,当数组变量被赋值或传递时,实际上会复制整个数组
  2. 切片本质是数组片段的描述,包括数组的指针,片段的长度和容量,切片操作并不复制切片指向的元素,而是复用原来切片的底层数组
    • 长度是切片实际拥有的元素,使用 len 可得到切片长度
    • 容量是切片预分配的内存能够容纳的元素个数,使用 cap 可得到切片容量
      • 当 append 之后的元素小于等于 cap,将会直接利用底层元素剩余的空间
      • 当 append 后的元素大于 cap,将会分配一块更大的区域来容纳新的底层数组,在容量较小的时候,通常是以 2 的倍数扩大
  3. 可能存在只使用了一小段切片,但是底层数组仍被占用,得不到使用,推荐使用 copy 替代默认的 re-slice

刚刚看了Java中对于string的优化,和这个还是有几分相似之处的。

谢谢楼主的文章
有个问题想问一下, 为什么我这样写:

package main

import (
	"fmt"
	"math/rand"
	"runtime"
	"time"
)

func printMem() {
	var rtm runtime.MemStats
	runtime.ReadMemStats(&rtm)
	fmt.Printf("%f MB\n", float64(rtm.Alloc)/1024./1024.)
}
func makeArr() []int {
	arr := make([]int, 8000000)
	return arr
}
func main() {
	arr := makeArr()
	rand.Seed(time.Now().UnixNano())
	for i := 0; i < len(arr); i++ {
		arr[i] = rand.Int()
	}
	arr=arr[:5]
	printMem()//61.218254 MB
	runtime.GC()
	printMem()//0.184059 MB
	fmt.Println(len(arr))
}

得出来的结果, 发现在调用runtime.GC之后内存使用明显减少,回收了部分底层数组呢?
想请教一下这个测试为什么会是这样的结果。谢谢!

谢谢楼主的文章 有个问题想问一下, 为什么我这样写:

package main

import (
	"fmt"
	"math/rand"
	"runtime"
	"time"
)

func printMem() {
	var rtm runtime.MemStats
	runtime.ReadMemStats(&rtm)
	fmt.Printf("%f MB\n", float64(rtm.Alloc)/1024./1024.)
}
func makeArr() []int {
	arr := make([]int, 8000000)
	return arr
}
func main() {
	arr := makeArr()
	rand.Seed(time.Now().UnixNano())
	for i := 0; i < len(arr); i++ {
		arr[i] = rand.Int()
	}
	arr=arr[:5]
	printMem()//61.218254 MB
	runtime.GC()
	printMem()//0.184059 MB
	fmt.Println(len(arr))
}

得出来的结果, 发现在调用runtime.GC之后内存使用明显减少,回收了部分底层数组呢? 想请教一下这个测试为什么会是这样的结果。谢谢!

因为你最后只用到了 arr 的长度,没有使用 arr 底层数组,所以底层数组直接被回收掉了。

你可以在代码最后加上 arr[0] = 1 看看,这样打印出来的结果就一样了。

@EndlessCheng

因为你最后只用到了 arr 的长度,没有使用 arr 底层数组,所以底层数组直接被回收掉了。

你可以在代码最后加上 arr[0] = 1 看看,这样打印出来的结果就一样了。

谢谢回复
还是有个疑问,
我理解的, arr切片本质上是个结构体, 这个结构体应该是在栈上的, 没有被垃圾回收, 那它指向的底层数组为什么会被回收呢?

@EndlessCheng

因为你最后只用到了 arr 的长度,没有使用 arr 底层数组,所以底层数组直接被回收掉了。
你可以在代码最后加上 arr[0] = 1 看看,这样打印出来的结果就一样了。

谢谢回复 还是有个疑问, 我理解的, arr切片本质上是个结构体, 这个结构体应该是在栈上的, 没有被垃圾回收, 那它指向的底层数组为什么会被回收呢?

看上去和编译优化有关,你的代码加上 -gcflags='-N' 编译后打印的结果就变成一样的了。

@EndlessCheng

@EndlessCheng

因为你最后只用到了 arr 的长度,没有使用 arr 底层数组,所以底层数组直接被回收掉了。
你可以在代码最后加上 arr[0] = 1 看看,这样打印出来的结果就一样了。

谢谢回复 还是有个疑问, 我理解的, arr切片本质上是个结构体, 这个结构体应该是在栈上的, 没有被垃圾回收, 那它指向的底层数组为什么会被回收呢?

看上去和编译优化有关,你的代码加上 -gcflags='-N' 编译后打印的结果就变成一样的了。

明白了, 谢谢大佬

博主,请问改电子书是如何部署搭建的?

博主,其中有一个例子:

func lastNumsBySlice(origin []int) []int {
return origin[len(origin)-2:]
}

last num 最后一个元素应该是 origin[len(origin)-1:] 吧

有个问题哈,事先知道slice长度的情况下最好先预设长度会比不预设快,但是实际我benchmark测试结果如下(golang 1.18),结果恰恰相反,这是怎么回事?

goos: windows

goarch: amd64

pkg: demo

cpu: Intel(R) Core(TM) i5-9400 CPU @ 2.90GHz

BenchmarkSlice1-6 181 6142071 ns/op

BenchmarkSlice2-6 150 7996002 ns/op

PASS ok

demo 3.832s

代码如下:

func slice1() {
	var s []int
	for i := 0; i < 1000000; i++ {
		s = append(s, i)
	}
}

//事先分配
func slice2() {
	var s []int = make([]int, 1000000)
	for i := 0; i < 1000000; i++ {
		s = append(s, i)
	}
}

有个问题哈,事先知道slice长度的情况下最好先预设长度会比不预设快,但是实际我benchmark测试结果如下(golang 1.18),结果恰恰相反,这是怎么回事?

goos: windows

goarch: amd64

pkg: demo

cpu: Intel(R) Core(TM) i5-9400 CPU @ 2.90GHz

BenchmarkSlice1-6 181 6142071 ns/op

BenchmarkSlice2-6 150 7996002 ns/op

PASS ok

demo 3.832s

代码如下:

func slice1() {
	var s []int
	for i := 0; i < 1000000; i++ {
		s = append(s, i)
	}
}

//事先分配
func slice2() {
	var s []int = make([]int, 1000000)
	for i := 0; i < 1000000; i++ {
		s = append(s, i)
	}
}

@programmer-liu slice2 里应该是 make([]int, 0, 1000000)

学到了

请教博主,为什么我将gc移到for循环外,两种方法的内存占用都会变得很小呢?

func testLastChars(t *testing.T, f func([]int) []int) {
	t.Helper()
	ans := make([][]int, 0)
	for k := 0; k < 100; k++ {
		origin := generateWithCap(128 * 1024) // 1M
		ans = append(ans, f(origin))
	}
	runtime.GC() // 两个方法都是 0.17M
	printMem(t)
	_ = ans
}

@thetacoding
请教博主,为什么我将gc移到for循环外,两种方法的内存占用都会变得很小呢?

func testLastChars(t *testing.T, f func([]int) []int) {
	t.Helper()
	ans := make([][]int, 0)
	for k := 0; k < 100; k++ {
		origin := generateWithCap(128 * 1024) // 1M
		ans = append(ans, f(origin))
	}
	runtime.GC() // 两个方法都是 0.17M
	printMem(t)
	_ = ans
}

因为你的ans在后面

@thetacoding
请教博主,为什么我将gc移到for循环外,两种方法的内存占用都会变得很小呢?

func testLastChars(t *testing.T, f func([]int) []int) {
	t.Helper()
	ans := make([][]int, 0)
	for k := 0; k < 100; k++ {
		origin := generateWithCap(128 * 1024) // 1M
		ans = append(ans, f(origin))
	}
	runtime.GC() // 两个方法都是 0.17M
	printMem(t)
	_ = ans
}

因为你的ans在GC代码之后没有继续使用了,所以会被直接回收,你可以在后面加一个ans[0] = []int{1}

commented

这个结果是不是有点问题?

8 printLenCap(nums) // len: 5, cap: 8 [1 2 3 4 50]