To-do List
[x] 算法
[ ] 项目
- [ ] cmu15445
- [x] mit6.824
- [ ] mit6.081
[ ] 八股
- [ ] 操作系统
- [ ] 计算机网络
- [ ] 数据库
- [ ] redis
- [x] 日常总结
算法
今天没写。。。 心态有点小崩 等会随便找个题练个手吧
项目
6.824
完成了mapreduce
主要是coordinator mapworker和reduceworker的协调,感觉还是表达能力差了点
放点主要代码吧
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
| func DoMap(mapf func(string, string) []KeyValue, task *Task) { fmt.Println("Begin map function,task information(TaskType Filename NReduce TaskId Finished Start):", task) intermediate := []KeyValue{} filename := task.Filename file, err := os.Open(filename) if err != nil { log.Fatalf("cannot open %v", filename) } content, err := ioutil.ReadAll(file) if err != nil { log.Fatalf("cannot read %v", filename) } file.Close() kva := mapf(filename, string(content)) intermediate = append(intermediate, kva...) fmt.Println("map生成的中间key:", intermediate) intermediateMap := make([][]KeyValue, task.NReduce) for _, tmp := range intermediate { intermediateMap[ihash(tmp.Key)%task.NReduce] = append(intermediateMap[ihash(tmp.Key)%task.NReduce], tmp) } sort.Sort(ByKey(intermediate)) i := 0 for i < task.NReduce { oname := "mr-" + strconv.Itoa(task.TaskId) + "-" + strconv.Itoa(i) ofile, _ := os.Create(oname) enc := json.NewEncoder(ofile) for _, kv := range intermediateMap[i] { enc.Encode(&kv) } i++ ofile.Close() } }
func DoReduce(reducef func(string, []string) string, task *Task) { i := 0 intermediate := []KeyValue{} for i < task.NReduce { filepath := task.Filename + strconv.Itoa(i) + "-" + strconv.Itoa(task.TaskId) fmt.Println("Begin map function,task information(TaskType Filename NReduce TaskId Finished Start):", task, "\nfilepath:", filepath) file, _ := os.Open(filepath) dec := json.NewDecoder(file) for { var kv KeyValue if err := dec.Decode(&kv); err != nil { break } intermediate = append(intermediate, kv) } file.Close() i++ } sort.Sort(ByKey(intermediate)) i = 0 oname := "mr-out-" + strconv.Itoa(task.TaskId) ofile, _ := os.Create(oname)
for i < len(intermediate) { j := i + 1 for j < len(intermediate) && intermediate[j].Key == intermediate[i].Key { j++ } values := []string{} for k := i; k < j; k++ { values = append(values, intermediate[k].Value) } output := reducef(intermediate[i].Key, values) fmt.Fprintf(ofile, "%v %v\n", intermediate[i].Key, output) i = j } }
|
八股
Golang-切片的容量是怎样增长的
一般都是在向slice追加了元素之后,才会引起扩容,追加元素调用的是append函数
先来看看append函数的原型:
1
| func append(slice []Type, elems ...Type) []Type
|
append函数的参数长度可变,因此可以追加多个值到slice中,还可以用…传入slice,直接追加一个切片
1 2
| slice = append(slice, elem1, elem2) slice = append(slice, anotherSlice...)
|
append函数返回值是一个新的slice,Go编译器不允许调用了append函数后不使用返回值。
使用 append 可以向 slice 追加元素,实际上是往底层数组添加元素。但是底层数组的长度是固定的,如果索引 len-1 所指向的元素已经是底层数组的最后一个元素,就没法再添加了。
这时,slice 会迁移到新的内存位置,新底层数组的长度也会增加,这样就可以放置新增的元素。同时,为了应对未来可能再次发生的 append 操作,新的底层数组的长度,也就是新 slice 的容量是留了一定的 buffer 的。否则,每次添加元素的时候,都会发生迁移,成本太高。
新 slice 预留的 buffer 大小是有一定规律的。在golang1.18版本更新之前网上大多数的文章都是这样描述slice的扩容策略的:
当原 slice 容量小于 1024 的时候,新 slice 容量变成原来的 2 倍;原 slice 容量超过 1024,新 slice 容量变成原来的1.25倍。
在1.18版本更新之后,slice的扩容策略变为了:
当原slice容量(oldcap)小于256的时候,新slice(newcap)容量为原来的2倍;原slice容量超过256,新slice容量newcap = oldcap+(oldcap+3*256)/4
举例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| package main
import "fmt"
func main() { s := make([]int, 0)
oldCap := cap(s)
for i := 0; i < 2048; i++ { s = append(s, i)
newCap := cap(s)
if newCap != oldCap { fmt.Printf("[%d -> %4d] cap = %-4d | after append %-4d cap = %-4d\n", 0, i-1, oldCap, i, newCap) oldCap = newCap } } }
|
我先创建了一个空的 slice,然后,在一个循环里不断往里面 append 新的元素。然后记录容量的变化,并且每当容量发生变化的时候,记录下老的容量,以及添加完元素之后的容量,同时记下此时 slice 里的元素。这样,我就可以观察,新老 slice 的容量变化情况,从而找出规律。
运行结果(1.18版本之前):
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| [0 -> -1] cap = 0 | after append 0 cap = 1 [0 -> 0] cap = 1 | after append 1 cap = 2 [0 -> 1] cap = 2 | after append 2 cap = 4 [0 -> 3] cap = 4 | after append 4 cap = 8 [0 -> 7] cap = 8 | after append 8 cap = 16 [0 -> 15] cap = 16 | after append 16 cap = 32 [0 -> 31] cap = 32 | after append 32 cap = 64 [0 -> 63] cap = 64 | after append 64 cap = 128 [0 -> 127] cap = 128 | after append 128 cap = 256 [0 -> 255] cap = 256 | after append 256 cap = 512 [0 -> 511] cap = 512 | after append 512 cap = 1024 [0 -> 1023] cap = 1024 | after append 1024 cap = 1280 [0 -> 1279] cap = 1280 | after append 1280 cap = 1696 [0 -> 1695] cap = 1696 | after append 1696 cap = 2304
|
运行结果(1.18版本):
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| [0 -> -1] cap = 0 | after append 0 cap = 1 [0 -> 0] cap = 1 | after append 1 cap = 2 [0 -> 1] cap = 2 | after append 2 cap = 4 [0 -> 3] cap = 4 | after append 4 cap = 8 [0 -> 7] cap = 8 | after append 8 cap = 16 [0 -> 15] cap = 16 | after append 16 cap = 32 [0 -> 31] cap = 32 | after append 32 cap = 64 [0 -> 63] cap = 64 | after append 64 cap = 128 [0 -> 127] cap = 128 | after append 128 cap = 256 [0 -> 255] cap = 256 | after append 256 cap = 512 [0 -> 511] cap = 512 | after append 512 cap = 848 [0 -> 847] cap = 848 | after append 848 cap = 1280 [0 -> 1279] cap = 1280 | after append 1280 cap = 1792 [0 -> 1791] cap = 1792 | after append 1792 cap = 2560
|
根据上面的结果我们可以看出在1.18版本之前:
在原来的slice容量oldcap小于1024的时候,新 slice 的容量newcap的确是oldcap的2倍。
但是,当oldcap大于等于 1024 的时候,情况就有变化了。当向 slice 中添加元素 1280 的时候,原来的slice 的容量为 1280,之后newcap变成了 1696,两者并不是 1.25 倍的关系(1696/1280=1.325)。添加完 1696 后,新的容量 2304 当然也不是 1696 的 1.25 倍。
在1.18版本之后:
在原来的slice 容量oldcap小于256的时候,新 slice 的容量newcap的确是oldcap 的2倍。
但是,当oldcap容量大于等于 256 的时候,情况就有变化了。当向 slice 中添加元素 512 的时候,老 slice 的容量为 512,之后变成了 848,两者并没有符合newcap = oldcap+(oldcap+3*256)/4 的策略(512+(512+3*256)/4)=832。添加完 848 后,新的容量 1280 当然也不是 按照之前策略所计算出的的1252。
难道现在网上各种文章中的扩容策略并不是正确的吗。我们直接搬出源码:源码面前,了无秘密。
从前面汇编代码我们也看到了,向 slice 追加元素的时候,若容量不够,会调用 growslice 函数,所以我们直接看它的代码。
golang版本1.9.5
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| func growslice(et *_type, old slice, cap int) slice { newcap := old.cap doublecap := newcap + newcap if cap > doublecap { newcap = cap } else { if old.len < 1024 { newcap = doublecap } else { for newcap < cap { newcap += newcap / 4 } } } capmem = roundupsize(uintptr(newcap) * ptrSize) newcap = int(capmem / ptrSize) }
|
golang版本1.18
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| func growslice(et *_type, old slice, cap int) slice { newcap := old.cap doublecap := newcap + newcap if cap > doublecap { newcap = cap } else { const threshold = 256 if old.cap < threshold { newcap = doublecap } else { for 0 < newcap && newcap < cap { newcap += (newcap + 3*threshold) / 4 } if newcap <= 0 { newcap = cap } } } capmem = roundupsize(uintptr(newcap) * ptrSize) newcap = int(capmem / ptrSize) }
|
如果只看前半部分,现在网上各种文章里说的 newcap 的规律是对的。现实是,后半部分还对 newcap 作了一个内存对齐,这个和内存分配策略相关。进行内存对齐之后,新 slice 的容量是要 大于等于 按照前半部分生成的newcap。
之后,向 Go 内存管理器申请内存,将老 slice 中的数据复制过去,并且将 append 的元素添加到新的底层数组中。
最后,向 growslice 函数调用者返回一个新的 slice,这个 slice 的长度并没有变化,而容量却增大了。
总结
。。。失败 但是差不多能知道哪里不足了 可以尝试一下 明天笔试加油