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.

8
  • 1
    Possible duplicate of Is micro-optimisation important when coding?
    – gnat
    Jun 15, 2017 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, 2017 at 12:39
  • @gnat This one is better : stackoverflow.com/questions/16635398/…
    – Walfrat
    Jun 15, 2017 at 14:15
  • 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, 2017 at 21:27
  • 1
    The best way to determine which is faster is to test them all with a sample data set. Jun 16, 2017 at 0:20

1 Answer 1

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".

7
  • "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, 2017 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, 2017 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.. Jun 26, 2017 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. Jun 26, 2017 at 14:55
  • 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, 2017 at 19:32

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.