2020-08-08 11:51:04 +03:00
|
|
|
# ND-structure generation and operations
|
2019-02-20 15:24:51 +03:00
|
|
|
|
|
|
|
**TODO**
|
|
|
|
|
2019-01-05 20:15:36 +03:00
|
|
|
# Performance for n-dimensional structures operations
|
|
|
|
|
|
|
|
One of the most sought after features of mathematical libraries is the high-performance operations on n-dimensional
|
|
|
|
structures. In `kmath` performance depends on which particular context was used for operation.
|
|
|
|
|
|
|
|
Let us consider following contexts:
|
2024-03-27 09:11:12 +03:00
|
|
|
|
2019-01-05 20:15:36 +03:00
|
|
|
```kotlin
|
2019-01-07 17:18:31 +03:00
|
|
|
// automatically build context most suited for given type.
|
2021-03-16 21:17:26 +03:00
|
|
|
val autoField = NDField.auto(DoubleField, dim, dim)
|
2021-05-07 15:59:21 +03:00
|
|
|
// specialized nd-field for Double. It works as generic Double field as well.
|
2019-06-08 16:30:06 +03:00
|
|
|
val specializedField = NDField.real(dim, dim)
|
2019-01-07 17:18:31 +03:00
|
|
|
//A generic boxing field. It should be used for objects, not primitives.
|
2021-03-16 21:17:26 +03:00
|
|
|
val genericField = NDField.buffered(DoubleField, dim, dim)
|
2019-01-07 17:18:31 +03:00
|
|
|
```
|
2024-03-27 09:11:12 +03:00
|
|
|
|
2021-05-07 15:59:21 +03:00
|
|
|
Now let us perform several tests and see, which implementation is best suited for each case:
|
2019-01-07 17:18:31 +03:00
|
|
|
|
|
|
|
## Test case
|
|
|
|
|
2021-05-07 15:59:21 +03:00
|
|
|
To test performance we will take 2d-structures with `dim = 1000` and add a structure filled with `1.0`
|
2019-01-07 17:18:31 +03:00
|
|
|
to it `n = 1000` times.
|
|
|
|
|
|
|
|
## Specialized
|
2024-03-27 09:11:12 +03:00
|
|
|
|
2019-01-07 17:18:31 +03:00
|
|
|
The code to run this looks like:
|
2024-03-27 09:11:12 +03:00
|
|
|
|
2019-01-07 17:18:31 +03:00
|
|
|
```kotlin
|
|
|
|
specializedField.run {
|
2024-08-17 21:11:13 +03:00
|
|
|
var res: NDBuffer<Float64> = one
|
2019-01-07 17:18:31 +03:00
|
|
|
repeat(n) {
|
|
|
|
res += 1.0
|
|
|
|
}
|
|
|
|
}
|
|
|
|
```
|
2024-03-27 09:11:12 +03:00
|
|
|
|
2019-01-07 17:18:31 +03:00
|
|
|
The performance of this code is the best of all tests since it inlines all operations and is specialized for operation
|
|
|
|
with doubles. We will measure everything else relative to this one, so time for this test will be `1x` (real time
|
2021-05-07 15:59:21 +03:00
|
|
|
on my computer is about 4.5 seconds). The only problem with this approach is that it requires specifying type
|
|
|
|
from the beginning. Everyone does so anyway, so it is the recommended approach.
|
2019-01-07 17:18:31 +03:00
|
|
|
|
|
|
|
## Automatic
|
2024-03-27 09:11:12 +03:00
|
|
|
|
2019-01-07 17:18:31 +03:00
|
|
|
Let's do the same with automatic field inference:
|
2024-03-27 09:11:12 +03:00
|
|
|
|
2019-01-07 17:18:31 +03:00
|
|
|
```kotlin
|
|
|
|
autoField.run {
|
|
|
|
var res = one
|
|
|
|
repeat(n) {
|
|
|
|
res += 1.0
|
|
|
|
}
|
|
|
|
}
|
|
|
|
```
|
2024-03-27 09:11:12 +03:00
|
|
|
|
2019-01-07 17:18:31 +03:00
|
|
|
Ths speed of this operation is approximately the same as for specialized case since `NDField.auto` just
|
2021-05-07 15:59:21 +03:00
|
|
|
returns the same `RealNDField` in this case. Of course, it is usually better to use specialized method to be sure.
|
2019-01-07 17:18:31 +03:00
|
|
|
|
|
|
|
## Lazy
|
2024-03-27 09:11:12 +03:00
|
|
|
|
2019-01-07 17:18:31 +03:00
|
|
|
Lazy field does not produce a structure when asked, instead it generates an empty structure and fills it on-demand
|
|
|
|
using coroutines to parallelize computations.
|
|
|
|
When one calls
|
2024-03-27 09:11:12 +03:00
|
|
|
|
2019-01-07 17:18:31 +03:00
|
|
|
```kotlin
|
|
|
|
lazyField.run {
|
|
|
|
var res = one
|
|
|
|
repeat(n) {
|
|
|
|
res += 1.0
|
|
|
|
}
|
|
|
|
}
|
|
|
|
```
|
2024-03-27 09:11:12 +03:00
|
|
|
|
2021-05-07 15:59:21 +03:00
|
|
|
The result will be calculated almost immediately but the result will be empty. To get the full result
|
2019-01-07 17:18:31 +03:00
|
|
|
structure one needs to call all its elements. In this case computation overhead will be huge. So this field never
|
|
|
|
should be used if one expects to use the full result structure. Though if one wants only small fraction, it could
|
|
|
|
save a lot of time.
|
|
|
|
|
|
|
|
This field still could be used with reasonable performance if call code is changed:
|
2024-03-27 09:11:12 +03:00
|
|
|
|
2019-01-07 17:18:31 +03:00
|
|
|
```kotlin
|
|
|
|
lazyField.run {
|
|
|
|
val res = one.map {
|
|
|
|
var c = 0.0
|
|
|
|
repeat(n) {
|
|
|
|
c += 1.0
|
|
|
|
}
|
|
|
|
c
|
|
|
|
}
|
|
|
|
|
|
|
|
res.elements().forEach { it.second }
|
|
|
|
}
|
|
|
|
```
|
2024-03-27 09:11:12 +03:00
|
|
|
|
2019-01-07 17:18:31 +03:00
|
|
|
In this case it completes in about `4x-5x` time due to boxing.
|
|
|
|
|
|
|
|
## Boxing
|
2024-03-27 09:11:12 +03:00
|
|
|
|
2019-01-07 17:18:31 +03:00
|
|
|
The boxing field produced by
|
2024-03-27 09:11:12 +03:00
|
|
|
|
2019-01-07 17:18:31 +03:00
|
|
|
```kotlin
|
|
|
|
genericField.run {
|
2024-08-17 21:11:13 +03:00
|
|
|
var res: NDBuffer<Float64> = one
|
2019-01-07 17:18:31 +03:00
|
|
|
repeat(n) {
|
|
|
|
res += 1.0
|
|
|
|
}
|
|
|
|
}
|
|
|
|
```
|
2024-03-27 09:11:12 +03:00
|
|
|
|
2021-05-07 15:59:21 +03:00
|
|
|
is the slowest one, because it requires boxing and unboxing the `double` on each operation. It takes about
|
2019-01-07 17:18:31 +03:00
|
|
|
`15x` time (**TODO: there seems to be a problem here, it should be slow, but not that slow**). This field should
|
|
|
|
never be used for primitives.
|
|
|
|
|
|
|
|
## Element operation
|
2024-03-27 09:11:12 +03:00
|
|
|
|
2019-01-07 17:18:31 +03:00
|
|
|
Let us also check the speed for direct operations on elements:
|
2024-03-27 09:11:12 +03:00
|
|
|
|
2019-01-07 17:18:31 +03:00
|
|
|
```kotlin
|
|
|
|
var res = genericField.one
|
|
|
|
repeat(n) {
|
|
|
|
res += 1.0
|
|
|
|
}
|
|
|
|
```
|
2024-03-27 09:11:12 +03:00
|
|
|
|
2019-01-07 17:18:31 +03:00
|
|
|
One would expect to be at least as slow as field operation, but in fact, this one takes only `2x` time to complete.
|
|
|
|
It happens, because in this particular case it does not use actual `NDField` but instead calculated directly
|
|
|
|
via extension function.
|
|
|
|
|
|
|
|
## What about python?
|
|
|
|
|
|
|
|
Usually it is bad idea to compare the direct numerical operation performance in different languages, but it hard to
|
|
|
|
work completely without frame of reference. In this case, simple numpy code:
|
2024-03-27 09:11:12 +03:00
|
|
|
|
2019-01-07 17:18:31 +03:00
|
|
|
```python
|
2021-07-12 20:21:46 +03:00
|
|
|
import numpy as np
|
|
|
|
|
2019-01-07 17:18:31 +03:00
|
|
|
res = np.ones((1000,1000))
|
|
|
|
for i in range(1000):
|
|
|
|
res = res + 1.0
|
|
|
|
```
|
2024-03-27 09:11:12 +03:00
|
|
|
|
|
|
|
gives the completion time of about `1.1x`, which means that specialized kotlin code in fact is working faster (I think
|
|
|
|
it is
|
2019-01-07 17:18:31 +03:00
|
|
|
because better memory management). Of course if one writes `res += 1.0`, the performance will be different,
|
2021-05-07 15:59:21 +03:00
|
|
|
but it would be different case, because numpy overrides `+=` with in-place operations. In-place operations are
|
2019-01-07 17:18:31 +03:00
|
|
|
available in `kmath` with `MutableNDStructure` but there is no field for it (one can still work with mapping
|
|
|
|
functions).
|