0

What are the gains in actual computing speed (verbosity of code being put aside) and for which case would each be recommended for, as a good design pattern? I would like to know the above in general but also if it helps in the context of the following which can re-written in all 3 ways

static void parallelTestsExecution(String name,Runnable ... runnables){
    ArrayList<BaseTestThread> list = new ArrayList();
    int counter =0; // use local variable counter to name threads accordingly
    for(Runnable runnable : runnables){//runnable contains test logic
        counter++;
        BaseTestThread t = new BaseTestThread(runnable);
        t.setName(name+" #"+counter);
        list.add(t);
    }

    for(BaseTestThread thread : list ){//must first start all threads
        thread.start();
    }

    for(BaseTestThread thread : list ){//start joins only after all have started
        try {
            thread.join();
        } catch (InterruptedException ex) {
            throw new RuntimeException("Interrupted - This should normally not happen.");
        }
    }

    for(BaseTestThread thread : list ){
        assertTrue("Thread: "+thread.getName() , thread.isSuccessfull());
    }

}

vs

 static void parallelTestsExecution(String name,Runnable ... runnables){
    ArrayList<BaseTestThread> list = new ArrayList();
    int counter =0; // use local variable counter to name threads accordingly
    for(Runnable runnable : runnables){
        counter++;
        BaseTestThread t = new BaseTestThread(runnable);//BaseTestThread extends thread
        t.setName(name+" #"+counter);
        list.add(t);
    }

    list.forEach((thread) -> {
        thread.start();
    });

    list.forEach((thread) -> {
        try {
            thread.join();
        } catch (InterruptedException ex) {
            throw new RuntimeException("Interrupted - This should normally not happen.");
        }
    });

    list.forEach((thread) -> {
        assertTrue("Thread: "+thread.getName() , thread.isSuccessfull());
    });

}

vs Populating the list with threads each associated with a runnable and using list.Stream()... etc (currently not confident enough with this one to post something more concrete)

I am specifically interested in learning how each of those different mechanisms work and what are their strength/weaknesses to apply my better judgment whenever its needed to decide what to use as a best practice. Question isnt about micro-optimisation in general and the importance of it.

Robert Harvey
  • 198,589
  • 55
  • 464
  • 673
