主頁 > 後端開發 > Go語言性能剖析利器--pprof實戰

Go語言性能剖析利器--pprof實戰

2022-12-17 06:34:51 後端開發

作者:耿宗杰

前言

關于pprof的文章在網上已是汗牛充棟,卻是千篇一律的命令介紹,鮮有真正實操的,本文將參考Go社區資料,結合自己的經驗,實戰Go程式的性能分析與優化程序,

優化思路

首先說一下性能優化的一般思路,系統性能的分析優化,一定是從大到小的步驟來進行的,即從業務架構的優化,到系統架構的優化,再到系統模塊間的優化,最后到代碼撰寫層面的優化,業務架構的優化是最具性價比的,技術難度相對較小,卻可以帶來大幅的性能提升,比如通過和同事或外部門溝通,減少了一些介面呼叫或者去掉了不必要的復雜的業務邏輯,可以輕松提升整個系統的性能,

系統架構的優化,比如加入快取,由http改進為rpc等,也可以在少量投入下帶來較大的性能提升,最后是程式代碼級別的性能優化,這又分為兩方面,一是合格的資料結構與使用,二才是在此基礎上的性能剖析,比如在Go語言中使用slice這種方便的資料結構時,盡可能提前申請足夠的記憶體防止append超過容量時的記憶體申請和資料拷貝;使用并發保護時盡量由RWMutex 代替mutex,甚至在極高并發場景下使用更細粒度的原子操作代替鎖等等,

優化實踐

下面進入正文,待優化程式是社區中一個例子,代碼有點長,實作的演算法是著名的計算機科學家Tarjan的求圖的強連通分量演算法,關于這個演算法的思想請自行google(就別自行百度了~),以下為實操程序(會有那么一丟丟長,,,):

初始版本代碼 havlak1.go:

