Slices instead of maps for (key -> value) entries

今天看了下fasthttp的源码,发现了一个有趣的地方,遂研究了一下。

详情请直接看原作者的一个slides

https://docs.google.com/presentation/d/e/2PACX-1vTxoBN41dYFB8aV8c0SDET3B2htsAavXPAwR-CMyfT2LfARR2KjOt8EPIU1zn8ceSuxrL8BmkOqqL_c/pub?start=false&loop=false&delayms=3000&slide=id.g524654fd95_0_111

这里简单分析一下为何这么替换和替换的一些细节。

第一眼看下来,what,哪边复用,有什么精妙的,但是要结合场景考虑,这个是专门为net.http写的,每一次请求它是不同的。

fasthttp中的reset思路是

sm=sm[:0]

不是我们一般习惯的

sm=nil

由于切片的源码是

// runtime/slice.go
type slice struct {
    array unsafe.Pointer // 数组指针
    len   int // 长度 
    cap   int // 容量
}

可见,这种思路完全是复用底层内存的,但是仅仅如此吗?作者还有个很精妙的地方

就是扩容,这种扩容是持久的,上一个请求的sliceMap够用,这个不够用了,ok,扩容,下一个如果和上一个相同的需求,不需要再次扩容了,一个服务的异样请求统共不会太多,所以一段时间后就基本不会发生扩容了。

这里的kv.key和kv.value,在前面的基础上就使用的是append(s[:0], k...)这种思路,你这里可能会问这不是发生string和slice的转换吗,如果看汇编的话,这种操作是会进行编译优化的。

没有call stringtoslice,性能是可以保证的。

源码示例

type argsKV struct {
    key     []byte
    value   []byte
    noValue bool
}

// 增加新的kv
func appendArg(args []argsKV, key, value string, noValue bool) []argsKV {
    var kv *argsKV
    args, kv = allocArg(args)
    // 复用原来key的内存空间
    kv.key = append(kv.key[:0], key...)
    if noValue {
        kv.value = kv.value[:0]
    } else {
        // 复用原来value的内存空间
        kv.value = append(kv.value[:0], value...)
    }
    kv.noValue = noValue
    return args
}

func allocArg(h []argsKV) ([]argsKV, *argsKV) {
    n := len(h)
    if cap(h) > n {
        // 复用底层数组空间,不用分配
        h = h[:n+1]
    } else {
        // 空间不足再分配
        h = append(h, argsKV{})
    }
    return h, &h[n]
}

 至于别的区别,可以看slice和map的源码,首先两者的struct就能看出这种方式的优点。

不过,话转回来,这好像让编程变得更麻烦了点,因为golang的设计就是不推荐走手动管内存的。如果没有性能瓶颈,还是建议采用Golang核心队伍的的思路。

一个没有高级趣味的人。 email:hushui502@gmail.com
原文地址:https://www.cnblogs.com/CherryTab/p/12849676.html