Leon
  • 197
  • 3
  • 10
  • 1
    Possible duplicate of [Is micro-optimisation important when coding?](https://softwareengineering.stackexchange.com/questions/99445/is-micro-optimisation-important-when-coding) – gnat Jun 15 '17 at 12:35
  • I am specifically interested in learning how each of those different mechanisms work and what are their strength/weaknesses to apply my better judgment whenever its needed to decide what to use. Question isnt about micro-optimisation in general and its importance. – Leon Jun 15 '17 at 12:39
  • @gnat This one is better : https://stackoverflow.com/questions/16635398/java-8-iterable-foreach-vs-foreach-loop – Walfrat Jun 15 '17 at 14:15
  • @Walfrat that one is indeed more related, although the OP asks about 2 of the 3 methods mentioned and that question was asked just short of a year before Java 8 was released and well over 4years now. I would assume with J8 being actually released and people having a lot more time to educate themselves and learn by experience, the responses would be of much more value. – Leon Jun 15 '17 at 14:38
  • 1
    I would rename `list` to `threads`. It's also a shame that there isn't an easy way to map an array of element to an array of elements and indices (That would allow you to really easily map those runnables to threads) – Alexander Jun 15 '17 at 21:27
  • Not really an answer, but have you considered using a parallel stream to handle the threading instead of creating a bunch of BaseTestThread. That would probably get you pretty decent performance. – Maybe_Factor Jun 16 '17 at 00:18
  • 1
    The best way to determine which is faster is to test them all with a sample data set. – Maybe_Factor Jun 16 '17 at 00:20
  • @Leon The top answer of my link talk about parrallel stream and Java 8. – Walfrat Jun 16 '17 at 06:41

1 Answers1

3

Performance

In your example, I can hardly imagine any significant performance difference between the different styles, as in both cases, the work is done in N parallel Threads. Creating and managing the threads will vastly dominate the run time, even if the threads themself do nothing.

The different styles only apply in how you create the threads, start the threads, and wait for the threads to finish.

Similarities

Both styles have the basic way of iterating in common. Both the Java5 loop style and the (default implementation of the) Java8 lambda style use the iterator() of the array/list/collection and its next() and hasNext() methods.

Differences

What's different is the way the body gets executed.

The Java5 style loop immediately uses your code as the loop body (inside your parallelTestsExecution() method), while the Java8 lambda style encapsulates the functionality into the accept() method of an anonymous Acceptor object that gets called for each iteration. [Have a look at the class files created from your source. With the second style, there will be some xxx$1 and similar classes for the aforementioned Acceptors.]

So, there is an overhead when using the Java8 lambda style, but I guess the Hotspot compiler can optimize this to run as fast as a normal loop.

But this style affects the accessibility of local variables residing outside of the loop body. With Java5 loops, they are fully accessible, read and write, because they are in the same method. With Java8 lambdas, you can only read locals, and only if they are declared final [* see below] - your loop body resides in a different class, and accessing variables from your parallelTestsExecution() method is impossible for the JVM. The trick that the compiler uses, is to pass the value of your variable into the hidden constructor of the anonymous Acceptor class, and have the Acceptor class store this copy as an instance field. So, you can't transform your first loop to lambda style because you modify the "counter" variable inside which is impossible from an anonymous class.

It also affects debugging. Suppose you set a breakpoint at your line "thread.start();". In the first approach, you'll find yourself immediately in your parallelTestsExecution() method. With lambdas, you'll see an accept() method inside a forEach() method inside your parallelTestsExecution() method, maybe with some more intermediate glue.

[*] Remark on "final": As Jules correctly commented, the "final" declaration is no longer necessary. If you use a local variable inside of a lambda, it still has to be effectively final. That doesn't give you any more possibilities, just saves you typing the word "final".

Ralf Kleberhoff
  • 5,891
  • 15
  • 19
  • "So, there is an overhead when using the Java8 lambda style" ... I'm not convinced there is. In the `foreach` style, each iteration needs two virtual calls to an Iterator to check whether additional items are available and to fetch the next; in the lambda style, control is passed over to the container which will likely iterate directly over the contained elements rather than using an Iterator (and in any case the type will be known exactly so can be trivially inlined), just issuing a single virtual call to the acceptor object. My suspicion is that using `forEach` is faster. – Jules Jun 24 '17 at 22:22
  • "With Java8 lambdas, you can only read locals, and only if they are declared final" -- this isn't true. It *was* true for Java 7 anonymous classes, but the restriction on declaring locals final has been lifted in Java 8, instead leaving only a requirement that it would not cause an error if the local *were* declared final. – Jules Jun 24 '17 at 22:25
  • @Jules: Sorry, I missed that point about final declarations. Still Leon's first loop wouldn't be able to modify the counter variable from inside an Acceptor. I'd put it this way: the compiler tries to declare the variable final if you didn't.. – Ralf Kleberhoff Jun 26 '17 at 14:33
  • @Jules: Regarding performance, you are right in some important situations. Namely ArrayList and Vector have specialized forEach() method implementations that don't rely on the Iterator-based default, but use a C style loop internally. Maybe in some future JRE we'll see more collections doing that. But anyway, most real-world programs will not notice any performance difference between Java5 and Java8 style loops. And if so, replace ArrayList and Vector loops by good old C style. – Ralf Kleberhoff Jun 26 '17 at 14:55
  • @Jules An interesting article for Kotlin performance https://sites.google.com/a/athaydes.com/renato-athaydes/posts/kotlinshiddencosts-benchmarks in Kotlin it shows a 300% performance hit on forEach. Not sure if that translates to Java, but it was interesting – MaxPower Jun 26 '17 at 18:39
  • 1
    @MaxPower - I think you're misinterpreting that. That's a performance hit for using the `forEach` method on a range versus directly looping on the range, which is converted to a direct local loop by the compiler. It's not comparable to using an indirect `for(var : collection)` style loop, even though Kotlin uses a syntax that makes it look like it works the same way. The "loop with step 1" benchmark is more comparable, as that does create an iterator for the purpose, and that does show a slight benefit for looping versus using `forEach`. But as kotlin handles native types differently to ... – Jules Jun 26 '17 at 19:32
  • ... Java, it's very hard to say how this would translate to a Java implementation. – Jules Jun 26 '17 at 19:33