简单数据流:与Java相比,速度极慢

时间:2017-03-16 17:54:59

标签: java performance go stream iterator

作为一名Java开发人员,我目前正在考虑Go,因为我认为它是一种有趣的语言。

首先,我决定采用我几个月前写的一个简单的Java项目,并在Go中重新编写它来比较性能和(主要是实际)比较代码的可读性/复杂性。

Java代码示例如下:

map

return new Stream<R>() { @Override public boolean hasNext() { return Stream.this.hasNext(); } @Override public R next() { return _f.apply(Stream.this.next()); } }; 函数的位置:

Stream

java.util.Iterator类只是map的扩展,可以为其添加自定义方法。除Stream之外的其他方法与标准Java package main import ( "fmt" ) type Iterator interface { HasNext() bool Next() interface{} } type Stream interface { HasNext() bool Next() interface{} Map(transformer func(interface{}) interface{}) Stream } /////////////////////////////////////// type incremetingIterator struct { i int } type SampleEntry struct { value int } func (s *SampleEntry) Value() int { return s.value } func (s *incremetingIterator) HasNext() bool { return s.i < 10000000 } func (s *incremetingIterator) Next() interface{} { s.i = s.i + 1 return &SampleEntry{ value: s.i, } } func CreateIterator() Iterator { return &incremetingIterator{ i: 0, } } /////////////////////////////////////// type stream struct { source Iterator } func (s *stream) HasNext() bool { return s.source.HasNext() } func (s *stream) Next() interface{} { return s.source.Next() } func (s *stream) Map(tr func(interface{}) interface{}) Stream { return &stream{ source: &mapIterator{ source: s, transformer: tr, }, } } func FromIterator(it Iterator) Stream { return &stream{ source: it, } } /////////////////////////////////////// type mapIterator struct { source Iterator transformer func(interface{}) interface{} } func (s *mapIterator) HasNext() bool { return s.source.HasNext() } func (s *mapIterator) Next() interface{} { return s.transformer(s.source.Next()) } /////////////////////////////////////// func main() { it := CreateIterator() ss := FromIterator(it) ss = ss.Map(func(in interface{}) interface{} { return &SampleEntry{ value: 2 * in.(*SampleEntry).value, } }) fmt.Println("Start") for ss.HasNext() { ss.Next() } fmt.Println("Over") } API不同。

无论如何,要重现这一点,我写了以下Go代码:

if(!empty($value->jenis_perusahaan))
{}

两者都产生相同的结果,但是当Java需要大约20ms时,Go需要1050ms(使用10M项目,测试运行几次)。

我对Go来说很新(几小时前开始)所以如果我做了一些非常糟糕的事情,请放纵: - )

谢谢!

3 个答案:

答案 0 :(得分:6)

另一个答案完全改变了原来的任务&#34;戏剧性地#34;并且恢复了一个简单的循环。我认为它是不同的代码,因此,它不能用于比较执行时间(该循环也可以用Java编写,这会缩短执行时间)。

现在让我们尝试保持&#34;流媒体方式&#34;手头的问题。

事先注意:

事先要注意一件事。在Java中,System.currentTimeMillis()的粒度可能大约为10毫秒(!!),与结果的数量级相同!这意味着在Java的20毫秒内错误率可能巨大!因此,您应该使用System.nanoTime()来衡量代码执行时间!有关详细信息,请参阅Measuring time differences using System.currentTimeMillis()

这也不是衡量执行时间的正确方法,因为第一次运行可能会慢几倍。有关详细信息,请参阅Order of the code and performance

您的原始Go提案在我的计算机上大致运行 1.1秒,这与您的大致相同。

删除interface{}项类型

Go doesn't have generics,尝试使用interface{}模仿此行为相同,如果您要使用的值是基本类型,则会对性能产生严重影响(例如int)或一些简单的结构(比如Go等效于Java Container类型)。见:The Laws of Reflection #The representation of an interface。在接口中包装int(或任何其他具体类型)需要创建一个(类型;值)对,保存要包装的动态类型和值(创建该对也涉及复制被包装的值;请参阅在答案How can a slice contain itself?)中对此进行分析。此外,当您想要访问该值时,您必须使用type assertion这是运行时检查,因此编译器无法帮助优化(并且检查)将添加到代码执行时间)!