// Go from multi-language-benchmark/src/havlak/go_pro// Copyright 2011 Google Inc.//// Licensed under the Apache License, Version 2.0 (the "License");// you may not use this file except in compliance with the License.// You may obtain a copy of the License at////     http://www.apache.org/licenses/LICENSE-2.0//// Unless required by applicable law or agreed to in writing, software// distributed under the License is distributed on an "AS IS" BASIS,// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.// See the License for the specific language governing permissions and// limitations under the License.// Test Program for the Havlak loop finder.//// This program constructs a fairly large control flow// graph and performs loop recognition. This is the Go// version.//package mainimport (   "flag"   "fmt"   "log"   "os"   "runtime/pprof")type BasicBlock struct {   Name     int   InEdges  []*BasicBlock   OutEdges []*BasicBlock}func NewBasicBlock(name int) *BasicBlock {   return &BasicBlock{Name: name}}func (bb *BasicBlock) Dump() {   fmt.Printf("BB#%06d:", bb.Name)   if len(bb.InEdges) > 0 {      fmt.Printf(" in :")      for _, iter := range bb.InEdges {         fmt.Printf(" BB#%06d", iter.Name)      }   }   if len(bb.OutEdges) > 0 {      fmt.Print(" out:")      for _, iter := range bb.OutEdges {         fmt.Printf(" BB#%06d", iter.Name)      }   }   fmt.Printf("\n")}func (bb *BasicBlock) NumPred() int {   return len(bb.InEdges)}func (bb *BasicBlock) NumSucc() int {   return len(bb.OutEdges)}func (bb *BasicBlock) AddInEdge(from *BasicBlock) {   bb.InEdges = append(bb.InEdges, from)}func (bb *BasicBlock) AddOutEdge(to *BasicBlock) {   bb.OutEdges = append(bb.OutEdges, to)}//-----------------------------------------------------------type CFG struct {   Blocks []*BasicBlock   Start  *BasicBlock}func NewCFG() *CFG {   return &CFG{}}func (cfg *CFG) NumNodes() int {   return len(cfg.Blocks)}func (cfg *CFG) CreateNode(node int) *BasicBlock {   if node < len(cfg.Blocks) {      return cfg.Blocks[node]   }   if node != len(cfg.Blocks) {      println("oops", node, len(cfg.Blocks))      panic("wtf")   }   bblock := NewBasicBlock(node)   cfg.Blocks = append(cfg.Blocks, bblock)   if len(cfg.Blocks) == 1 {      cfg.Start = bblock   }   return bblock}func (cfg *CFG) Dump() {   for _, n := range cfg.Blocks {      n.Dump()   }}//-----------------------------------------------------------type BasicBlockEdge struct {   Dst *BasicBlock   Src *BasicBlock}func NewBasicBlockEdge(cfg *CFG, from int, to int) *BasicBlockEdge {   self := new(BasicBlockEdge)   self.Src = https://www.cnblogs.com/Jcloud/archive/2022/12/16/cfg.CreateNode(from)   self.Dst = cfg.CreateNode(to)   self.Src.AddOutEdge(self.Dst)   self.Dst.AddInEdge(self.Src)   return self}//-----------------------------------------------------------// Basic Blocks and Loops are being classified as regular, irreducible,// and so on. This enum contains a symbolic name for all these classifications//const (   _             = iota // Go has an interesting iota concept   bbTop                // uninitialized   bbNonHeader          // a regular BB   bbReducible          // reducible loop   bbSelf               // single BB loop   bbIrreducible        // irreducible loop   bbDead               // a dead BB   bbLast               // sentinel)// UnionFindNode is used in the Union/Find algorithm to collapse// complete loops into a single node. These nodes and the// corresponding functionality are implemented with this class//type UnionFindNode struct {   parent    *UnionFindNode   bb        *BasicBlock   loop      *SimpleLoop   dfsNumber int}// Init explicitly initializes UnionFind nodes.//func (u *UnionFindNode) Init(bb *BasicBlock, dfsNumber int) {   u.parent = u   u.bb = bb   u.dfsNumber = dfsNumber   u.loop = nil}// FindSet implements the Find part of the Union/Find Algorithm//// Implemented with Path Compression (inner loops are only// visited and collapsed once, however, deep nests would still// result in significant traversals).//func (u *UnionFindNode) FindSet() *UnionFindNode {   var nodeList []*UnionFindNode   node := u   for ; node != node.parent; node = node.parent {      if node.parent != node.parent.parent {         nodeList = append(nodeList, node)      }   }   // Path Compression, all nodes' parents point to the 1st level parent.   for _, ll := range nodeList {      ll.parent = node.parent   }   return node}// Union relies on path compression.//func (u *UnionFindNode) Union(B *UnionFindNode) {   u.parent = B}// Constants//// Marker for uninitialized nodes.const unvisited = -1// Safeguard against pathological algorithm behavior.const maxNonBackPreds = 32 * 1024// IsAncestor//// As described in the paper, determine whether a node 'w' is a// "true" ancestor for node 'v'.//// Dominance can be tested quickly using a pre-order trick// for depth-first spanning trees. This is why DFS is the first// thing we run below.//// Go comment: Parameters can be written as w,v int, inlike in C, where//   each parameter needs its own type.//func isAncestor(w, v int, last []int) bool {   return ((w <= v) && (v <= last[w]))}// listContainsNode//// Check whether a list contains a specific element. //func listContainsNode(l []*UnionFindNode, u *UnionFindNode) bool {   for _, ll := range l {      if ll == u {         return true      }   }   return false}// DFS - Depth-First-Search and node numbering.//func DFS(currentNode *BasicBlock, nodes []*UnionFindNode, number map[*BasicBlock]int, last []int, current int) int {   nodes[current].Init(currentNode, current)   number[currentNode] = current   lastid := current   for _, target := range currentNode.OutEdges {      if number[target] == unvisited {         lastid = DFS(target, nodes, number, last, lastid+1)      }   }   last[number[currentNode]] = lastid   return lastid}// FindLoops//// Find loops and build loop forest using Havlak's algorithm, which// is derived from Tarjan. Variable names and step numbering has// been chosen to be identical to the nomenclature in Havlak's// paper (which, in turn, is similar to the one used by Tarjan).//func FindLoops(cfgraph *CFG, lsgraph *LSG) {   if cfgraph.Start == nil {      return   }   size := cfgraph.NumNodes()   nonBackPreds := make([]map[int]bool, size)   backPreds := make([][]int, size)   number := make(map[*BasicBlock]int)   header := make([]int, size, size)   types := make([]int, size, size)   last := make([]int, size, size)   nodes := make([]*UnionFindNode, size, size)   for i := 0; i < size; i++ {      nodes[i] = new(UnionFindNode)   }   // Step a:   //   - initialize all nodes as unvisited.   //   - depth-first traversal and numbering.   //   - unreached BB's are marked as dead.   //   for i, bb := range cfgraph.Blocks {      number[bb] = unvisited      nonBackPreds[i] = make(map[int]bool)   }   DFS(cfgraph.Start, nodes, number, last, 0)   // Step b:   //   - iterate over all nodes.   //   //   A backedge comes from a descendant in the DFS tree, and non-backedges   //   from non-descendants (following Tarjan).   //   //   - check incoming edges 'v' and add them to either   //     - the list of backedges (backPreds) or   //     - the list of non-backedges (nonBackPreds)   //   for w := 0; w < size; w++ {      header[w] = 0      types[w] = bbNonHeader      nodeW := nodes[w].bb      if nodeW == nil {         types[w] = bbDead         continue // dead BB      }      if nodeW.NumPred() > 0 {         for _, nodeV := range nodeW.InEdges {            v := number[nodeV]            if v == unvisited {               continue // dead node            }            if isAncestor(w, v, last) {               backPreds[w] = append(backPreds[w], v)            } else {               nonBackPreds[w][v] = true            }         }      }   }   // Start node is root of all other loops.   header[0] = 0   // Step c:   //   // The outer loop, unchanged from Tarjan. It does nothing except   // for those nodes which are the destinations of backedges.   // For a header node w, we chase backward from the sources of the   // backedges adding nodes to the set P, representing the body of   // the loop headed by w.   //   // By running through the nodes in reverse of the DFST preorder,   // we ensure that inner loop headers will be processed before the   // headers for surrounding loops.   //   for w := size - 1; w >= 0; w-- {      // this is 'P' in Havlak's paper      var nodePool []*UnionFindNode      nodeW := nodes[w].bb      if nodeW == nil {         continue // dead BB      }      // Step d:      for _, v := range backPreds[w] {         if v != w {            nodePool = append(nodePool, nodes[v].FindSet())         } else {            types[w] = bbSelf         }      }      // Copy nodePool to workList.      //      workList := append([]*UnionFindNode(nil), nodePool...)      if len(nodePool) != 0 {         types[w] = bbReducible      }      // work the list...      //      for len(workList) > 0 {         x := workList[0]         workList = workList[1:]         // Step e:         //         // Step e represents the main difference from Tarjan's method.         // Chasing upwards from the sources of a node w's backedges. If         // there is a node y' that is not a descendant of w, w is marked         // the header of an irreducible loop, there is another entry         // into this loop that avoids w.         //         // The algorithm has degenerated. Break and         // return in this case.         //         nonBackSize := len(nonBackPreds[x.dfsNumber])         if nonBackSize > maxNonBackPreds {            return         }         for iter := range nonBackPreds[x.dfsNumber] {            y := nodes[iter]            ydash := y.FindSet()            if !isAncestor(w, ydash.dfsNumber, last) {               types[w] = bbIrreducible               nonBackPreds[w][ydash.dfsNumber] = true            } else {               if ydash.dfsNumber != w {                  if !listContainsNode(nodePool, ydash) {                     workList = append(workList, ydash)                     nodePool = append(nodePool, ydash)                  }               }            }         }      }      // Collapse/Unionize nodes in a SCC to a single node      // For every SCC found, create a loop descriptor and link it in.      //      if (len(nodePool) > 0) || (types[w] == bbSelf) {         loop := lsgraph.NewLoop()         loop.SetHeader(nodeW)         if types[w] != bbIrreducible {            loop.IsReducible = true         }         // At this point, one can set attributes to the loop, such as:         //         // the bottom node:         //    iter  = backPreds[w].begin();         //    loop bottom is: nodes[iter].node);         //         // the number of backedges:         //    backPreds[w].size()         //         // whether this loop is reducible:         //    type[w] != BasicBlockClass.bbIrreducible         //         nodes[w].loop = loop         for _, node := range nodePool {            // Add nodes to loop descriptor.            header[node.dfsNumber] = w            node.Union(nodes[w])            // Nested loops are not added, but linked together.            if node.loop != nil {               node.loop.Parent = loop            } else {               loop.AddNode(node.bb)            }         }         lsgraph.AddLoop(loop)      } // nodePool.size   } // Step c}// External entry point.func FindHavlakLoops(cfgraph *CFG, lsgraph *LSG) int {   FindLoops(cfgraph, lsgraph)   return lsgraph.NumLoops()}//======================================================// Scaffold Code//======================================================// Basic representation of loops, a loop has an entry point,// one or more exit edges, a set of basic blocks, and potentially// an outer loop - a "parent" loop.//// Furthermore, it can have any set of properties, e.g.,// it can be an irreducible loop, have control flow, be// a candidate for transformations, and what not.//type SimpleLoop struct {   // No set, use map to bool   basicBlocks map[*BasicBlock]bool   Children    map[*SimpleLoop]bool   Parent      *SimpleLoop   header      *BasicBlock   IsRoot       bool   IsReducible  bool   Counter      int   NestingLevel int   DepthLevel   int}func (loop *SimpleLoop) AddNode(bb *BasicBlock) {   loop.basicBlocks[bb] = true}func (loop *SimpleLoop) AddChildLoop(child *SimpleLoop) {   loop.Children[child] = true}func (loop *SimpleLoop) Dump(indent int) {   for i := 0; i < indent; i++ {      fmt.Printf("  ")   }   // No ? operator ?   fmt.Printf("loop-%d nest: %d depth %d ",      loop.Counter, loop.NestingLevel, loop.DepthLevel)   if !loop.IsReducible {      fmt.Printf("(Irreducible) ")   }   // must have > 0   if len(loop.Children) > 0 {      fmt.Printf("Children: ")      for ll := range loop.Children {         fmt.Printf("loop-%d", ll.Counter)      }   }   if len(loop.basicBlocks) > 0 {      fmt.Printf("(")      for bb := range loop.basicBlocks {         fmt.Printf("BB#%06d ", bb.Name)         if loop.header == bb {            fmt.Printf("*")         }      }      fmt.Printf("\b)")   }   fmt.Printf("\n")}func (loop *SimpleLoop) SetParent(parent *SimpleLoop) {   loop.Parent = parent   loop.Parent.AddChildLoop(loop)}func (loop *SimpleLoop) SetHeader(bb *BasicBlock) {   loop.AddNode(bb)   loop.header = bb}//------------------------------------// Helper (No templates or such)//func max(x, y int) int {   if x > y {      return x   }   return y}// LoopStructureGraph//// Maintain loop structure for a given CFG.//// Two values are maintained for this loop graph, depth, and nesting level.// For example://// loop        nesting level    depth//----------------------------------------// loop-0      2                0//   loop-1    1                1//   loop-3    1                1//     loop-2  0                2//var loopCounter = 0type LSG struct {   root  *SimpleLoop   loops []*SimpleLoop}func NewLSG() *LSG {   lsg := new(LSG)   lsg.root = lsg.NewLoop()   lsg.root.NestingLevel = 0   return lsg}func (lsg *LSG) NewLoop() *SimpleLoop {   loop := new(SimpleLoop)   loop.basicBlocks = make(map[*BasicBlock]bool)   loop.Children = make(map[*SimpleLoop]bool)   loop.Parent = nil   loop.header = nil   loop.Counter = loopCounter   loopCounter++   return loop}func (lsg *LSG) AddLoop(loop *SimpleLoop) {   lsg.loops = append(lsg.loops, loop)}func (lsg *LSG) Dump() {   lsg.dump(lsg.root, 0)}func (lsg *LSG) dump(loop *SimpleLoop, indent int) {   loop.Dump(indent)   for ll := range loop.Children {      lsg.dump(ll, indent+1)   }}func (lsg *LSG) CalculateNestingLevel() {   for _, sl := range lsg.loops {      if sl.IsRoot {         continue      }      if sl.Parent == nil {         sl.SetParent(lsg.root)      }   }   lsg.calculateNestingLevel(lsg.root, 0)}func (lsg *LSG) calculateNestingLevel(loop *SimpleLoop, depth int) {   loop.DepthLevel = depth   for ll := range loop.Children {      lsg.calculateNestingLevel(ll, depth+1)      ll.NestingLevel = max(loop.NestingLevel, ll.NestingLevel+1)   }}func (lsg *LSG) NumLoops() int {   return len(lsg.loops)}func (lsg *LSG) Root() *SimpleLoop {   return lsg.root}//======================================================// Testing Code//======================================================func buildDiamond(cfgraph *CFG, start int) int {   bb0 := start   NewBasicBlockEdge(cfgraph, bb0, bb0+1)   NewBasicBlockEdge(cfgraph, bb0, bb0+2)   NewBasicBlockEdge(cfgraph, bb0+1, bb0+3)   NewBasicBlockEdge(cfgraph, bb0+2, bb0+3)   return bb0 + 3}func buildConnect(cfgraph *CFG, start int, end int) {   NewBasicBlockEdge(cfgraph, start, end)}func buildStraight(cfgraph *CFG, start int, n int) int {   for i := 0; i < n; i++ {      buildConnect(cfgraph, start+i, start+i+1)   }   return start + n}func buildBaseLoop(cfgraph *CFG, from int) int {   header := buildStraight(cfgraph, from, 1)   diamond1 := buildDiamond(cfgraph, header)   d11 := buildStraight(cfgraph, diamond1, 1)   diamond2 := buildDiamond(cfgraph, d11)   footer := buildStraight(cfgraph, diamond2, 1)   buildConnect(cfgraph, diamond2, d11)   buildConnect(cfgraph, diamond1, header)   buildConnect(cfgraph, footer, from)   footer = buildStraight(cfgraph, footer, 1)   return footer}var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to this file")func main() {   flag.Parse()   if *cpuprofile != "" {      f, err := os.Create(*cpuprofile)      if err != nil {         log.Fatal(err)      }      pprof.StartCPUProfile(f)      defer pprof.StopCPUProfile()   }   lsgraph := NewLSG()   cfgraph := NewCFG()   cfgraph.CreateNode(0) // top   cfgraph.CreateNode(1) // bottom   NewBasicBlockEdge(cfgraph, 0, 2)   for dummyloop := 0; dummyloop < 15000; dummyloop++ {      FindHavlakLoops(cfgraph, NewLSG())   }   n := 2   for parlooptrees := 0; parlooptrees < 10; parlooptrees++ {      cfgraph.CreateNode(n + 1)      buildConnect(cfgraph, 2, n+1)      n = n + 1      for i := 0; i < 100; i++ {         top := n         n = buildStraight(cfgraph, n, 1)         for j := 0; j < 25; j++ {            n = buildBaseLoop(cfgraph, n)         }         bottom := buildStraight(cfgraph, n, 1)         buildConnect(cfgraph, n, top)         n = bottom      }      buildConnect(cfgraph, n, 1)   }   FindHavlakLoops(cfgraph, lsgraph)   for i := 0; i < 50; i++ {      FindHavlakLoops(cfgraph, NewLSG())   }   fmt.Printf("# of loops: %d (including 1 artificial root node)\n", lsgraph.NumLoops())   lsgraph.CalculateNestingLevel()}

