5

As a Java dev, I'm currently looking at Go because I think it's an interesting language.

To start with it, I decided to take a simple Java project I wrote months ago, and re-write it in Go to compare performances and (mainly, actually) compare the code readability/complexity.

The Java code sample is the following:

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;
    }

}

Where the map function is:

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

        @Override
        public R next() {
            return _f.apply(Stream.this.next());
        }
    };

And the Stream class is just an extension to java.util.Iterator to add custom methods to it. Other methods than map differs from standard Java Stream API.

Anyway, to reproduce this, I wrote the following Go code:

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")
}

Both producing the same result but when Java takes about 20ms, Go takes 1050ms (with 10M items, test ran several times).

I'm very new to Go (started couple of hours ago) so please be indulgent if I did something really bad :-)

Thank you!

icza
  • 389,944
  • 63
  • 907
  • 827
cambierr
  • 391
  • 4
  • 14
  • 6
    As you can see, writing Java in Go doesn't work out so well. It's not a single thing that's making it slow, there's just a lot of extra indirection and allocation for this simple task. – JimB Mar 16 '17 at 18:03
  • Well, it's a very simple exemple of how several data streams are processed, merged, ... and I was expecting Go to give me better results than 50* slower. Any idea of what have I done wrong ? Especially since the same code (adapted), in C++ gives the same results as Java :) – cambierr Mar 16 '17 at 18:09
  • 7
    This program has a lot of extra indirection that makes it hard to optimize. It's possible a future Go compiler could do much better, but most Go isn't written this way, especially if performance is an issue. The structure of Go programs tends to more closely resemble C programs than C++ or Java. – JimB Mar 16 '17 at 18:32
  • Interesting. Would you have suggestions on how to efficiently process this kind of workload ? To give you more context: Each stream is a TimeSerie that will be processed to finally get a single TS (after interpolation, aggregation, ...). Thanks ! – cambierr Mar 16 '17 at 18:35
  • Asking how to process some stream of time series data in Go is too broad a question. – peterSO Mar 16 '17 at 19:20
  • 2
    Well, I think that my sample code is a pretty good example of one of the tasks executed on the different TimeSerie streams. Isn't it ? – cambierr Mar 16 '17 at 19:24
  • 1
    your code has a lot of `interface`. that's interface pollution. https://www.goinggo.net/2016/10/avoid-interface-pollution.html. Try to use `interface` when you wanted to decouple something or getting various type of data. – Gujarat Santana Mar 17 '17 at 03:20
  • 1
    @GujaratSantana Right! I managed to speed up the code by a factor of 5 just by removing interface{} and specifying the type (event if this make me loose the ability to use several types). Anyway, that's still a 1/10 difference in term of performances compared to java. Any other idea ? – cambierr Mar 17 '17 at 07:49

3 Answers3

6

The other answer changed the original task quite "dramatically", and reverted to a simple loop. I consider it to be different code, and as such, it cannot be used to compare execution times (that loop could be written in Java as well, which would give smaller execution time).

Now let's try to keep the "streaming manner" of the problem at hand.

Note beforehand:

One thing to note beforehand. In Java, the granularity of System.currentTimeMillis() could be around 10 ms (!!) which is in the same order of magnitude of the result! This means the error rate could be huge in Java's 20 ms! So instead you should use System.nanoTime() to measure code execution times! For details, see Measuring time differences using System.currentTimeMillis().

Also this is not the correct way to measure execution times, as running things for the first time might run several times slower. For details, see Order of the code and performance.

Genesis

Your original Go proposal runs on my computer roughly for 1.1 seconds, which is about the same as yours.

Removing interface{} item type

Go doesn't have generics, trying to mimic this behavior with interface{} is not the same and have serious performance impact if the value you want to work with is a primitive type (e.g. int) or some simple structs (like the Go equivalent of your Java Container type). See: The Laws of Reflection #The representation of an interface. Wrapping an int (or any other concrete type) in an interface requires creating a (type;value) pair holding the dynamic type and value to be wrapped (creation of this pair also involves copying the value being wrapped; see an analysis of this in the answer How can a slice contain itself?). Moreover when you want to access the value, you have to use a type assertion which is a runtime check, so the compiler can't be of any help optimizing that (and the check will add to the code execution time)!

So let's not use interface{} for our items, but instead use a concrete type for our case:

type Container struct {
    value int
}

We will use this in the iterator's and stream's next method: Next() Container, and in the mapper function:

type Mapper func(Container) Container

Also we may utilize embedding, as the method set of Iterator is a subset of that of Stream.

Without further ado, here is the complete, runnable example:

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)
}

Execution time: 210 ms. Nice, we're already sped it up 5 times, yet we're far from Java's Stream performance.

"Removing" Iterator and Stream types

Since we can't use generics, the interface types Iterator and Stream doesn't really need to be interfaces, since we would need new types of them if we'd wanted to use them to define iterators and streams of another types.

So the next thing we do is we remove Stream and Iterator, and we use their concrete types, their implementations above. This will not hurt readability at all, in fact the solution is shorter:

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)
}

Execution time: 50 ms, we've again sped it up 4 times compared to our previous solution! Now that's the same order of magnitude of the Java's solution, and we've lost nothing from the "streaming manner". Overall gain from the asker's proposal: 22 times faster.

Given the fact that in Java you used System.currentTimeMillis() to measure execution, this may even be the same as Java's performance. Asker confirmed: it's the same!

Regarding the same performance

Now we're talking about roughly the "same" code which does pretty simple, basic tasks, in different languages. If they're doing basic tasks, there is not much one language could do better than the other.

