飞道的博客

go 关于函数调用栈的优化思考

198人阅读  评论(0)

函数调用栈可以算是走向秃头程序员的必备知识了,这篇文章并没有讲的很全,只是说明我为什么会遇到调用栈这个东西,只根据我的案例做一些思考,看过的具体文章我会放在最后

为啥会想到函数调用栈这个问题呢?看下面两个函数

// 提一点课外的,例如,MD5,SHA1 这种 hash 函数的使用,直接
// xxx.Sum() 就好,我开始想到了既然 hash 函数要复用,那我
// 写一个全局变量(xxx.New())用不就好了?结果发现实际并不
// 是的,如果有不知道的可以看一下源码,xxx.Sum() 内部做了
// 定义变量,初始化,写入,计算 这几步;而 New 的全局对象
// 在具体使用时也和上面是一样的效果(内存并没有少用多少,
// 结构体的 Sum 方法会进行自身拷贝),而且会有并发问题
// 观察源码就会注意到的,下面也进行了说明
func MD5(data string) string {
   
	tmp := md5.Sum([]byte(data))
	return hex.EncodeToString(tmp[:])
}
// 好,已经解释了 tmp := md5.Sum([]byte(data))
// 这一句不存在什么优化的问题了
// 我开始思考优化问题的关键是下面的几句
// hex.EncodeToString(tmp[:])
// 这个函数内部做了 初始化切片,调用 Encode 方法,返回切片转换的字符串
func MD52(data string) string {
   
	tmp := md5.Sum([]byte(data))
	tmp2 := make([]byte, 32)
	hex.Encode(tmp2, tmp[:])
	return string(tmp2)
}
  • 上面两个函数分别是优化前和优化后的,不知道你能不能看出哪里进行了优化?
  • 当然,这个优化并不大,后面经测试也就 20ns 左右😂,但从底层原理来说确实优化了
  • 下面是我对上面注释的一些解释
// 使用全局的 xxx.New() 对象存在的问题
// 我刚开始打算使用一个全局变量来做
// 发现是有并发问题而且使用全局变量根本没有节省空间
// 可能人家本来也不是这样用的,我非要这样想😂
import (
	"crypto/md5"
	"encoding/hex"
)

var h = md5.New()

func main() {
   
	_ = MD5("hello,world!")
}

func MD5(data string) string {
   
	// 并发问题
	// 在 Write 写入后如果别的  goroutine 刚好调用了 Reset
	// 不就炸了??
	// 所以 xxx.New() 不是这么用的
	// 不知道的注意一下吧,知道的当我没说 -_-
	h.Reset()
	h.Write([]byte(data))

	// Sum 函数会对结构体进行拷贝,所以这样用达不到节省内存的目的
	tmp := h.Sum(nil)
	return hex.EncodeToString(tmp[:])
}

/*
// Sum 函数
func (d *digest) Sum(in []byte) []byte {
	// Make a copy of d so that caller can keep writing and summing.
	// 说的结构体拷贝指这里 !!!
	d0 := *d
	hash := d0.checkSum()
	return append(in, hash[:]...)
}
*/

// =====================================================

// 为什么想到这里可能还可以优化???
// EncodeToString 返回给上级 MD5 函数一个 string
// 而 MD5 又返回给上级一个 string
// 我就想,这里是不是多了一次拷贝呢?
// 如果把 EncodeToString 里做的事调换到 MD5 函数里去做
// 不就少了一次返回了吗?
// 就去查函数调用栈这个知识点(还好以前听过哈哈😄)

// EncodeToString returns the hexadecimal encoding of src.
func EncodeToString(src []byte) string {
   
	dst := make([]byte, EncodedLen(len(src)))
	Encode(dst, src)
	return string(dst)
	// 第一次返回,发生拷贝
}

func MD5(data string) string {
   
	h.Reset()
	h.Write([]byte(data))
	tmp := h.Sum(nil)
	return hex.EncodeToString(tmp[:])
	// 第二次返回,又发生拷贝
}
  • 然后我去查看函数调用栈这个知识点相关的文章,验证是否和我预想的一致,会多发生一次拷贝(依赖于返回值是如何实现的)
  • 经过查找,大致解释一下调用过程,只做简单举例哦
  • main 函数中发生 函数调用,此时 main 函数就叫做 caller,被调函数叫做callee
  • 函数实参是在 caller 栈帧里的,返回值是在 callee 栈帧中,返回是拷贝至caller 的本地变量中
  • 函数实参是从右往左进入caller 栈帧的,返回值进入 callee 栈帧是从左往右,返回时拷贝到 caller 栈帧时就是从右往左了
  • 所以简单画个图
  • 不知道有没有用😂,就是说少一次函数调用就可以少一次拷贝
  • 测试:
package hashs_test

import (
	"crypto/md5"
	"crypto/sha1"
	"encoding/hex"
	"testing"
)

func MD5(data string) string {
   
	tmp := md5.Sum([]byte(data))
	return hex.EncodeToString(tmp[:])
}

func MD52(data string) string {
   
	tmp := md5.Sum([]byte(data))
	tmp2 := make([]byte, 32)
	hex.Encode(tmp2, tmp[:])
	return string(tmp2)
}

func SHA1(data string) string {
   
	tmp := sha1.Sum([]byte(data))
	return hex.EncodeToString(tmp[:])
}

func SHA12(data string) string {
   
	tmp := sha1.Sum([]byte(data))
	tmp2 := make([]byte, 40)
	hex.Encode(tmp2, tmp[:])
	return string(tmp2)
}

func BenchmarkMD5(b *testing.B) {
   
	for i := 0; i < b.N; i++ {
   
		_ = MD5("hello,world")
	}
}

func BenchmarkMD52(b *testing.B) {
   
	for i := 0; i < b.N; i++ {
   
		_ = MD52("hello,world")
	}
}

func BenchmarkSHA1(b *testing.B) {
   
	for i := 0; i < b.N; i++ {
   
		_ = SHA1("hello,world")
	}
}

func BenchmarkSHA12(b *testing.B) {
   
	for i := 0; i < b.N; i++ {
   
		_ = SHA12("hello,world")
	}
}
/*
// 多次测试都稳定在 182 ns/op 左右
goos: windows
goarch: amd64
pkg: tiku/hashs
cpu: Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
BenchmarkMD5
BenchmarkMD5-12    	 6508209	       183.0 ns/op
PASS

// 稳定在 162-164
goos: windows
goarch: amd64
pkg: tiku/hashs
cpu: Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
BenchmarkMD52
BenchmarkMD52-12    	 7254024	       163.3 ns/op
PASS

// 223 - 225
goos: windows
goarch: amd64
pkg: tiku/hashs
cpu: Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
BenchmarkSHA1
BenchmarkSHA1-12    	 5296179	       224.6 ns/op
PASS

// 200
goos: windows
goarch: amd64
pkg: tiku/hashs
cpu: Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
BenchmarkSHA12
BenchmarkSHA12-12    	 5926756	       200.9 ns/op
PASS
*/

真实场景中我们不需要注意这些细枝末节(也注意不到),因为函数的发明就是要我们用的,作为业务来用不需要考虑,当我们要做一个框架,需要很多复用时我们在去考虑减少拷贝次数等等优化方法

参考:
Go 函数调用━栈和寄存器视角
函数调用栈


转载:https://blog.csdn.net/li18434/article/details/115692588
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场