我們借助macbook系統上的time命令來列印程式運行的時間(內核態、用戶態、總時間):

編譯后運行程式:

用戶態耗時23.07s,內核態耗時0.4s,總耗時13.7s(用了兩個核,170%),因為程式里面已經先開啟了pprof統計cpu耗時,直接top命令看:

可以看到,pprof資料采集持續了12.99s,采樣時長是19.49s(還是兩核的緣故),

這里要說一下,無論是cpu耗時統計還是記憶體占用統計,都是間隔采樣,cpu耗時時每隔一段時間(大概是10ms)對呼叫堆疊的函式進行記錄,最后分析在所有的記錄次數中,各個函式出現的次數,包括在運行中的次數,和入堆疊次數(說明它呼叫了別的函式),記憶體占用統計是每分配512K記錄一次分配路徑,

耗時最多的是mapaccess1_fast64,這是運行時中的map讀寫時的資料查詢函式,如果編譯程式時沒有禁用行內,看到的會有所不同,其中會顯示FindHavlakLoops函式,并標識為inline,因為FindHavlakLoops里面就呼叫了FindLoops,所以在編譯器會直接把這個函式展開,用FindLoops替換FindHavlakLoops函式,也可以在不禁用行內編譯時,設定pprof的noinlines開關為true,默認為false,即noinlines=true,

