akarnokd / akarnokd/akarnokd-misc

RxJava 1, 2 & Reactor comparison benchmarks 14-03-2018

Ouverte
#7 4 commentaires 28 réactions 0 personnes assignées Voir sur GitHub
Langage dominant
Java
Étoiles
54
Forks
20
Merge moyen
1 h 20 min
PR mergées (30 j)
5

Description

## Environment

- i7 4790 stock settings
- Windows 10 x64 fully patched
- Java 8u162
- RxJava 1.3.6
- RxJava 2.1.10
- Reactor Core 3.2.0-M1 (53582aeb14ead1dc7b1f19a0f7f682b74287a6c7)
- [JMH Compare GUI](https://github.com/akarnokd/jmh-compare-gui/releases/tag/v1.3.2) workspace: [benchmarks_180314.xml](https://gist.github.com/akarnokd/20bb54cab8e6870fce86e90216f378a6)

### Memory usage

These tests check how much memory is allocated when working with 1,000,000 instances of varios flows and components (total megabytes, smaller is better, green is better):

![image](https://user-images.githubusercontent.com/1269832/37401565-c1c2b5d6-2788-11e8-9e82-2dbb2e5f7ac4.png)

Notes:
- I'm not aware of any Async- and Behavior-like processors in Reactor
- `Flowable.empty()` is surprisingly large
- Looks like reactor has some extra storage in various operators while others do benefit from having to no worry about atomic field updaters.

### Async throughput

This benchmark measures how many items can be transferred over a flow when work stealing is possible (async) or the source and consumers are pinned to a specific thread (pipeline):

![image](https://user-images.githubusercontent.com/1269832/37401948-1272941e-278a-11e8-8e69-152d8b943b5e.png)

The tests use `Executor`s wrapped into schedulers, not the built-in schedulers. Reactor is clearly winning in both situations. The likely reason for this is that by default, the RxJava wrappers for `Executor`s always trampoline while Reactor's default wrapper does not, saving on double trampolining.

### Blocking

These benchmarks measure the overhead of blocking for the first or last element of a 1,000,000 source or how much overhead presents itself when blocking for an empty source (ops/s, larger is better):

![image](https://user-images.githubusercontent.com/1269832/37403150-21359cd6-278e-11e8-91c5-f1476e8ce321.png)

- Interesting to see why RxJava 1's last is so much faster than the others.

For the 0-1 types, there is only 0 or 1 element to wait for blockingly:

![image](https://user-images.githubusercontent.com/1269832/37403306-a6600c0c-278e-11e8-88af-272460cf76ac.png)

![image](https://user-images.githubusercontent.com/1269832/37403384-f5cca494-278e-11e8-984d-96bfd793e7d2.png)

- Reactor's blocking method does contain optimizations for scalar and empty sources, bypassing the subscription and blocking entirely.

### Hot sources

These measure the throughput of various processor and subject types (ops/s, larger is better):

![image](https://user-images.githubusercontent.com/1269832/37403952-aea4ac2c-2790-11e8-9ec1-ca971c3b402f.png)

- Reactor has no equivalent of Async- and Behavior-type processor as of now
- It's interesting to see RxJava 1's Replay and Unicast subject perform better, worth investigating
- v2 BehaviorSubject and Processors have extra overhead due an additional lock per item to avoid latest-subscribe races.

### Subscribing

These measure the overhead of subscribing to various simple sources (ops/s, larger is better):

![image](https://user-images.githubusercontent.com/1269832/37404341-de203326-2791-11e8-993f-bffbabffe17f.png)

- The `Single` type sources would emit a pre-created exception instead of an item.
- Reactor is optimized for scalar and empty sources as well as it uses a weaker concurrency `just` implementation.

### Streaming

There are various sub-benchmarks measuring the multi-value behavior of various flows:

#### array

These measure the throughput when the source data is in an array, which minimizes GC due to autoboxing (see range below):

![image](https://user-images.githubusercontent.com/1269832/37404537-73aeebf8-2792-11e8-9fcf-e59dea6211fd.png)

- Reactor uses a weaker concurrency `just` implementation (count == 1 case).

#### range

These measure the throughput when the source data is generated integers which get autoboxed, thus there is an additional GC overhead

![image](https://user-images.githubusercontent.com/1269832/37404700-e1aabc22-2792-11e8-885a-bcf046233baa.png)

- Reactor uses a weaker concurrency `just` implementation (count == 1 case).

#### iterable

These measure the throughput when the source data is in an `Iterable`:

![image](https://user-images.githubusercontent.com/1269832/37404840-4ec3f576-2793-11e8-8628-8ae03e4668ea.png)

- It's odd how both Reactor and RxJava 2 `Observable` perform worse despite the presumably lower overhead on longer sequences.

#### concatMap onto just

These measure the throughput when a source sequence is mapped into plain `just` inner sources within `concatMap`:

![image](https://user-images.githubusercontent.com/1269832/37405055-ea66f848-2793-11e8-842f-5fa7f4e2af6f.png)

- RxJava 2 `Observable.concatMap` is not optimized for scalar sources, thus the code goes through the regular subscription routine, adding a lot of overhead.

#### flatMap onto just

These measure the throughput when a source sequence is mapped into plain `just` inner sources within `flatMap`:

![image](https://user-images.githubusercontent.com/1269832/37405244-6e767d84-2794-11e8-8b3c-d3138b8c3d3d.png)

The reason v1 is faster is because it uses `synchronized` as the trampolining mechanic which gets optimized away by the JIT. The v2 version uses lock-free atomics which can't be optimized away but should have much better concurrent properties.

#### concatMap onto range

These measure the throughput when a source sequence is mapped onto a two element `range` within `concatMap`:

![image](https://user-images.githubusercontent.com/1269832/37405478-04a4de86-2795-11e8-8fed-7a1c57dd85a4.png)

- The main overhead here is the request arbitration between subsequent inner sources, which doesn't happen in v2 `Observable`.

#### flatMap onto range

These measure the throughput when a source sequence is mapped onto a two element `range` within `concatMap`:
![image](https://user-images.githubusercontent.com/1269832/37405624-66d0c818-2795-11e8-94a6-ced737997ccf.png)

- The main overhead here is the request management with inner sources, which doesn't happen in v2 `Observable`.

#### concatMap cross-mapping

In these throughput measures, the total number of items is always 1,000,000, which is made out of the outer item `count` times the individual inner items: 10 x 100,000; 100 x 10,000 etc. It tells about if the `concatMap` prefers long outer sources or long inner sources:

![image](https://user-images.githubusercontent.com/1269832/37405940-4185c382-2796-11e8-82e1-26c99334ab16.png)

#### flatMap cross-mapping

In these throughput measures, the total number of items is always 1,000,000, which is made out of the outer item `count` times the individual inner items: 10 x 100,000; 100 x 10,000 etc. It tells about if `flatMap` prefers long outer sources or long inner sources:

![image](https://user-images.githubusercontent.com/1269832/37406075-9646770e-2796-11e8-8db6-af55ee94d1b4.png)

#### flatten onto scalar

These measure the `flatMapIterable` (`concatMapIterable` just an alias) performance which when mapped onto a singleton value. Unfortunately, there exist no standard way to detect an `Iterable` has a single value without trying to iterate through it.

![image](https://user-images.githubusercontent.com/1269832/37406317-2ec5595a-2797-11e8-8fd5-f748fc7bb831.png)

- Reactor is significantly faster here for some reason, as is the v2 `Observable`. It is worth investigating what causes the extra overhead in `Flowable`.

#### flatten onto a range

These measure the `flatMapIterable` (`concatMapIterable` just an alias) performance which when mapped onto a two element source.

![image](https://user-images.githubusercontent.com/1269832/37406805-5730c8d8-2798-11e8-82db-803845deb7c3.png)

#### flatten cross-mapping

In these throughput measures, the total number of items is always 1,000,000, which is made out of the outer item `count` times the individual inner items: 10 x 100,000; 100 x 10,000 etc. It tells about if `flatMapIterable` prefers long outer sources or long inner sources:

![image](https://user-images.githubusercontent.com/1269832/37406618-e0581d74-2797-11e8-9257-3b007b2c96af.png)

## Conclusion

Reactor core is sometimes better, sometimes worse than RxJava 2. At least they are mostly better than RxJava 1. There seems to be some opportunity for optimizing certain RxJava 2 operators further.

Guide de contribution

Aucun guide de contribution indexé pour ce dépôt

Évaluation

Cette issue n'a pas encore été évaluée.

Recevez les nouvelles issues par e-mail

Un résumé court des issues GitHub adaptées aux débutants.