string与[]byte相互转换
在写程序的过程中经常遇到string与[]byte的相互转换,但是这种转换是有代价的,string与[]byte并不共享底层内存空间,所以每次转换都伴随着内存的分配与底层字节的拷贝。
我们可以借助unsafe完成指针类型转换,避开内存分配与复制,从而提升性能。属于黑魔法,尽量不要用。
/*
struct string{
uint8 *str;
int len;
}
struct []uint8{
uint8 *array;
int len;
int cap;
}
uintptr是golang的内置类型,是能存储指针的整型,uintptr的底层类型是int,它和unsafe.Pointer可相互转换。
但是转换后的string与[]byte共享底层空间,如果修改了[]byte那么string的值也会改变,就违背了string应该是只读的规范了,可能会造成难以预期的影响。
*/
func str2byte(s string) []byte {
x := (*[2]uintptr)unsafe.Pointer(&s)
h := [3]uintptr{x[0],x[1],x[1]}
return *(*[]byte)(unsafe.Pointer(&h))
}
func byte2str(b []byte) string{
return *(*string)(unsafe.Pointer(&b))
}
map使用注意事项
预设容量
map可以动态扩容,所以我们可以不关心map的大小,但是每次动态扩容时需要付出数据拷贝和重新哈希成本,如果我们能预先知道一个map最终的容量,那么最好在初始化时就指定。
bigMap := make(map[int]int,100000)
直接存储小对象值而不是指针
对于小对象,直接将数据交由 map 保存,远比用指针高效。这不但减少了堆内存分配,关键还在于垃圾回收器不会扫描非指针类型 key/value 对象。
//存储值对象
m := make(map[int]int,1000)
for i := 0 ;i<10000;i++ {
m[i]=i;
}
//存储指针对象
//如果value是个小对象,直接存储值会比较好
m := make(map[int]*int,1000)
for i := 0 ;i<10000;i++ {
value := i
m[i]=&value;
}
手动删除没有元素的map
map可以动态扩容,我们可以不断的往map中添加新元素,但是map并不会自动收缩空间,即使一个map中的所有元素都被删除,map依然会保留所有已分配的空间。
var dict map[int]int = make(map[int]int)
for i := 0 ;i<100000;i++ {
dict[i] = i
}
for key := range dict {
delete(key) //即使删掉所有的元素,dict的容量仍然>=100000
}
// 如果不再使用dict那么手动设置为nil
// dict=nil
//也可以把dict指向一个新创建的小map,原有的map所占用的内存空间会被回收
//dict = make(map[int]int)
了解defer
defer是提高可读性和避免资源未释放的非常有用的关键字,是使用golang写出可靠、稳定程序的利器。
defer后面的表达式会被放入一个类似于栈(stack)的结构,在当前方法返回的时候,列表中的表达式会按照后进先出的顺序执行。
defer本身会有一定的性能损失,但是和它带来的好处相比根本不值得一提,我们需要注意的关键点是defer表达式会在函数返回时被调用,意味着有些资源只能在函数结束时才被释放。
func f(){
m.lock()
defer m.unlock()
//....业务处理逻辑
//这是很常见的上锁方式,但是m.unlock()只会在函数返回时调用,如果业务处理逻辑耗时很长,那么会一直占用着锁,在高并发情况下严重影响性能。
//解决办法是找到**最小临界区**,在处理完最小临界区后及时释放掉锁。
}
func f() {
m.lock()
//...最小临界区
m.unlock()
//...继续处理
}
字符串拼接
字符串的拼接大概有以下几种方式
fmt.Sprintf
fmt.Sprintf(“%s%s%d%s%s”,”hello”,”world”,2016,”come”,”on”)
使用”+”
"hello"+"world"+ strconv.FormatInt(2016,10) +"come"+"on"
使用strings.Join()
将参数组装成[]string,然后调用strings.join,效率最高的一种方式,推荐使用
strs := []string{"hello","world",strconv.FormatInt(2016,10),"come","on"}
str = strings.Join(strs,"")
为什么使用string.Join效率最好呢,来看下strings.Join的代码
func Join(a []string, sep string) string {
//计算最终字符串的长度,根据最终长度创建[]byte,避免拼接过程中内存重新分配
n := len(sep) * (len(a) - 1)
for i := 0; i < len(a); i++ {
n += len(a[i])
}
b := make([]byte, n)
//使用copy函数是最高效的
bp := copy(b, a[0])
for _, s := range a[1:] {
bp += copy(b[bp:], sep)
bp += copy(b[bp:], s)
}
return string(b)
}
但是有时候很难将参数拼接成[]string,这时我们可以使用byte.Buffer
var buffer bytes.Buffer
for i := 0; i < 1000; i++ {
buffer.WriteString("a")
}
reflect的性能影响
反射带来了极大的方便,但是同时也有一定的性能损失。性能要求极高的模块中应该注意反射所带来的性能损失。
JSON是一种常用的数据交换格式,但Go的encoding/json库依赖于反射来对json进行序列化和反序列化。使用ffjson,可以通过使用代码生成的方式来避免反射的使用,相比使用原生库可以提升2~3倍的性能。
channel并不是很高效
Don't communicate by sharing memory, share memory by communicating.
参考文献
雨痕学堂(微信订阅号)
Go高性能编程技巧
golang: 利用unsafe操作未导出变量
How to efficiently concatenate strings in Go