這里看到最大的函式并不是業務函式而是系統函式,那沒啥好優化系統函式的(只能是咱用的不對了唄~),就看是哪里呼叫的這個系統函式:

web mapaccess1_fast64

可以看到,呼叫最多的是DFS和FindLoops,那就看看這倆函式里面是怎么使用map的,來看DFS:

可以看到,DFS函式里耗時較長又是map操作的,是242 246 和250三行,對于這里的優化方法,是用list結構代替map結構,因為能用list達到效果的情況下沒必要用map,這個咋一看沒問題,但好像又有點別扭對吧?其實是因為這個程式的本身特點,這里有明顯的一點,就是BasicBlock結構的特殊性,本身的Name屬性就是自身的索引下標,查找某個BasicBlock不需要使用map來記錄,直接通過下標訪問顯然是最低的時間復雜度(關于map和list的時間復雜度就不多說了,map看起來是O1,但其實沒有考慮hash計算本身的程序和解決hash沖突的成本,而list是必然的O1),通過把這部分的map修改為list資料結構,版本H2,再編譯查看耗時情況:

此時耗時降低到了8s,再次查看cpu耗時分布:

可以看到,top1變成了scanobject函式,不再是map讀寫了,看到這個函式,以及下邊的mallocgc,我們要知道,這都是記憶體相關的,前者是記憶體物件掃描的處理函式,是垃圾回收三色標記的一個程序,后者則是直接管理記憶體分配和回收的(注意是同時負責記憶體分配和回收,malloc && gc),這說明目前程式花了很多時間在進行記憶體管理,看比例是8%,那就要分析一下了,是什么東西產生了這么多記憶體變化,合不合理,分析記憶體,就是pprof的memprofile功能了,添加一下相關的記憶體統計代碼,具體怎么添加大家肯定都會,就不貼了(網上多得是),添加完之后,重新編譯生成版本H3,執行H3,生成對應的記憶體監測檔案:

