为什么这个简单的Go程序比它的Node.js对应程序慢?

时间:2017-06-20 01:53:20

标签: javascript performance go

我正在尝试使用Go来实现叶子上具有值的二叉树,即相当于:

data Tree a 
  = Node {left: Tree, right: Tree} 
  | Leaf {value: a}

我有两个问题:1,我无法找到一种方法来创建一个包含多个构造函数的类型,因此我必须将所有数据放在一个中。 2,我不能使它变成多态,所以我不得不使用interface{}(我猜这是类型系统的“选择退出”?)。这是我能做的最好的事情:

package main

import ("fmt")

type Tree struct {
  IsLeaf bool
  Left *Tree
  Value interface{}
  Right *Tree
}

func build(n int) *Tree {
  if (n == 0) {
    return &Tree{IsLeaf: true, Left: nil, Value: 1, Right: nil}
  } else {
    return &Tree{IsLeaf: false, Left: build(n - 1), Value: 0, Right: build(n - 1)}
  }
}

func sum(tree *Tree) int {
  if (tree.IsLeaf) {
    return tree.Value.(int)
  } else {
    return sum(tree.Left) + sum(tree.Right)
  }
}

func main() {
  fmt.Println(sum(build(23)))
}

它实现了类型并通过对生成的巨大树进行求和来测试它。我已经开始在JavaScript中进行等效的实现(包括构造函数的冗余数据,为了公平):

const build = n => {
  if (n === 0) {
    return {IsLeaf: true, Value: 1, Left: null, Right: null};
  } else {
    return {IsLeaf: false, Value: 0, Left: build(n - 1), Right: build(n - 1)};
  }
}

const sum = tree => {
  if (tree.IsLeaf) {
    return tree.Value;
  } else {
    return sum(tree.Left) + sum(tree.Right);
  }
}

console.log(sum(build(23)));

我已使用go build test.go编译了Go代码,并使用time ./test进行了运行。我用node test.js运行了Node.js代码。经过多次测试后,Go程序平均运行大约2.5秒,而Node.js运行大约1.0秒。

这使得这个简单程序的Go 2.5x比Node.js慢,这是不正确的,因为Go是一个带有成熟编译器的静态类型编译语言,而JavaScript是一个无类型的,解释的。

为什么我的Go程序这么慢?我错过了一些编译器标志,还是代码有问题?

3 个答案:

答案 0 :(得分:10)

摘要

由于类型断言和reduntant数据,此代码较慢。

Go不鼓励你在炎热的地方写类型断言:

tree.Value.(int) 

取出这种类型的断言(并相应地将Value更改为int类型),并且您的代码执行速度大约快两倍(应该以您的节点示例的速度为单位)。

也可以取出冗余数据,代码执行速度提高约三倍。请参阅帖子末尾的游乐场示例。

详细

我认为这是设计的错误,而不是实施。阅读你的问题,我认为Go的类型系统如何运作存在一些混乱。

Go的对象模型不鼓励您使用catch-all类型进行多态性(有关Go的多态性的讨论,请参阅this excellent answer的上半部分)。

在JavaScript世界中,每个对象都是特定类型。在Go中,如果struct符合interface的合同,则structs可被视为特定的接口类型。请注意,struct不是对象 - 您所谓的构造函数只是interface{}初始化程序。