Also keep in mind that Java is a mature adult (over 21 years old), and had an enormous time to evolve and be optimized; actually Java's JIT (just-in-time compilation) is doing a pretty good job for long running processes, such as yours. Go is much younger, still just a kid (will be 5 years old 11 days from now), and probably will have better performance improvements in the foreseeable future than Java.

Further improvements

This "streamy" way may not be the "Go" way to approach the problem you're trying to solve. This is merely the "mirror" code of your Java's solution, using more idiomatic constructs of Go.

Instead you should take advantage of Go's excellent support for concurrency, namely goroutines (see go statement) which are much more efficient than Java's threads, and other language constructs such as channels (see answer What are golang channels used for?) and select statement.

Properly chunking / partitioning your originally big task to smaller ones, a goroutine worker pool might be quite powerful to process big amount of data. See Is this an idiomatic worker thread pool in Go?

Also you claimed in your comment that "I don't have 10M items to process but more 10G which won't fit in memory". If this is the case, think about IO time and the delay of the external system you're fetching the data from to process. If that takes significant time, it might out-weight the processing time in the app, and app's execution time might not matter (at all).

Go is not about squeezing every nanosecond out of execution time, but rather providing you a simple, minimalist language and tools, by which you can easily (by writing simple code) take control of and utilize your available resources (e.g. goroutines and multi-core CPU).

(Try to compare the Go language spec and the Java language spec. Personally I've read Go's lang spec multiple times, but could never get to the end of Java's.)

Community
  • 1
  • 1
icza
  • 389,944
  • 63
  • 907
  • 827
  • Thank you very much for you really interesting response. I changed the way I do measure java execution time to use nanotime but results are the same. Your last code sample give me the exact same results as what java does which is already cool but I expected Go to give better performances than what Java does. – cambierr Mar 17 '17 at 10:13
  • 1
    after reading your "Further improvements" edit: A Stream contains ordered items in my case, so using routines seems a bit complicated and I'm not sure that routines and channels would really give better performances since from my tests, the Iterator pattern is twice as fast as the channel pattern. About the IO latency, the Streaming approach plus the IO request paging makes it almost transparent. Thank your for all the explanations ! – cambierr Mar 17 '17 at 10:16
  • @cambierr Please read the last section (**Futher improvements** – still editing). The approach I used is the mirror of your Java solution, Go does give you tools to do it faster if approached differenetly. – icza Mar 17 '17 at 10:16
  • 1
    @cambierr Regarding the same performance: added a new section **Regarding the same performance**. – icza Mar 17 '17 at 11:04
5

This is I think an interesting question as it gets to the heart of differences between Java and Go and highlights the difficulties of porting code. Here is the same thing in go minus all the machinery (time ~50ms here):

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))

More seriously here is the same thing with a map over a slice of entries which is a more idiomatic version of what you have above and could work with any sort of Entry struct. This actually works out at a faster time on my machine of 30ms than the for loop above (anyone care to explain why?), so probably similar to your Java version:

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))
}

Things that will make operations more expensive -

  • Passing around interface{}, don't do this
  • Building a separate iterator type - use range or for loops
  • Allocations - so building new types to store answers, transform in place

Re using interface{}, I would avoid this - this means you have to write a separate map (say) for each type, not a great hardship. Instead of building an iterator, a range is probably more appropriate. Re transforming in place, if you allocate new structs for each result it'll put pressure on the garbage collector, using a Map func like this is an order of magnitude slower:

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

To stream split the data into chunks and do the same as above (keeping a memo of last object if you depend on previous calcs). If you have independent calculations (not as here) you could also fan out to a bunch of goroutines doing the work and get it done faster if there is a lot of it (this has overhead, in simple examples it won't be faster).

Finally, if you're interested in data processing with go, I'd recommend visiting this new site: http://gopherdata.io/

Kenny Grant
  • 9,360
  • 2
  • 33
  • 47
  • This is an interesting respone, thanks for it! Could you provide me with a sample on how to do this in a *Streaming* manner ? the fact is that I don't have 10M items to process but more 10G which won't fit in memory :) Thanks ! – cambierr Mar 17 '17 at 07:29
  • I've added a comment, it's pretty much as you'd expect I imagine, chunk your data (which is what java stream will do under the hood), you could build abstract machinery to do all these operations (as you would find in java), but it might be better to just pick the simplest thing that works, if you use it a lot in different domains, you could look into building the machinery. Go doesn't have generics, so you won't easily be able to build one performant machine that everyone can use for any kind of data (probably a good thing). – Kenny Grant Mar 17 '17 at 08:33
  • Cool. Thank you for your help & explanation! – cambierr Mar 17 '17 at 08:37
  • @cambierr Yes, there's solution to "retain" the streaming manner. See my answer here. – icza Mar 17 '17 at 10:05
0

Just as a complement to the previous comments, I changed the code of both Java and Go implementations to run the test 100 times.

What's interesting here is that Go takes a constant time between 69 and 72ms.

Owever, Java takes 71ms the first time (71ms, 19ms, 12ms) and then between 5 and 7ms.

From my test and understanding, this comes from the fact that the JVM takes a bit of time to properly load the classes and do some optimization.

In the end I'm still having this 10 times performance difference but I'm not giving up and I'll try to have a better understanding of how Go works to try to have it more fast :)

cambierr
  • 391
  • 4
  • 14
  • If benchmarking you should definitely try it with real work/data and verify the output, because otherwise you can't be sure your work is not just being optimised away. – Kenny Grant Mar 17 '17 at 11:38