查看記憶體的分配情況,在這里不禁止行內,因為在實際運行時該行內的函式也是會被展開替換掉的:

可以看到,節點創建是第一,FindLoops是第二,因為創建BasicBlock首先很簡單,沒有復雜的程序,其次這是程式中的一個基礎物件結構,若要改結構體,那可能涉及到演算法也得改,這個顯然是不合適的,不僅可能改出bug,還可能收益不高,那我們就順著看第二位的FindLoops,這個就是前邊我們看到的呼叫mapaccess內置函式的另一個業務函式,所以優化的方法跟上面類似,還是優化map的使用,替換為list,這里有一點特殊的是,替換成list之后,map的追加需要改一下,加一個判斷重復的邏輯,所以新加了一個appendUnique方法,再次編譯,版本H4,查看耗時:

這時程式耗時降到了5.6s,再次查看記憶體分配,確認優化效果:

可以看到,FindLoops函式已經不在高位,記憶體消耗降下去了,再看一下此時的cpu耗時分布:

可以看到,FindLoops成為top1,scanobject函式成了第二位,這就對了,因為我們就是要讓cpu更多的去運行業務代碼,把時間花到真正需要的地方

這時就可以進行下一輪的性能優化了(這就是性能調優,一遍一遍的排查耗時,壓縮不必要的cpu時間,壓榨計算性能),繼續看一下此時FindLoops到底在哪兒化的時間多,是否合理:

