6 ms·
for reference, a pure java version through JMH that takes 0.38 seconds on my machine. This uses parallel stream so its multithreaded. Single threaded it takes
by eggsnbacon1 6y ago
for reference, a pure java version through JMH that takes 0.38 seconds on my machine. This uses parallel stream so its multithreaded.
Single threaded it takes 0.71 seconds. Removing the blackhole to allow dead code elimination takes single thread down to 0.41 seconds. This is close to PyPy, which I assume is dead code eliminating the string conversion as well.
package org.example;
import org.eclipse.collections.api.block.procedure.primitive.IntProcedure;
import org.eclipse.collections.impl.list.Interval;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.infra.Blackhole;
public class MyBenchmark {
@Benchmark
public void testMethod(final Blackhole blackhole) {
Interval.oneTo(20)
.parallelStream().forEach((IntProcedure)
outer ->
Interval.oneTo(1000000)
.forEach(
(IntProcedure)
inner ->
// prevent dead code elimination
blackhole.consume(Integer.toString(inner))));
}
}
- 6c696e7578 6y agoNot too surprising as dynamic languages take 4-20 times longer at numerical work, from rough experience. Java/c/c#/c++/rust etc are roughly at the same end of the spectrum (unless you're creating stupid numbers of objects). Perl/python/ruby, they're dynamic, so expect slower results. I like the threaded approach you're using.
- eggsnbacon1 6y ago> like the threaded approach you're using. thanks :) Eclipse Collections can also do batched loops which might speed this up. Telling Java how many threads you want will probably help as well Interestingly, I tried plain old for(i) loops and the result was exactly the same. At least for Eclipse Collections, the syntactic sugar for ranges and forEach is completely optimized away, apparently.
- palinkapika 6y agoYou might already know this, but there is one potential caveat with APIs like that when it comes to performance or at least measuring their performance. The hot loop (the code that actually iterates over your data points) does not live in your code base. But performance often depends on what the JIT compiler makes out of that loop. If the API is used at several locations in your program, the compiler might not be able to generate code that is optimal for your call-site and inputs. Instead, it will generate generalized code that works for all inputs but might be slower. However, when writing benchmarks, there is often no other code around to force the JIT compiler to generate such generalized code. The following code demonstrates this. If the parameter warmup is set, I invoke the forEach methods with different inputs first (they do the same but are different methods in the Java byte code). The purpose is to force the compiler to generate generalized code: @Param({"true", "false"}) public boolean warmup; @Setup public void setup() { if (!warmup) { Interval.oneTo(20).forEach( (IntProcedure) i -> Interval.oneTo(1_000_000).forEach( (IntProcedure) j -> Integer.toString(j))); Interval.oneTo(20).forEach( (IntProcedure) i -> Interval.oneTo(1_000_000).forEach( (IntProcedure) j -> Integer.toString(j))); Interval.oneTo(20).forEach( (IntProcedure) i -> Interval.oneTo(1_000_000).forEach( (IntProcedure) j -> Integer.toString(j))); } } @Benchmark public void coollectionsBlackhole(Blackhole blackhole) { Interval.oneTo(20).forEach( (IntProcedure) i -> Interval.oneTo(1_000_000).forEach( (IntProcedure)j -> blackhole.consume(Integer.toString(j)))); } @Benchmark public void collectionsDeadCode() { Interval.oneTo(20).forEach( (IntProcedure) i -> Interval.oneTo(1_000_000).forEach( (IntProcedure)j -> Integer.toString(j))); } And the for loop implementations for reference: @Benchmark public void loopBlackhole(Blackhole blackHole) { for (int j = 0; j < 20; j++) { for (int i = 0; i < 1_000_000; i++) { blackHole.consume(Integer.toString(i)); } } } @Benchmark public void loopDeadCode() { for (int j = 0; j < 20; j++) { for (int i = 0; i < 1_000_000; i++) { Integer.toString(i); } } } @Benchmark public void loopBlackholeOnly(Blackhole hole) { for (int j = 0; j < 20; j++) { for (int i = 0; i < 1_000_000; i++) { hole.consume(0xCAFEBABE); } } } On my desktop machine, this gives me the following results (Java 11/Hotspot/C2): Benchmark (warmup) Mode Cnt Score Error Units collectionsDeadCode true avgt 99 162.482 ± 1.615 ms/op collectionsDeadCode false avgt 99 218.816 ± 4.217 ms/op coollectionsBlackhole true avgt 99 235.122 ± 1.362 ms/op coollectionsBlackhole false avgt 99 270.192 ± 1.627 ms/op loopBlackhole true avgt 99 207.214 ± 1.162 ms/op loopBlackhole false avgt 99 206.711 ± 0.932 ms/op loopBlackholeOnly true avgt 99 74.774 ± 0.180 ms/op loopBlackholeOnly false avgt 99 74.359 ± 0.180 ms/op loopDeadCode true avgt 99 143.394 ± 0.900 ms/op loopDeadCode false avgt 99 142.654 ± 0.795 ms/op All results are for single threaded code. Now, the difference is not huge, but significant. Overall it looks like the invocation of toString(int) is not removed and accounts for most of the runtime. Just to be clear: I am not saying one should stay way from the stream APIs. As soon as the work per item is more than just a few arithmetic operations, there is a good chance the difference in runtime is negligible. But when doing numerical work (aggregations etc.), a simple loop might be the better option. Finally, these differences are of course compiler-dependent. For example, C2 might behave differently than Graal and who knows what the future brings.