因此,我们不要将interface{}用于我们的项目,而是针对我们的案例使用具体类型:

type Container struct {
    value int
}

我们将在迭代器和流的下一个方法中使用它:Next() Container,并在mapper函数中使用:

type Mapper func(Container) Container

我们也可以使用embedding,因为Iterator的{​​{3}}是Stream的一部分。

不用多说,这是一个完整的,可运行的例子:

package main

import (
    "fmt"
    "time"
)

type Container struct {
    value int
}

type Iterator interface {
    HasNext() bool
    Next() Container
}

type incIter struct {
    i int
}

func (it *incIter) HasNext() bool {
    return it.i < 10000000
}

func (it *incIter) Next() Container {
    it.i++
    return Container{value: it.i}
}

type Mapper func(Container) Container

type Stream interface {
    Iterator
    Map(Mapper) Stream
}

type iterStream struct {
    Iterator
}

func NewStreamFromIter(it Iterator) Stream {
    return iterStream{Iterator: it}
}

func (is iterStream) Map(f Mapper) Stream {
    return mapperStream{Stream: is, f: f}
}

type mapperStream struct {
    Stream
    f Mapper
}

func (ms mapperStream) Next() Container {
    return ms.f(ms.Stream.Next())
}

func (ms mapperStream) Map(f Mapper) Stream {
    return nil // Not implemented / needed
}

func main() {
    s := NewStreamFromIter(&incIter{})
    s = s.Map(func(in Container) Container {
        return Container{value: in.value * 2}
    })

    fmt.Println("Start")
    start := time.Now()

    j := 0
    for s.HasNext() {
        s.Next()
        j++
    }

    fmt.Println(time.Since(start))
    fmt.Println("j:", j)
}

执行时间: 210 ms 。很好,我们已经加强了 5次,但我们远远没有达到Java的Stream性能。

&#34;删除&#34; IteratorStream类型

由于我们无法使用泛型,接口类型IteratorStream并不真正需要成为接口,因为我们需要新的如果我们想要使用它们来定义其他类型的迭代器和流,那么它们的类型。

接下来我们要做的是删除StreamIterator,我们使用它们的具体类型,即上面的实现。这根本不会损害可读性,事实上解决方案更短:

package main

import (
    "fmt"
    "time"
)

type Container struct {
    value int
}

type incIter struct {
    i int
}

func (it *incIter) HasNext() bool {
    return it.i < 10000000
}

func (it *incIter) Next() Container {
    it.i++
    return Container{value: it.i}
}

type Mapper func(Container) Container

type iterStream struct {
    *incIter
}

func NewStreamFromIter(it *incIter) iterStream {
    return iterStream{incIter: it}
}

func (is iterStream) Map(f Mapper) mapperStream {
    return mapperStream{iterStream: is, f: f}
}

type mapperStream struct {
    iterStream
    f Mapper
}

func (ms mapperStream) Next() Container {
    return ms.f(ms.iterStream.Next())
}

func main() {
    s0 := NewStreamFromIter(&incIter{})
    s := s0.Map(func(in Container) Container {
        return Container{value: in.value * 2}
    })

    fmt.Println("Start")
    start := time.Now()

    j := 0
    for s.HasNext() {
        s.Next()
        j++
    }

    fmt.Println(time.Since(start))
    fmt.Println("j:", j)
}

执行时间: 50 ms ,与之前的解决方案相比,我们再次加强 4次!现在,这与Java解决方案的数量级相同,而且我们从流媒体方式中没有丢失任何内容。#34;。来自提问者提案的总体收益: 22次更快。

鉴于在Java中您使用System.currentTimeMillis()来衡量执行,这甚至可能与Java的性能相同。 Asker证实:它是一样的!

关于相同的表现

现在我们大致谈论的是&#34;同一&#34;用不同语言执行非常简单的基本任务的代码。如果他们正在执行基本任务,那么一种语言可能比另一种语言做得更好。

还要记住,Java是一个成熟的成年人(超过21岁),并且有大量的时间来进化和优化;实际上,Java method set(即时编译)对于长时间运行的进程(例如你的进程)做得非常好。 Go更年轻,仍然只是一个 kid (将在11天后成为5岁),并且在可预见的未来可能会比Java更好的性能改进。

