ARTS 第10周| LeetCode 23 Merge k Sorted Lists | Go 性能调优 | 项目管理很重要

ARTS

ARTS 是陈浩(网名左耳朵耗子)在极客时间专栏里发起的一个活动,目的是通过分享的方式来坚持学习。

每人每周写一个 ARTS:Algorithm 是一道算法题,Review 是读一篇英文文章,Technique/Tips 是分享一个小技术,Share 是分享一个观点。

本周内容

本周你将看到:

  1. 多路归并排序的 Golang 实现。
  2. Go 官方教你如何做性能分析。
  3. 项目经理真的很重要。

Algorithm

本周的算法题是一道链表和归并排序结合的题目:23. Merge k Sorted Lists.

这道题目的解法不陌生,对于用 Go 来解这道题的我来说,这道题的难点反而是在实现上。Go 中的 container/heap 我之前是不了解的,幸亏在我决定自己实现一个堆之前尝试搜索了一下 Go 是否有类似的工具。不过话说回来,container/heap这个工具也着实过于简单。只帮你实现堆化(heapify),其他的堆内实际数据类型需要你自己定义,同时类型需要支持 Less() 方法,如果不支持的话需要你自己定义。此外你还要实现上述类型的 Pop()Push() 方法对底层数据的具体操作方式。其他需要注意的地方我都有已经写在注释中了。

func mergeKLists(lists []*ListNode) *ListNode {
    if len(lists) == 0 {
        return nil
    }

    var fhead = &ListNode{}
    h := &IntHeap{}
    heap.Init(h)

    for i := range lists {
        if lists[i] == nil {
            continue
        }
        heap.Push(h, node{Val: lists[i].Val, ListIdx: i})
        lists[i] = lists[i].Next
    }

    curr := fhead
    // container/heap 会自动帮你做 heapify
    // 每次从小顶堆 pop 之后要从原来的 list 中再取出一个 node
    // 如果原来的 list 为空就继续 pop
    // 这也就是为什么不直接存 ListNode.Val 到 heap 里
    // 为了保存当前从堆 pop 出来的值来自哪个 list 专门声明了 node 类型
    // 另外,可能你会对为什么一定要从 pop 出来的值原来的 list 再取一个 node
    // 原因在于,本地的 merge 逻辑就是随时都在堆里放当前所有 list 的最小值
    // 每次都从原 list 取一个值放进堆里,这个值被 push 进去之后可能不会成为堆顶
    // 但是成为一旦原 list 的下一个值可能成为堆顶或者可能在其被 push 进堆之前成为堆顶
    // 就会造成最终 merge 排序的乱序
    // 所以最稳健和简单的做法就是每次都从原 list 找下一个值 push 到堆里
    for h.Len() > 0 {
        n := heap.Pop(h).(node)
        curr.Next = &ListNode{Val: n.Val}
        if lists[n.ListIdx] != nil {
            heap.Push(h, node{
                Val:     lists[n.ListIdx].Val,
                ListIdx: n.ListIdx})
            lists[n.ListIdx] = lists[n.ListIdx].Next
        }
        curr = curr.Next
    }
    return fhead.Next
}

type node struct {
    Val     int
    ListIdx int
}

type IntHeap []node

func (h IntHeap) Len() int { return len(h) }

// Less 递增表示构成小顶堆
func (h IntHeap) Less(i, j int) bool { return h[i].Val < h[j].Val }

func (h IntHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] }

func (h *IntHeap) Push(x interface{}) {
    *h = append(*h, x.(node))
}

func (h *IntHeap) Pop() interface{} {
    ret := (*h)[len(*h)-1]
    *h = (*h)[:len(*h)-1]
    return ret
}

Review 文章推荐

本周推荐的文章是 Go 官网的一篇性能调优的科普文章:Profiling Go Programs.

这篇文章其实是对下面这篇论文Loop Recognition in C++/Java/Go/Scala的一个官方回应,因为论文里的几种语言性能的测试结果对 Golang 很不「友好」。总之从结果来看 Go 在上述基准测试中的表现都不太好,Go 官方表示并「不是我们性能不太好,而是你们不互用!」

The Go program presented in that paper runs quite slowly, making it an excellent opportunity to demonstrate how to use Go's profiling tools to take a slow program and make it faster.

怎么样,是不是感觉似曾相识?

于是官方把怎么使用 pprof 工具开始,再到具体优化 Go 代码的 CPU 占用和内存使用效率,全在这篇文章里介绍了一遍。当然,对于具体的优化内容不可能面面俱到。文中主要是对某些数据类型是不用不当(比如将 map 改成 slice)降低 CPU 的使用率,对某些频繁创建和删除的对象做了本地缓存来降低内存申请和垃圾回收对 CPU 的消耗。

上面的两项措施从基准结果来看效果显著,然后作者不忘嘲讽一下之前那篇论文。

Having a garbage-collected language doesn't mean you can ignore memory allocation issues. In this case, a simple solution is to introduce a cache so that each call to FindLoops reuses the previous call's storage when possible. (In fact, in Hundt's paper, he explains that the Java program needed just this change to get anything like reasonable performance, but he did not make the same change in the other garbage-collected implementations.)

最后,一顿操作之后,作者给出了优化的结果:比论文中的结论至少可以快 6 倍到 11 倍。同时,作者强调「还有」可以优化的地方存在。但是,后续的优化操作在使用的工具和方法上,与上述优化过程中使用过的工具和方法没有任何区别。

There's more we can do to clean up the program and make it faster, but none of it requires profiling techniques that we haven't already shown.

最后的最后,作者还要补充一句 Go 和 C++ 可以在「某种程度上」不相上下。

Benchmarks are only as good as the programs they measure. We used go tool pprof to study an inefficient Go program and then to improve its performance by an order of magnitude and to reduce its memory usage by a factor of 3.7. A subsequent comparison with an equivalently optimized C++ program shows that Go can be competitive with C++ when programmers are careful about how much garbage is generated by inner loops

Tip 编程技巧

本周很想找到一个变成技巧,但我还是失败了。哭干了眼泪。

Share 灵光一闪

周五晚上和一个老同学聊到了一些在各自不同的公司项目管理模式的经历。对于我目前的工作来说,项目的进度完全不需要我去考虑,因为有项目经历在负责推荐整个项目。我只需要负责我自己的部分,保证能按时完成。如果有对接的其他部门或者团队的开发进度比预期更慢的话,我都会让项目经理去解决。而我的同学所在的公司在这方面似乎分工并不严格,导致我的这位同学默默做了很多催促对接团队项目进度的工作,甚至是为对方的团队进行了一些友情援助式的开发工作。

然而,项目到最后还是未能按期交付。对方的团队领导反而觉得我这位同学工作内容不够饱和,一直是「嘴强王者」,而事实上他已经提前半个月完成了自己的开发工作。这种事情在我这里是不可能发生了,我首先就不可能跑去别的团队催促他们的工作进度。因为那不是我的职责,再者因为我不了解对方的项目排期和优先级等等等,我更绝不可能去插手其他团队。

说实话听到我这个同学的经历,我是震惊的。首先我无法理解他为什么会插手其他团队的工作,再次我也无法理解对方团队这种落井下石的行为。这些本来应该由项目经理完成的工作,如果平时的开发顺利的话,是很难注意到的。可一旦发生了某些问题导致项目需要人为推进,那么项目经理的重要性就会凸显出来。

所以在平时的开发中,不要总觉得项目经理就是催你我干活的,他们多数时候都是在帮你解决很多你绝对不想自己解决的问题。

本周阅读列表

你可能感兴趣的:(golang,leetcode,性能优化,项目管理)