Generic definition for NDArray

This commit is contained in:
Alexander Nozik 2018-08-29 13:45:06 +03:00
parent 6c25042f0f
commit ba63b2e373
8 changed files with 223 additions and 81 deletions

View File

@ -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 { repositories {
mavenCentral() mavenCentral()
@ -12,3 +14,9 @@ dependencies {
testCompile "org.jetbrains.kotlin:kotlin-test-common:$kotlin_version" testCompile "org.jetbrains.kotlin:kotlin-test-common:$kotlin_version"
} }
kotlin {
experimental {
coroutines "enable"
}
}

View File

@ -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<T> {
operator fun get(index: Int): T
operator fun set(index: Int, value: T)
}
/**
* Generic implementation of NDField based on continuous buffer
*/
abstract class BufferNDField<T>(shape: List<Int>, field: Field<T>) : NDField<T>(shape, field) {
/**
* Strides for memory access
*/
private val strides: List<Int> by lazy {
ArrayList<Int>(shape.size).apply {
var current = 1
add(1)
shape.forEach {
current *= it
add(current)
}
}
}
protected fun offset(index: List<Int>): 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<Int>{
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<T>
override fun produce(initializer: (List<Int>) -> T): NDArray<T> {
val buffer = createBuffer(capacity){initializer(index(it))}
return BufferNDArray(this, buffer)
}
class BufferNDArray<T>(override val context: BufferNDField<T>, val data: Buffer<T>) : NDArray<T> {
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<T> get() = this
}
}

View File

@ -198,15 +198,3 @@ operator fun <T> T.div(arg: NDArray<T>): NDArray<T> = arg.transform { _, value -
} }
} }
/**
* Create a platform-specific NDArray of doubles
*/
expect fun realNDArray(shape: List<Int>, initializer: (List<Int>) -> Double = { 0.0 }): NDArray<Double>
fun real2DArray(dim1: Int, dim2: Int, initializer: (Int, Int) -> Double = { _, _ -> 0.0 }): NDArray<Double> {
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<Double> {
return realNDArray(listOf(dim1, dim2, dim3)) { initializer(it[0], it[1], it[2]) }
}

View File

@ -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<Int>, initializer: (List<Int>) -> Double = { 0.0 }): NDArray<Double>
fun real2DArray(dim1: Int, dim2: Int, initializer: (Int, Int) -> Double = { _, _ -> 0.0 }): NDArray<Double> {
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<Double> {
return realNDArray(listOf(dim1, dim2, dim3)) { initializer(it[0], it[1], it[2]) }
}
class SimpleNDField<T: Any>(field: Field<T>, shape: List<Int>) : BufferNDField<T>(shape, field) {
override fun createBuffer(capacity: Int, initializer: (Int) -> T): Buffer<T> {
val array = ArrayList<T>(capacity)
(0 until capacity).forEach {
array.add(initializer(it))
}
return object : Buffer<T> {
override fun get(index: Int): T = array[index]
override fun set(index: Int, value: T) {
array[index] = initializer(index)
}
}
}
}
fun <T: Any> simpleNDArray(field: Field<T>, shape: List<Int>, initializer: (List<Int>) -> T): NDArray<T> {
return SimpleNDField(field, shape).produce { initializer(it) }
}

View File

@ -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)
}
}

View File

@ -1,4 +1,7 @@
apply plugin: 'kotlin-platform-jvm' plugins{
id "kotlin-platform-jvm"
id "me.champeau.gradle.jmh" version "0.4.5"
}
repositories { repositories {
mavenCentral() mavenCentral()

View File

@ -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)
}
}

View File

@ -3,78 +3,22 @@ package scientifik.kmath.structures
import scientifik.kmath.operations.DoubleField import scientifik.kmath.operations.DoubleField
import java.nio.DoubleBuffer import java.nio.DoubleBuffer
private class RealNDField(shape: List<Int>) : NDField<Double>(shape, DoubleField) { private class RealNDField(shape: List<Int>) : BufferNDField<Double>(shape, DoubleField) {
override fun createBuffer(capacity: Int, initializer: (Int) -> Double): Buffer<Double> {
val array = DoubleArray(capacity, initializer)
val buffer = DoubleBuffer.wrap(array)
return object : Buffer<Double> {
override fun get(index: Int): Double = buffer.get(index)
/** override fun set(index: Int, value: Double) {
* Strides for memory access buffer.put(index, value)
*/
private val strides: List<Int> by lazy {
ArrayList<Int>(shape.size).apply {
var current = 1
add(1)
shape.forEach {
current *= it
add(current)
} }
} }
} }
fun offset(index: List<Int>): 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<Int>) -> Double): NDArray<Double> {
//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<Double> {
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<Double> get() = this
}
} }
actual fun realNDArray(shape: List<Int>, initializer: (List<Int>) -> Double): NDArray<Double> { actual fun realNDArray(shape: List<Int>, initializer: (List<Int>) -> Double): NDArray<Double> {
//TODO cache fields? //TODO create a cache for fields to save time generating strides?
return RealNDField(shape).produce { initializer(it) } return RealNDField(shape).produce { initializer(it) }
} }