进一步改进

这&#34;流线型&#34;方式可能不是&#34; Go&#34;解决你试图解决的问题的方法。这只是&#34;镜像&#34; Java解决方案的代码,使用Go的更多惯用结构。

相反,您应该利用Go对并发性的出色支持,即 goroutines (请参阅JIT语句),它们比Java的线程更有效,和其他语言结构,例如go(请参阅回答channels)和What are golang channels used for?声明。

正确地将原始大任务分块/分区为较小的任务, goroutine worker pool 可能非常强大,可以处理大量数据。看到 select

此外,您在评论中声明&#34;我没有10M项目需要处理,但更多10G不适合记忆&#34; 。如果是这种情况,请考虑IO时间以及您从中处理数据的外部系统的延迟。如果这需要很长时间,它可能会超出应用程序中的处理时间,并且应用程序的执行时间可能无关紧要(根本不会)。

Go并不是要在执行时间之外压缩每一纳秒,而是为您提供简单,极简主义的语言和工具,通过这些工具,您可以轻松地(通过编写简单的代码)控制和利用您的可用资源(例如goroutines和多核CPU)。

(尝试比较Is this an idiomatic worker thread pool in Go?Go language spec。我个人多次阅读Go的规范,但永远不会到达Java的末尾。峰)

答案 1 :(得分:5)

这是一个有趣的问题,因为它触及了Java和Go之间的差异核心,并强调了移植代码的难度。在减去所有机器(这里的时间~50ms)时,这是同样的事情:

values := make([]int64, 10000000)
start := time.Now()
fmt.Println("Start")
for i := int64(0); i < 10000000; i++ {
    values[i] = 2 * i
}
fmt.Println("Over after:", time.Now().Sub(start))

更严重的是,在一条条目上的地图是相同的,这是一个更具惯用性的上述版本,可以使用任何类型的Entry结构。这实际上在30ms的机器上比上面的for循环更快的时间(任何人都在解释原因?),所以可能类似于你的Java版本:

package main

import (
    "fmt"
    "time"
)

type Entry struct {
    Value int64
}

type EntrySlice []*Entry

func New(l int64) EntrySlice {
    entries := make(EntrySlice, l)
    for i := int64(0); i < l; i++ {
        entries[i] = &Entry{Value: i}
    }
    return entries
}

func (entries EntrySlice) Map(fn func(i int64) int64) {
    for _, e := range entries {
        e.Value = fn(e.Value)
    }
}

func main() {

    entries := New(10000000)

    start := time.Now()
    fmt.Println("Start")
    entries.Map(func(v int64) int64 {
        return 2 * v
    })
    fmt.Println("Over after:", time.Now().Sub(start))
}

会使操作更加昂贵的事情 -

  • 绕过界面{},不要这样做
  • 构建单独的迭代器类型 - 使用范围或for循环
  • 分配 - 因此构建新类型以存储答案,进行适当的转换

重新使用接口{},我会避免这种情况 - 这意味着你必须为每种类型写一个单独的地图(比方说),而不是一个很大的困难。而不是构建迭代器,范围可能更合适。重新转换到位,如果为每个结果分配新的结构,它会对垃圾收集器施加压力,使用像这样的Map函数会慢一个数量级:

entries.Map(func(e *Entry) *Entry {
    return &Entry{Value: 2 * e.Value}
})

要将数据流分割为块,并执行与上面相同的操作(如果依赖于先前的计算,则保留最后一个对象的备忘录)。如果你有独立的计算(不是在这里),你也可以扇出一大堆goroutine来完成这项工作,并且如果它有很多的话就更快地完成它(这有开销,在简单的例子中它不会更快) 。

最后,如果您对使用go进行数据处理感兴趣,我建议您访问这个新网站:http://gopherdata.io/

答案 2 :(得分:0)

作为对先前评论的补充,我更改了Java和Go实现的代码以运行测试100次。

这里有趣的是,Go需要69到72毫秒的恒定时间。

Owever,Java第一次占用71ms(71ms,19ms,12ms),然后是5到7ms。

从我的测试和理解来看,这是因为JVM需要一些时间来正确加载类并进行一些优化。

最后,我仍然有10倍的性能差异,但我并没有放弃,我会尝试更好地了解Go如何努力让它更快:)