從各個陳述句的耗時來看,好像沒啥大問題,沒有很離譜的耗時(360ms那行是因為回圈的緣故),這說明編碼上沒有大問題,按照我們前邊一開始說的程式優化的步驟,就需要往上找了,看能不能優化邏輯來減少不必要的計算,以達到提升性能的目的(即,每一個計算步驟處理的都沒啥大問題了,那能不能少算點兒),這里FindLoops在程式入口,花了不少時間來初始化一系列臨時變數,加起來有180ms,這就意味著每次呼叫FindLoops函式,都要先花180ms的準備作業,這部分臨時變數的多次創建+初始化,可以通過加記憶體快取的方式來減少重復創建和申請,這里涉及到的標準解法其實就是物件池(像Go創建的web服務,在并發量很高的情況下,每一次http請求的決議都需要創建物件+申請記憶體+反序列這一系列的初始動作,這時就可以借助sync.Pool來減少這種重復作業,提升性能),同理,這里也是加入了一個記憶體快取物件cache:

把原本的記憶體申請初始化程序做了替換,在原有快取不夠的情況下,申請新的變數,否者截取原有快取使用,減少記憶體申請:

調整完畢后,編譯新版本H5,再看下耗時:

這時候程式的耗時已經降到4.1s,相比一開始的13.7s,已經提升了兩倍多,看下現在的cpu耗時分布:

