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

cam*_*err 5 java performance iterator stream go

作为Java开发人员,我目前正在研究Go,因为我觉得它是一种有趣的语言.

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

Java代码示例如下:

public static void main(String[] args) {
    long start = System.currentTimeMillis();

    Stream<Container> s = Stream.from(new Iterator<Container>() {
        int i = 0;

        @Override
        public boolean hasNext() {
            return i < 10000000;
        }

        @Override
        public Container next() {
            return new Container(i++);
        }
    });

    s = s.map((Container _source) -> new Container(_source.value * 2));

    int j = 0;
    while (s.hasNext()) {
        s.next();
        j++;
    }

    System.out.println(System.currentTimeMillis() - start);

    System.out.println("j:" + j);
}

public static class Container {

    int value;

    public Container(int v) {
        value = v;
    }

}
Run Code Online (Sandbox Code Playgroud)

map函数为:

return new Stream<R>() {
        @Override
        public boolean hasNext() {
            return Stream.this.hasNext();
        }

        @Override
        public R next() {
            return _f.apply(Stream.this.next());
        }
    };
Run Code Online (Sandbox Code Playgroud)

Stream类只是一个扩展java.util.Iterator自定义方法添加到它.其他方法map与标准Java StreamAPI 不同.

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

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")
}
Run Code Online (Sandbox Code Playgroud)

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

我是Go的新手(几小时前开始)所以如果我做了非常糟糕的事情,请放纵:-)

谢谢!

icz*_*cza 6

另一个答案非常"戏剧性地"改变了原始任务,并恢复了一个简单的循环.我认为它是不同的代码,因此,它不能用于比较执行时间(该循环也可以用Java编写,这会产生更短的执行时间).

现在让我们试着保持手头问题的"流式传输方式".

事先注意:

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

这也不是衡量执行时间的正确方法,因为第一次运行可能会慢几倍.有关详细信息,请参阅代码和性能顺序.

创世纪

你的原始Go提议在我的电脑上运行大约1.1秒,与你的大致相同.

删除interface{}项目类型

Go没有仿制药,试图模仿这种行为与interface{}相同的,并有严重影响性能,如果你要使用的值是一个基本类型(如int)或一些简单结构(如围棋相当于你的Java Container类型).请参阅:反射定律#界面的表示.int在接口中包装(或任何其他具体类型)需要创建一个(类型;值)对,保存要包装的动态类型和值(创建该对还涉及复制被包装的值;请参阅对此进行分析.回答切片如何包含自身?).此外,当您想要访问该值时,您必须使用类型断言,这是一个运行时检查,因此编译器无法帮助优化(并且检查将增加代码执行时间)!

所以我们不要使用interface{}我们的项目,而是使用具体的类型:

type Container struct {
    value int
}
Run Code Online (Sandbox Code Playgroud)

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

type Mapper func(Container) Container
Run Code Online (Sandbox Code Playgroud)

此外,我们可以利用嵌入作为设置方法Iterator是其的一个子集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)
}
Run Code Online (Sandbox Code Playgroud)

执行时间:210毫秒.很好,我们已经加速了5次,但我们远远没有Java的Stream表现.

"删除" 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)
}
Run Code Online (Sandbox Code Playgroud)

执行时间:50毫秒,与之前的解决方案相比,我们再次加速了4倍!现在,这与Java解决方案的数量级相同,而且我们没有从"流式传输方式"中丢失任何东西.提问者提案的总体收益:快了22倍.

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

关于相同的表现

现在我们谈论的是大致相同的代码,这些代码用不同的语言完成非常简单的基本任务.如果他们正在完成基本任务,那么一种语言可能比另一种语言做得更好.

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

进一步改进

这种"流畅"的方式可能不是解决您试图解决的问题的"Go"方式.这只是Java解决方案的"镜像"代码,使用了更多惯用的Go结构.

相反,你应该利用Go对并发性的出色支持,即goroutines(参见go语句),它比Java的线程更有效,以及其他语言结构,如通道(请参阅回答什么是golang通道?)和select语句.

正确地将原始大任务分块/分区为较小的任务,goroutine工作池可能非常强大,可以处理大量数据.请参阅 Go中的这是一个惯用的工作线程池吗?

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

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

(尝试比较Go语言规范Java语言规范.我个人多次阅读Go的lang规范,但永远不会到达Java的末尾.)


Ken*_*ant 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))
Run Code Online (Sandbox Code Playgroud)

更严重的是,在一条条目上的地图是相同的,这是一个更具惯用性的上述版本,可以使用任何类型的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))
}
Run Code Online (Sandbox Code Playgroud)

会使运营更加昂贵的事情 -

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

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

entries.Map(func(e *Entry) *Entry {
    return &Entry{Value: 2 * e.Value}
})
Run Code Online (Sandbox Code Playgroud)

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

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