可以将在type Tree struct { IsLeaf bool Left *Tree Value int Right *Tree } ..... func sum(tree *Tree) int { if (tree.IsLeaf) { return tree.Value } else { return sum(tree.Left) + sum(tree.Right) } } 上运行的Go代码编写为所有类型的占位符,但该语言并不真正鼓励您以这种方式编写代码(正如您在问题中指出的那样,它是编写干净代码的挑战与在JavaScript中编写代码的方式相同。

因为Go实际上没有对象,所以尝试编写在Go中感觉非常面向对象的代码将具有挑战性(此外,Go没有标准继承或方法重载)。出于这个原因,我认为你的代码不是Go鼓励程序员编写的代码。所以,这不是一个公平的考验。

Type assertion is slow。 (我并不是围绕Go的内部设计,但这肯定表明程序员不会写出很多类型的断言)。因此,您的代码不具备高效性就不足为奇了。我将您的代码更改为:

IsLeaf

我的机器加速了2倍。

可能还有其他优化 - 您可以删除Value,并且不需要在非叶节点上存储值(或者,您可以在整个树中分配值,因此不要浪费Value)。我不知道JavaScript是否优化了这些不必要的Leaf,但我不相信Go会这样做。

所以,我认为你的代码使用的内存比它需要的多得多,这对性能也无济于事。

重要吗?

我个人并不相信“我在X和Y中编写了这个程序,并发现Y更慢”,特别是因为很难在框架之间进行公平比较。还有很多其他的变异来源 - 程序员知识,机器负载,启动时间等。

要做一个公平的测试,你需要编写每种语言惯用的代码,但也要使用相同的代码。我认为实现这两者并不现实。

如果此代码是您的特定方案,并且性能是主要目标,那么此测试可能会有所帮助。但是,否则我认为这不是一个非常有意义的比较。

在规模上,我希望其他考虑因素能够超越你创建和遍历树的速度。有一些技术问题,如数据吞吐量和负载下的性能,还有程序员时间和维护工作等问题。

但是,学术练习很有意思。编写这样的代码是查找框架边缘的好方法。

编辑:我尝试使你的代码更像Go,它的优势在于比原版快3倍的速度。:

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

游乐场的树有点沉重,但您可以复制并粘贴代码以在本地运行。

我还没有尝试过更多优化,例如并行构建树。

您可以扩展此设计以获得所需的多态行为(通过提供替代Sum()实现),但我不确定Sum()对非数字类型的含义。不知道如何定义{{1}}是一种很好的例子,这种思维会导致不决定通过泛型包含多态性。

答案 1 :(得分:3)

我认为这可能是有益的。这是我对平衡二叉树的实现,它使用递归,转到例程和通道。它本来是用作包,这就是我使用导出和未导出功能的原因。导出的函数是你应该使用的/ mod等。我很久以前写过它...有很多东西可以写得更好..我刚才为你添加了一个Sum函数。 我添加了23个节点,并以每秒1/4的速度获得总和..

更新如果您查看Tree结构我现在保留一个Total字段,我添加了一个名为GetTreeTotal()的新函数。在Add()函数中,我在添加节点时更新该字段。现在sum()不必以质量计算,那就是现在只有Tree的元数据。所以从这个意义上说。超级快。使用类似的逻辑,树上的节点数也可以保存为元数据。知道这些信息,可以加速像TreeToArray()这样的函数,因为可以预先定义切片的大小。减少分配..等等

UPDATE2 这个问题让我很好奇,我重写了下面的代码并将其转换为一个包。 https://github.com/marcsantiago/GoTree迭代插入几乎快3倍(包括基准),但是当插入量非常高时你真的看到了这种差异。

package main

import (
    "encoding/json"
    "errors"
    "fmt"
    "math/rand"
    "sync"
    "time"
)

type node struct {
    Left  *node
    Right *node
    Data  int
}

// Tree ...
type Tree struct {
    Root  *node
    Total int
}

// FindNode ...
func (t *Tree) FindNode(data int) bool {
    newNode := node{
        Data: data,
    }
    if t.Root != nil {
        if t.findNode(t.Root, newNode) != nil {
            return true
        }
    }
    return false
}

func (t *Tree) findNode(search *node, target node) *node {
    var returnNode *node
    if search == nil {
        return returnNode
    }
    if search.Data == target.Data {
        return search
    }
    returnNode = t.findNode(search.Left, target)
    if returnNode == nil {
        returnNode = t.findNode(search.Right, target)
    }
    return returnNode
}

// Add ...
func (t *Tree) Add(data int) {
    t.Total += data
    if data < 0 {
        panic(errors.New("Only submit positive integers"))
    }
    nodeToAdd := node{
        Data: data,
    }
    if t.Root == nil {
        t.Root = new(node)
    }
    if t.Root.Data == 0 {
        t.Root = &nodeToAdd
        return
    }

    t.add(t.Root, nodeToAdd)
    return
}

func (t *Tree) add(oldnode *node, newNode node) {
    if newNode.Data < oldnode.Data {
        if oldnode.Left == nil {
            // t.Total += newNode.Data
            oldnode.Left = &newNode
        } else {
            // t.Total += newNode.Data
            t.add(oldnode.Left, newNode)
        }
    } else if newNode.Data > oldnode.Data {
        if oldnode.Right == nil {
            // t.Total += newNode.Data
            oldnode.Right = &newNode
        } else {
            // t.Total += newNode.Data
            t.add(oldnode.Right, newNode)
        }
    }
    return
}

// InOrderTraversal ...
func (t *Tree) InOrderTraversal() {
    if t.Root != nil {
        currentNode := t.Root
        if currentNode.Left == nil && currentNode.Right == nil {
            fmt.Println(currentNode.Data)
        } else {
            t.inOrderTraversal(currentNode)
        }
    }
    return
}

func (t *Tree) inOrderTraversal(n *node) {
    if n.Left != nil {
        t.inOrderTraversal(n.Left)
    }
    fmt.Println(n.Data)
    if n.Right != nil {
        t.inOrderTraversal(n.Right)
    }
    return
}

// Traversal ...
func (t *Tree) Traversal() {
    if t.Root != nil {
        currentNode := t.Root
        if currentNode.Left == nil && currentNode.Right == nil {
            fmt.Println(currentNode.Data)
        } else {
            t.traversal(currentNode)
        }
    }
    return
}

func (t *Tree) traversal(n *node) {
    fmt.Println(n.Data)
    if n.Left != nil {
        t.traversal(n.Left)
    }

    if n.Right != nil {
        t.traversal(n.Right)
    }
    return
}

// Sum ...
func (t *Tree) Sum() (total int) {
    var wg sync.WaitGroup
    c := make(chan int, 100)
    if t.Root != nil {
        currentNode := t.Root
        if currentNode.Left == nil && currentNode.Right == nil {
            return 1
        }
        wg.Add(1)
        t.sum(currentNode, c, &wg)
    }
    go func() {
        wg.Wait()
        close(c)
    }()
    for n := range c {
        total += n
    }
    return total
}

func (t *Tree) sum(n *node, counter chan int, wg *sync.WaitGroup) {
    defer wg.Done()

    if n.Left != nil {
        wg.Add(1)
        go t.sum(n.Left, counter, wg)
    }

    counter <- n.Data

    if n.Right != nil {
        wg.Add(1)
        go t.sum(n.Right, counter, wg)
    }

    return
}

// CountEdges ...
func (t *Tree) CountEdges() (edges int) {
    c := make(chan int, 10)
    if t.Root != nil {
        currentNode := t.Root
        if currentNode.Left == nil && currentNode.Right == nil {
            return 1
        }
        t.countEdges(currentNode, c)
    }

    for {
        n := <-c
        if n == 0 {
            close(c)
            break
        }
        edges++
    }
    return edges
}

func (t *Tree) countEdges(n *node, counter chan int) {
    if n.Left != nil {
        go t.countEdges(n.Left, counter)
    }

    if n.Left == nil && n.Right == nil {
        counter <- 0
    } else {
        counter <- 1
    }

    if n.Right != nil {
        go t.countEdges(n.Right, counter)
    }
    return
}

// GenerateRandomTree ...
func (t *Tree) GenerateRandomTree() {
    u := time.Now()
    source := rand.NewSource(u.Unix())
    r := rand.New(source)
    arr := r.Perm(1000)
    for _, a := range arr {
        t.Add(a)
    }
    return
}

// GetRootData ...
func (t *Tree) GetRootData() int {
    return t.Root.Data
}

// GetTreeTotal ...
func (t *Tree) GetTreeTotal() int {
    return t.Total
}

// TreeToArray ...
func (t *Tree) TreeToArray() []int {
    ch := make(chan int, 10)
    arr := []int{}
    if t.Root != nil {
        currentNode := t.Root
        if currentNode.Left == nil && currentNode.Right == nil {
            return []int{currentNode.Data}
        }
        t.traversalGetVals(currentNode, ch)
    }

    for {
        n := <-ch
        if n == -1 {
            close(ch)
            break
        }
        arr = append(arr, n)
    }
    return arr
}

func (t *Tree) traversalGetVals(n *node, ch chan int) {
    if n.Left != nil {
        ch <- n.Left.Data
        go t.traversalGetVals(n.Left, ch)
    }

    if n.Right != nil {
        ch <- n.Right.Data
        go t.traversalGetVals(n.Right, ch)
    }
    if n.Left == nil && n.Right == nil {
        ch <- -1
    }
    return
}

// ShiftRoot ...
func (t *Tree) ShiftRoot(newRoot int) {
    arr := t.TreeToArray()
    n := Tree{}
    n.Add(newRoot)
    for _, i := range arr {
        n.Add(i)
    }
    *t = n
}

// PrintTree ...
func (t *Tree) PrintTree() {
    b, err := json.MarshalIndent(t, "", " ")
    if err != nil {
        panic(err)
    }
    fmt.Println(string(b))
}

func main() {
    // t := Tree{}
    // t.GenerateRandomTree()
    // t.PrintTree()
    // fmt.Println("total:", t.Sum())

    t := Tree{}
    t.Add(10)
    t.Add(100)
    t.Add(2)
    t.Add(3)

    fmt.Println(t.Sum()) // should be 115
    fmt.Println(t.GetTreeTotal())

    // t := Tree{}
    // for i := 1; i <= 23; i++ {
    //  t.Add(i)
    // }
    // fmt.Println("total:", t.Sum())

}

答案 2 :(得分:1)

问题主要在于碎片内存分配(通过递归堆栈)。这会导致大量小额分配,随后垃圾收集器也会有大量工作。您可以通过预先分配一个包含所有节点的数组并为分配保留运行索引来避免这种情况:

bar.go

package bar

type Tree struct {
    Left  *Tree
    Value int
    Right *Tree
    IsLeaf bool
}

func build(level int, curridx *int, src *[]Tree) *Tree {
    if level == 0 {
        (*src)[*curridx] = Tree{Left: nil, Value: 1, Right: nil, IsLeaf:true}
        *curridx++
        return &(*src)[*curridx-1]
    } else {
        (*src)[*curridx] = Tree{Left: build(level-1, curridx, src), Value: 1, Right: build(level-1, curridx, src)}
        *curridx++
        return &(*src)[*curridx-1]
    }
}

func sum(tree *Tree) int {
    if (tree.IsLeaf) {
        return tree.Value.(int)
    } else {
        return sum(tree.Left) + sum(tree.Right)
    }
}

bar_test.go

package bar

import "testing"
import "math"

func TestMe(t *testing.T) {
    for x := 0; x < 10; x++ {
        levels := 23
        nrnodes := int(math.Pow(2.0, float64(levels+1))) //there are actually 24 levels
        mapping := make([]Tree, nrnodes, nrnodes)
        index := 0
        t.Error(sum(build(levels, &index, &mapping)))
    }
}

这会使每次迭代的速度提高0.5秒。

请注意以下内容的构建:

go test -cpuprofile cpu.outgo tool pprof cpu.out + web