From ba63b2e373d8b8199f97a523d03123e7563d8f6f Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Wed, 29 Aug 2018 13:45:06 +0300 Subject: [PATCH] Generic definition for NDArray --- kmath-common/build.gradle | 12 ++- .../kmath/structures/BufferNDField.kt | 93 +++++++++++++++++++ .../scientifik/kmath/structures/NDArray.kt | 12 --- .../kmath/structures/NDArrayFactories.kt | 38 ++++++++ .../kmath/structures/SimpleNDFieldTest.kt | 15 +++ kmath-jvm/build.gradle | 5 +- .../kmath/structures/ArrayBenchmark.kt | 53 +++++++++++ .../kmath/structures/RealNDArray.kt | 76 ++------------- 8 files changed, 223 insertions(+), 81 deletions(-) create mode 100644 kmath-common/src/main/kotlin/scientifik/kmath/structures/BufferNDField.kt create mode 100644 kmath-common/src/main/kotlin/scientifik/kmath/structures/NDArrayFactories.kt create mode 100644 kmath-common/src/test/kotlin/scientifik/kmath/structures/SimpleNDFieldTest.kt create mode 100644 kmath-jvm/src/jmh/kotlin/scietifik/kmath/structures/ArrayBenchmark.kt diff --git a/kmath-common/build.gradle b/kmath-common/build.gradle index 2712fb0eb..eecc32818 100644 --- a/kmath-common/build.gradle +++ b/kmath-common/build.gradle @@ -1,6 +1,8 @@ -description = "Platform-independent interfaces for kotlin maths" +plugins{ + id "kotlin-platform-common" +} -apply plugin: 'kotlin-platform-common' +description = "Platform-independent interfaces for kotlin maths" repositories { mavenCentral() @@ -12,3 +14,9 @@ dependencies { testCompile "org.jetbrains.kotlin:kotlin-test-common:$kotlin_version" } +kotlin { + experimental { + coroutines "enable" + } +} + diff --git a/kmath-common/src/main/kotlin/scientifik/kmath/structures/BufferNDField.kt b/kmath-common/src/main/kotlin/scientifik/kmath/structures/BufferNDField.kt new file mode 100644 index 000000000..6a2df9acc --- /dev/null +++ b/kmath-common/src/main/kotlin/scientifik/kmath/structures/BufferNDField.kt @@ -0,0 +1,93 @@ +package scientifik.kmath.structures + +import scientifik.kmath.operations.Field +import kotlin.coroutines.experimental.buildSequence + + +/** + * A generic buffer for both primitives and objects + */ +interface Buffer { + operator fun get(index: Int): T + operator fun set(index: Int, value: T) +} + +/** + * Generic implementation of NDField based on continuous buffer + */ +abstract class BufferNDField(shape: List, field: Field) : NDField(shape, field) { + + /** + * Strides for memory access + */ + private val strides: List by lazy { + ArrayList(shape.size).apply { + var current = 1 + add(1) + shape.forEach { + current *= it + add(current) + } + } + } + + protected fun offset(index: List): Int { + return index.mapIndexed { i, value -> + if (value < 0 || value >= shape[i]) { + throw RuntimeException("Index out of shape bounds: ($i,$value)") + } + value * strides[i] + }.sum() + } + + protected fun index(offset: Int): List{ + return buildSequence { + var current = offset + var strideIndex = strides.size-2 + while (strideIndex>=0){ + yield(current / strides[strideIndex]) + current %= strides[strideIndex] + strideIndex-- + } + }.toList().reversed() + } + + private val capacity: Int + get() = strides[shape.size] + + + protected abstract fun createBuffer(capacity: Int, initializer: (Int) -> T): Buffer + + override fun produce(initializer: (List) -> T): NDArray { + val buffer = createBuffer(capacity){initializer(index(it))} + return BufferNDArray(this, buffer) + } + + + class BufferNDArray(override val context: BufferNDField, val data: Buffer) : NDArray { + + override fun get(vararg index: Int): T { + return data[context.offset(index.asList())] + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is BufferNDArray<*>) return false + + if (context != other.context) return false + if (data != other.data) return false + + return true + } + + override fun hashCode(): Int { + var result = context.hashCode() + result = 31 * result + data.hashCode() + return result + } + + override val self: NDArray get() = this + } +} + + diff --git a/kmath-common/src/main/kotlin/scientifik/kmath/structures/NDArray.kt b/kmath-common/src/main/kotlin/scientifik/kmath/structures/NDArray.kt index 5acac4ff1..462cd6adb 100644 --- a/kmath-common/src/main/kotlin/scientifik/kmath/structures/NDArray.kt +++ b/kmath-common/src/main/kotlin/scientifik/kmath/structures/NDArray.kt @@ -198,15 +198,3 @@ operator fun T.div(arg: NDArray): NDArray = arg.transform { _, value - } } -/** - * Create a platform-specific NDArray of doubles - */ -expect fun realNDArray(shape: List, initializer: (List) -> Double = { 0.0 }): NDArray - -fun real2DArray(dim1: Int, dim2: Int, initializer: (Int, Int) -> Double = { _, _ -> 0.0 }): NDArray { - return realNDArray(listOf(dim1, dim2)) { initializer(it[0], it[1]) } -} - -fun real3DArray(dim1: Int, dim2: Int, dim3: Int, initializer: (Int, Int, Int) -> Double = { _, _, _ -> 0.0 }): NDArray { - return realNDArray(listOf(dim1, dim2, dim3)) { initializer(it[0], it[1], it[2]) } -} \ No newline at end of file diff --git a/kmath-common/src/main/kotlin/scientifik/kmath/structures/NDArrayFactories.kt b/kmath-common/src/main/kotlin/scientifik/kmath/structures/NDArrayFactories.kt new file mode 100644 index 000000000..b3e3850e9 --- /dev/null +++ b/kmath-common/src/main/kotlin/scientifik/kmath/structures/NDArrayFactories.kt @@ -0,0 +1,38 @@ +package scientifik.kmath.structures + +import scientifik.kmath.operations.Field + +/** + * Create a platform-optimized NDArray of doubles + */ +expect fun realNDArray(shape: List, initializer: (List) -> Double = { 0.0 }): NDArray + +fun real2DArray(dim1: Int, dim2: Int, initializer: (Int, Int) -> Double = { _, _ -> 0.0 }): NDArray { + return realNDArray(listOf(dim1, dim2)) { initializer(it[0], it[1]) } +} + +fun real3DArray(dim1: Int, dim2: Int, dim3: Int, initializer: (Int, Int, Int) -> Double = { _, _, _ -> 0.0 }): NDArray { + return realNDArray(listOf(dim1, dim2, dim3)) { initializer(it[0], it[1], it[2]) } +} + + +class SimpleNDField(field: Field, shape: List) : BufferNDField(shape, field) { + override fun createBuffer(capacity: Int, initializer: (Int) -> T): Buffer { + val array = ArrayList(capacity) + (0 until capacity).forEach { + array.add(initializer(it)) + } + + return object : Buffer { + override fun get(index: Int): T = array[index] + + override fun set(index: Int, value: T) { + array[index] = initializer(index) + } + } + } +} + +fun simpleNDArray(field: Field, shape: List, initializer: (List) -> T): NDArray { + return SimpleNDField(field, shape).produce { initializer(it) } +} \ No newline at end of file diff --git a/kmath-common/src/test/kotlin/scientifik/kmath/structures/SimpleNDFieldTest.kt b/kmath-common/src/test/kotlin/scientifik/kmath/structures/SimpleNDFieldTest.kt new file mode 100644 index 000000000..5a4317251 --- /dev/null +++ b/kmath-common/src/test/kotlin/scientifik/kmath/structures/SimpleNDFieldTest.kt @@ -0,0 +1,15 @@ +package scientifik.kmath.structures + +import scientifik.kmath.operations.DoubleField +import kotlin.test.Test +import kotlin.test.assertEquals + + +class SimpleNDFieldTest{ + @Test + fun testStrides(){ + val ndArray = simpleNDArray(DoubleField, listOf(10,10)){(it[0]+it[1]).toDouble()} + assertEquals(ndArray[5,5], 10.0) + } + +} \ No newline at end of file diff --git a/kmath-jvm/build.gradle b/kmath-jvm/build.gradle index 82bda0270..0f1240a3a 100644 --- a/kmath-jvm/build.gradle +++ b/kmath-jvm/build.gradle @@ -1,4 +1,7 @@ -apply plugin: 'kotlin-platform-jvm' +plugins{ + id "kotlin-platform-jvm" + id "me.champeau.gradle.jmh" version "0.4.5" +} repositories { mavenCentral() diff --git a/kmath-jvm/src/jmh/kotlin/scietifik/kmath/structures/ArrayBenchmark.kt b/kmath-jvm/src/jmh/kotlin/scietifik/kmath/structures/ArrayBenchmark.kt new file mode 100644 index 000000000..3430b14d4 --- /dev/null +++ b/kmath-jvm/src/jmh/kotlin/scietifik/kmath/structures/ArrayBenchmark.kt @@ -0,0 +1,53 @@ +package scietifik.kmath.structures + +import org.openjdk.jmh.annotations.* +import java.nio.IntBuffer + + +@Fork(1) +@Warmup(iterations = 2) +@Measurement(iterations = 50) +@State(Scope.Benchmark) +open class ArrayBenchmark { + + lateinit var array: IntArray + lateinit var arrayBuffer: IntBuffer + lateinit var nativeBuffer: IntBuffer + + @Setup + fun setup() { + array = IntArray(10000) { it } + arrayBuffer = IntBuffer.wrap(array) + nativeBuffer = IntBuffer.allocate(10000) + for (i in 0 until 10000) { + nativeBuffer.put(i,i) + } + } + + @Benchmark + fun benchmarkArrayRead() { + var res = 0 + for (i in 1..10000) { + res += array[10000 - i] + } + print(res) + } + + @Benchmark + fun benchmarkBufferRead() { + var res = 0 + for (i in 1..10000) { + res += arrayBuffer.get(10000 - i) + } + print(res) + } + + @Benchmark + fun nativeBufferRead() { + var res = 0 + for (i in 1..10000) { + res += nativeBuffer.get(10000 - i) + } + print(res) + } +} \ No newline at end of file diff --git a/kmath-jvm/src/main/kotlin/scientifik/kmath/structures/RealNDArray.kt b/kmath-jvm/src/main/kotlin/scientifik/kmath/structures/RealNDArray.kt index 2c213f91e..8099e8700 100644 --- a/kmath-jvm/src/main/kotlin/scientifik/kmath/structures/RealNDArray.kt +++ b/kmath-jvm/src/main/kotlin/scientifik/kmath/structures/RealNDArray.kt @@ -3,78 +3,22 @@ package scientifik.kmath.structures import scientifik.kmath.operations.DoubleField import java.nio.DoubleBuffer -private class RealNDField(shape: List) : NDField(shape, DoubleField) { +private class RealNDField(shape: List) : BufferNDField(shape, DoubleField) { + override fun createBuffer(capacity: Int, initializer: (Int) -> Double): Buffer { + val array = DoubleArray(capacity, initializer) + val buffer = DoubleBuffer.wrap(array) + return object : Buffer { + override fun get(index: Int): Double = buffer.get(index) - /** - * Strides for memory access - */ - private val strides: List by lazy { - ArrayList(shape.size).apply { - var current = 1 - add(1) - shape.forEach { - current *= it - add(current) + override fun set(index: Int, value: Double) { + buffer.put(index, value) } } } - - fun offset(index: List): Int { - return index.mapIndexed { i, value -> - if (value < 0 || value >= shape[i]) { - throw RuntimeException("Index out of shape bounds: ($i,$value)") - } - value * strides[i] - }.sum() - } - - val capacity: Int - get() = strides[shape.size] - - - override fun produce(initializer: (List) -> Double): NDArray { - //TODO use sparse arrays for large capacities - val buffer = DoubleBuffer.allocate(capacity) - //FIXME there could be performance degradation due to iteration procedure. Replace by straight iteration - NDArray.iterateIndexes(shape).forEach { - buffer.put(offset(it), initializer(it)) - } - return RealNDArray(this, buffer) - } - - class RealNDArray(override val context: RealNDField, val data: DoubleBuffer) : NDArray { - - override fun get(vararg index: Int): Double { - return data.get(context.offset(index.asList())) - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as RealNDArray - - if (context.shape != other.context.shape) return false - if (data != other.data) return false - - return true - } - - override fun hashCode(): Int { - var result = context.shape.hashCode() - result = 31 * result + data.hashCode() - return result - } - - //TODO generate fixed hash code for quick comparison? - - - override val self: NDArray get() = this - } } - actual fun realNDArray(shape: List, initializer: (List) -> Double): NDArray { - //TODO cache fields? + //TODO create a cache for fields to save time generating strides? + return RealNDField(shape).produce { initializer(it) } } \ No newline at end of file