Please don’t rely on this Gitea instance being around forever.
If any of your build scripts use my (kageru’s) projects hosted here, check my Github or IEW on Github for encoding projects. If you can’t find what you’re looking for there, tell me to migrate it.
We’ll use that small example from the last section as our first example:
take a range of numbers, double each number, and compute the sum –
except this time, we’ll do the numbers from 1 to 1 billion.
Since everything we’re doing is lazy, memory usage shouldn’t be an issue.
I will use different implementations to solve them and benchmark all of them.
Here are the different approaches I came up with:
- a simple for loop in Java
- Java’s LongStream
- a for each loop with a range in Kotlin
- Java’s LongStream called from Kotlin[^ktjava]
- Java’s Stream wrapped in a Kotlin Sequence
- a Kotlin range wrapped in a Sequence
- Kotlin’s Sequence with a generator to create the range
[^ktjava]: Mainly to make sure there is no performance difference between the two.
The benchmarks were executed on an Intel Xeon E3-1271 v3 with 32 GB of RAM,
running Arch Linux with kernel 5.4.20-1-lts,
using the (at the time of writing) latest OpenJDK preview build (`15-ea+17-717`),
Kotlin 1.4-M1, and [jmh](https://openjdk.java.net/projects/code-tools/jmh/) version 1.23.
The bytecode target was set to Java 15 for the Java code and Java 13 for Kotlin
(newer versions are currently unsupported).
Source code for the Java tests:
```java
public long stream() {
return LongStream.range(1, upper)
.map(l -> l * 2)
.sum();
}
public long loop() {
long sum = 0;
for (long i = 0; i <upper;i++){
sum += i * 2;
}
return sum;
}
```
and for Kotlin:
```kotlin
fun stream() =
LongStream.range(1, upper)
.map { it * 2 }
.sum()
fun loop(): Long {
var sum = 0L
for (l in 1L until upper) {
sum += l * 2
}
return sum
}
fun streamWrappedInSequence() =
LongStream.range(1L, upper)
.asSequence()
.map { it * 2 }
.sum()
fun sequence() =
(1 until upper).asSequence()
.map { it * 2 }
.sum()
fun withGenerator() =
generateSequence(0L, { it + 1L })
.take(upper.toInt())
.map { it * 2 }
.sum()
```
with `const val upper = 1_000_000_000L`.[^`1 until upper` is used in these examples because unlike `lower..upper`, `until` is end-inclusive like Java’s LongStream.range().]
Without wasting any more of your time, here are the results:
which will already advance the underlying iterator.
The value returned by that will be stored temporarily until `nextLong()` is called.
```java
public boolean tryAdvance(LongConsumer consumer) {
final long i = from;
if (i <upTo){
from++;
consumer.accept(i);
return true;
}
// more stuff down here
}
```
where `consumer.accept()` is
```java
public void accept(T t) {
valueReady = true;
nextElement = t;
}
```
Knowing this, I have to wonder why `nextLong()` takes as long as it does.
Looking at [the implementation](https://github.com/openjdk/jdk/blob/6bab0f539fba8fb441697846347597b4a0ade428/src/java.base/share/classes/java/util/Spliterators.java#L756),
I don’t understand where all that time is going.
`hasNext()` should always be called before `next()`,
so `next()` just has to return a precomputed value.
Nevertheless, we can now explain the performance difference with the additional boxing step.
Primitives good; everything else bad.
With that in mind, I wrote a second test that avoids the unboxing issue to compare Streams and Sequences.
The next snippet uses a simple wrapper class that guarantees that we have no primitives
to execute a few operations on a Stream/Sequence.
I’ll use this opportunity to also compare parallel and sequential streams.
I hope that we can get a few more types like it in the future,
which will be especially useful once Kotlin/Native reaches maturity
and starts being used for small/embedded hardware.
Apart from the performance, Sequences are also a lot easier to understand and even extend than Streams.
Implementing your own Sequence requires barely more than an implementation of the underlying iterator,
as can be seen in [CoalescingSequence](https://git.kageru.moe/kageru/Sekwences/src/branch/master/src/main/kotlin/moe/kageru/sekwences/CoalescingSequence.kt)
which I implemented last year to get a feeling for how all of this works.