可以看到,相比上次的分布,前兩位都是業務代碼函式了,說明進一步提高了業務代碼的耗時占比,降低了無關系統函式的負載,這種直接使用全域變數的方式加cache不是并發安全的,但是因為這里程式的邏輯本身也不是并發的,所以這里沒問題,

到這里,實操的優化程序就走完了,提煉總結一下優化的步驟和使用的主要方法命令有:

1.通過top5查看cpu耗時:確定是字典資料操作占比最大;

2.通過web命令查看是誰主要呼叫了字典操作函式:確定有DFS和FindLoops;

3.使用list 命令查看DFS函式中每行代碼的耗時,找到跟map操作相關的部分,確定優化方法:把map換成list;

4.重新運行,再用top命令查看cpu耗時分布:找到耗時最大的是記憶體物件掃描函式scanobject;

5.加入記憶體剖析代碼,運行查看記憶體分配的大小分布:確定FindLoops占用較大;

6.使用list命令查看FindLoops函式的每行記憶體開銷:確定是map創建占用記憶體較大;

7.同樣使用list資料結構替換map,重新運行,再看記憶體大小分布:確定FindLoops已不在高位,同時再看CPU耗時時,scanobject已經降下去了,目的達到;

8.此時又開始新的一輪排查,繼續查看cpu耗時排行榜,FindLoops耗時居首;

9.繼續使用list方法看FindLoops函式每行的耗時,沒有明顯的問題,那就要換思路,從排查編碼問題轉換為排查邏輯問題,減少計算程序:這里是加快取;

10.加完快取看到性能明顯提升了,說明優化對了,這時又該回圈進入下一輪的排查的優化了,以此往復,,,直到壓榨出我們能達到的最大性能!

以上就是本次程式優化的整體步驟和思路,程序中秉持的思路方法是一貫的,就是不斷的用pprof排查top級的函式耗時和記憶體占用,通過優化資料結構、減少不必要的計算程序、降低記憶體分配和回收的負擔等方式來提升性能,這一點對所有的程式都適用,也是后續可以借鑒的方法論

兩點感悟

1.優化程式的大前提是你一定要對程式有足夠深入的理解! (或者說我們不能優化程式優化出bug來啊,,,),最后生產H6版本,之所以又對性能提升了一倍,全是建立在作者對程式完全理解的基礎之上的,所以他才可以調整回圈邏輯,復用程式物件,調整呼叫邏輯等等,這一層再往上層思考,就到了程式邏輯,系統架構內置整體的業務架構層面了,這其實就又回到了一開始我們說的程式優化由大到小的總體思路上了,

2.帶GC的語言相比不帶的,反而更考驗記憶體管理的技術,Go語言在開發效率上的提升,是把垃圾回收交給了系統來處理,但別忘了,消耗的依然是我們自己的cpu時間(羊毛,那不得從羊身上來...),所以我們在使用每種結構體,進行每種計算操作時,都要明白其背后涉及的記憶體開銷,要把記憶體變化放到潛意識里去管理,

以上就是本次pprof優化Go程式的實操程序及總結感想,供參考,感謝~

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/540122.html

標籤:其他

上一篇:網易云VIP音樂NCM檔案轉MP3,C語言版本。

下一篇:全自動化資料洞察!資料分布對比可視化!?

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more