【问题标题】:Can I optimise this further so that it runs faster?我可以进一步优化它以使其运行得更快吗?
【发布时间】:2017-10-31 17:16:21
【问题描述】:

正如您在以下 pprof 输出中所见,我有这些嵌套的 for 循环,这些循环占用了我程序的大部分时间。源码是golang,代码解释如下:

  8.55mins    1.18hrs     20:   for k := range mapSource {
  4.41mins    1.20hrs     21:           if positions, found := mapTarget[k]; found {
         .          .     22:                   // save all matches
  1.05mins   1.05mins     23:                   for _, targetPos := range positions {
  2.25mins   2.33mins     24:                           for _, sourcePos := range mapSource[k] {
     1.28s     15.78s     25:                                   matches = append(matches, match{int32(targetPos), int32(sourcePos)})
         .          .     26:                           }
         .          .     27:                   }
         .          .     28:           }
         .          .     29:   }

目前我使用的结构是 2 map[int32][]int32,targetMap 和 sourceMap。

对于给定的键,这些映射包含一个整数数组。现在我想在两个映射中找到匹配的键,并将元素的组合保存在数组中。

例如:

sourceMap[1] = [3,4]
sourceMap[5] = [9,10]

targetMap[1] = [1,2,3]
targetMap[2] = [2,3]
targetMap[3] = [1,2]

唯一的共同键是1,结果是[(3,1), (3,2), (3,3), (4,1), (4,2), (4,3)]

是否有任何可能的方式(更合适的数据结构或其他)可以提高我的程序速度?

在我的例子中,map 可以包含 1000 到 150000 个键,而里面的数组通常很小。

编辑:并发不是一个选项,因为它已经在多个线程中同时运行了多次。

【问题讨论】:

  • 一方面,您可以获取最外层循环中的值 (for k := -> for k,v :=),这将消除最内层循环中的额外查找 (:= range mapSource[k] -> := range v )。也无需将targetPossourcePos 转换为int32,因为根据您的问题它们已经是int32。总体而言,尽管地图可能是最有效的结构。如果您对最终数据集的大小或最小大小有所了解,可以使用该容量预先分配 matches
  • 数组总是排序的(就像你的例子一样)?
  • 不确定它会有多大帮助,但for _, x := range slice { 格式将重新声明 x 并在每次迭代时为其分配值。尝试改用索引值,看看它是否有帮助。 for i := range slice { ... slice[i]
  • @dave 是的,他们是
  • 也许保留另一个源键映射 ==> 目标键。这将删除每个源键的目标映射的顺序迭代。

标签: performance go complexity-theory


【解决方案1】:

我可以进一步优化它以使其运行得更快吗?

有没有可能的方法(更合适的数据结构或 什么)可以提高我的程序的速度?

大概吧。


XY problem 正在询问您的 尝试的解决方案,而不是您的实际问题。这将导致 人们浪费了大量的时间和精力 寻求帮助,以及那些提供帮助的人。


我们甚至没有关于您的问题的最基本信息、原始输入数据的形式、内容和频率的描述,以及您想要的输出。哪些原始数据应该驱动基准?

我创建了一些虚构的原始数据,产生了一些虚构的输出和结果:

BenchmarkPeterSO-4   30    44089894 ns/op    5776666 B/op      31 allocs/op
BenchmarkIvan-4      10   152300554 ns/op   26023924 B/op    6022 allocs/op

您的算法可能很慢。

【讨论】:

    【解决方案2】:

    我可能会这样做,以便我可以同时做一些工作:

    https://play.golang.org/p/JHAmPRh7jr

    package main
    
    import (
        "fmt"
        "sync"
    )
    
    var final [][]int32
    var wg sync.WaitGroup
    var receiver chan []int32
    func main() {
        final = [][]int32{}
        mapTarget := make(map[int32][]int32)
        mapSource := make(map[int32][]int32)
        mapSource[1] = []int32{3, 4}
        mapSource[5] = []int32{9, 10}
    
        mapTarget[1] = []int32{1, 2, 3}
        mapTarget[2] = []int32{2, 3}
        mapTarget[3] = []int32{1, 2}
        wg = sync.WaitGroup{}
        receiver = make(chan []int32)
        go func() {
            for elem := range receiver {
                final = append(final, elem)
                wg.Done()
            }
        }()
        for k := range mapSource {
            if _, ok := mapTarget[k]; ok {
                wg.Add(1)
                go permutate(mapSource[k], mapTarget[k])
            }
        }
        wg.Wait()
        fmt.Println(final)
    
    }
    
    func permutate(a, b []int32) {
        for i := 0; i < len(a); i++ {
            for j := 0; j < len(b); j++ {
                wg.Add(1)
                receiver <- []int32{a[i], b[j]}
            }
        }
        wg.Done()
    }
    

    您甚至可能想看看您是否从中得到任何好处:

    for k := range mapSource {
          wg.Add(1)
          go func(k int32) {
              if _, ok := mapTarget[k]; ok {
                  wg.Add(1)
                  go permutate(mapSource[k], mapTarget[k])
              }
              wg.Done()
          }(k)
     }
    

    【讨论】:

    • 这段代码有一个竞争条件,因为有多个 goroutines 同时写入final。如果您将其包装在互斥锁中,我的预感是,此版本的执行速度将比原始版本慢得多,因为您将 constant 争用该锁。
    • 重点是 (1) 编写的代码将不起作用 (2) 你可以用通道解决这个问题,但是你又回到了一个单一的 goroutine 正在做所有附加的地方到数组中,除非你在混合中添加了一大堆 goroutine,所以我保证它的性能会比 OP 的代码差。
    • @Ivan,我很想看看你的原始数据集的基准。
    【解决方案3】:

    最好的优化可能首先涉及更改源数据结构和目标数据结构,这样您就不必进行太多迭代,但是如果不了解更多关于您要解决的潜在问题是什么,就很难确定,以及如何生成地图。

    但是,根据确切的数字,有一个优化可以让您获得大约 2 倍的提升(只是有根据的猜测)。

    var sources, targets []int32
    
    for k, srcPositions := range mapSource {
        if tgtPositions, found := mapTarget[k]; found {
            sources = append(sources, srcPositions...)
            targets = append(targets, tgtPositions...)
        }
    }
    
    matches = make([]match, len(sources) * len(targets))
    i := 0
    for _, s := range(sources) {
        for _, t := range(targets) {
            matches[i] = match{s, t}
            i++
        }
    }
    

    总体思路是尽量减少必须完成的复制量,并提高内存引用的局部性。我认为这是你可以用这个数据结构做的最好的事情。我的直觉是,这不是解决潜在问题的最佳数据结构,而且还有更大的收获。

    【讨论】:

      【解决方案4】:

      一开始我在想:

      1. 计算一批共有的keys,并计算出最终的slice size。

      2. 使用步骤 1 计算的容量制作切片。

      3. 一一追加。

      然后是下一个结构,但它不会将最终结果生成为数组,而是所有附加工作都只是链接节点。

      type node struct {
          val    int
          parent *node
          next   *node
          child  *node
      }
      
      type tree struct {
          root  *node
          level int
      }
      
      var sourceMap map[int]*tree
      

      【讨论】:

        猜你喜欢
        • 2022-09-30
        • 2016-02-19
        • 2020-01-13
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-06-27
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多