Memory on JVM and JS

This commit is contained in:
Alexander Nozik 2019-02-20 12:54:39 +03:00
parent 472dfd7b88
commit 25e8e03494
24 changed files with 441 additions and 177 deletions

View File

@ -16,6 +16,7 @@ dependencies {
implementation project(":kmath-commons")
implementation project(":kmath-koma")
implementation group: "com.kyonifer", name:"koma-core-ejml", version: "0.12"
implementation "org.jetbrains.kotlinx:kotlinx-io-jvm:0.1.5"
//compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
//jmh project(':kmath-core')
}

View File

@ -61,7 +61,7 @@ open class NDFieldBenchmark {
val dim = 1000
val n = 100
val bufferedField = NDField.auto(intArrayOf(dim, dim), RealField)
val bufferedField = NDField.auto(RealField, intArrayOf(dim, dim))
val specializedField = NDField.real(intArrayOf(dim, dim))
val genericField = NDField.buffered(intArrayOf(dim, dim), RealField)
val lazyNDField = NDField.lazy(intArrayOf(dim, dim), RealField)

View File

@ -1,15 +1,13 @@
package scientifik.kmath.structures
import scientifik.kmath.operations.Complex
import scientifik.kmath.operations.toComplex
import kotlin.system.measureTimeMillis
fun main() {
val dim = 1000
val n = 1000
val realField = NDField.real(intArrayOf(dim, dim))
val complexField = NDField.complex(intArrayOf(dim, dim))
val realField = NDField.real(dim, dim)
val complexField = NDField.complex(dim, dim)
val realTime = measureTimeMillis {

View File

@ -9,9 +9,9 @@ fun main(args: Array<String>) {
val n = 1000
// automatically build context most suited for given type.
val autoField = NDField.auto(intArrayOf(dim, dim), RealField)
val autoField = NDField.auto(RealField, dim, dim)
// specialized nd-field for Double. It works as generic Double field as well
val specializedField = NDField.real(intArrayOf(dim, dim))
val specializedField = NDField.real(dim, dim)
//A generic boxing field. It should be used for objects, not primitives.
val genericField = NDField.buffered(intArrayOf(dim, dim), RealField)

View File

@ -2,7 +2,7 @@ import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
buildscript {
val kotlinVersion: String by rootProject.extra("1.3.21")
val ioVersion: String by rootProject.extra("0.1.4")
val ioVersion: String by rootProject.extra("0.1.5")
val coroutinesVersion: String by rootProject.extra("1.1.1")
val atomicfuVersion: String by rootProject.extra("0.12.1")

View File

@ -12,6 +12,7 @@ kotlin {
sourceSets {
val commonMain by getting {
dependencies {
api(project(":kmath-memory"))
api(kotlin("stdlib"))
}
}

View File

@ -1,5 +1,11 @@
package scientifik.kmath.operations
import scientifik.kmath.structures.Buffer
import scientifik.kmath.structures.MutableBuffer
import scientifik.kmath.structures.ObjectBuffer
import scientifik.memory.MemoryReader
import scientifik.memory.MemorySpec
import scientifik.memory.MemoryWriter
import kotlin.math.*
/**
@ -67,7 +73,25 @@ data class Complex(val re: Double, val im: Double) : FieldElement<Complex, Compl
val theta: Double get() = atan(im / re)
companion object
companion object : MemorySpec<Complex> {
override val objectSize: Int = 16
override fun MemoryReader.read(offset: Int): Complex =
Complex(readDouble(offset), readDouble(offset + 8))
override fun MemoryWriter.write(offset: Int, value: Complex) {
writeDouble(offset, value.re)
writeDouble(offset + 8, value.im)
}
}
}
fun Double.toComplex() = Complex(this, 0.0)
fun Double.toComplex() = Complex(this, 0.0)
fun Buffer.Companion.complex(size: Int, init: (Int) -> Complex): Buffer<Complex> {
return ObjectBuffer.create(Complex, size, init)
}
fun MutableBuffer.Companion.complex(size: Int, init: (Int) -> Complex): Buffer<Complex> {
return ObjectBuffer.create(Complex, size, init)
}

View File

@ -3,6 +3,7 @@ package scientifik.kmath.structures
import scientifik.kmath.operations.Complex
import scientifik.kmath.operations.ComplexField
import scientifik.kmath.operations.FieldElement
import scientifik.kmath.operations.complex
typealias ComplexNDElement = BufferedNDFieldElement<Complex, ComplexField>
@ -128,4 +129,6 @@ operator fun ComplexNDElement.plus(arg: Double) =
map { it + arg }
operator fun ComplexNDElement.minus(arg: Double) =
map { it - arg }
map { it - arg }
fun NDField.Companion.complex(vararg shape: Int) = ComplexNDField(shape)

View File

@ -3,6 +3,7 @@ package scientifik.kmath.structures
import scientifik.kmath.operations.Field
import scientifik.kmath.operations.Ring
import scientifik.kmath.operations.Space
import kotlin.jvm.JvmName
/**
@ -118,7 +119,7 @@ interface NDField<T, F : Field<T>, N : NDStructure<T>> : Field<N>, NDRing<T, F,
/**
* Create a nd-field for [Double] values or pull it from cache if it was created previously
*/
fun real(shape: IntArray) = realNDFieldCache.getOrPut(shape){RealNDField(shape)}
fun real(vararg shape: Int) = realNDFieldCache.getOrPut(shape) { RealNDField(shape) }
/**
* Create a nd-field with boxing generic buffer
@ -134,9 +135,9 @@ interface NDField<T, F : Field<T>, N : NDStructure<T>> : Field<N>, NDRing<T, F,
* Create a most suitable implementation for nd-field using reified class.
*/
@Suppress("UNCHECKED_CAST")
inline fun <reified T : Any, F : Field<T>> auto(shape: IntArray, field: F): BufferedNDField<T, F> =
inline fun <reified T : Any, F : Field<T>> auto(field: F, vararg shape: Int): BufferedNDField<T, F> =
when {
T::class == Double::class -> real(shape) as BufferedNDField<T, F>
T::class == Double::class -> real(*shape) as BufferedNDField<T, F>
else -> BoxingNDField(shape, field, Buffer.Companion::auto)
}
}

View File

@ -24,7 +24,7 @@ interface NDElement<T, C, N : NDStructure<T>> : NDStructure<T> {
* Create a optimized NDArray of doubles
*/
fun real(shape: IntArray, initializer: RealField.(IntArray) -> Double = { 0.0 }) =
NDField.real(shape).produce(initializer)
NDField.real(*shape).produce(initializer)
fun real1D(dim: Int, initializer: (Int) -> Double = { _ -> 0.0 }) =
@ -55,7 +55,7 @@ interface NDElement<T, C, N : NDStructure<T>> : NDStructure<T> {
field: F,
noinline initializer: F.(IntArray) -> T
): BufferedNDFieldElement<T, F> {
val ndField = NDField.auto(shape, field)
val ndField = NDField.auto(field, *shape)
return BufferedNDFieldElement(ndField, ndField.produce(initializer).buffer)
}
}

View File

@ -0,0 +1,48 @@
package scientifik.kmath.structures
import scientifik.memory.*
/**
* A non-boxing buffer based on [ByteBuffer] storage
*/
open class ObjectBuffer<T : Any>(protected val memory: Memory, protected val spec: MemorySpec<T>) : Buffer<T> {
override val size: Int get() = memory.size / spec.objectSize
private val reader = memory.reader()
override fun get(index: Int): T = reader.read(spec, spec.objectSize * index)
override fun iterator(): Iterator<T> = (0 until size).asSequence().map { get(it) }.iterator()
companion object {
fun <T : Any> create(spec: MemorySpec<T>, size: Int) =
ObjectBuffer(Memory.allocate(size * spec.objectSize), spec)
inline fun <T : Any> create(
spec: MemorySpec<T>,
size: Int,
crossinline initializer: (Int) -> T
): ObjectBuffer<T> =
MutableObjectBuffer(Memory.allocate(size * spec.objectSize), spec).also { buffer ->
(0 until size).forEach {
buffer[it] = initializer(it)
}
}
}
}
class MutableObjectBuffer<T : Any>(memory: Memory, spec: MemorySpec<T>) : ObjectBuffer<T>(memory, spec),
MutableBuffer<T> {
private val writer = memory.writer()
override fun set(index: Int, value: T) = writer.write(spec, spec.objectSize * index, value)
override fun copy(): MutableBuffer<T> = MutableObjectBuffer(memory.copy(), spec)
companion object {
}
}

View File

@ -1,13 +1,14 @@
package scientifik.kmath.structures
import org.junit.Test
import scientifik.kmath.operations.Complex
import scientifik.kmath.operations.complex
import kotlin.test.Test
import kotlin.test.assertEquals
class ComplexBufferSpecTest {
@Test
fun testComplexBuffer() {
val buffer = MutableBuffer.complex(20){Complex(it.toDouble(), -it.toDouble())}
val buffer = Buffer.complex(20) { Complex(it.toDouble(), -it.toDouble()) }
assertEquals(Complex(5.0, -5.0), buffer[5])
}
}

View File

@ -63,7 +63,7 @@ class NumberNDFieldTest {
@Test
fun testInternalContext() {
NDField.real(array1.shape).run {
NDField.real(*array1.shape).run {
with(L2Norm) {
1 + norm(array1) + exp(array2)
}

View File

@ -1,44 +0,0 @@
package scientifik.kmath.structures
import java.nio.ByteBuffer
/**
* A specification for serialization and deserialization objects to buffer (at current buffer position)
*/
interface BufferSpec<T : Any> {
/**
* Read an object from buffer in current position
*/
fun ByteBuffer.readObject(): T
/**
* Write object to [ByteBuffer] in current buffer position
*/
fun ByteBuffer.writeObject(value: T)
}
/**
* A [BufferSpec] with fixed unit size. Allows storage of any object without boxing.
*/
interface FixedSizeBufferSpec<T : Any> : BufferSpec<T> {
val unitSize: Int
/**
* Read an object from buffer in given index (not buffer position
*/
fun ByteBuffer.readObject(index: Int): T {
position(index * unitSize)
return readObject()
}
/**
* Put an object in given index
*/
fun ByteBuffer.writeObject(index: Int, obj: T) {
position(index * unitSize)
writeObject(obj)
}
}

View File

@ -1,46 +0,0 @@
package scientifik.kmath.structures
import scientifik.kmath.operations.Complex
import scientifik.kmath.operations.ComplexField
import java.nio.ByteBuffer
/**
* A serialization specification for complex numbers
*/
object ComplexBufferSpec : FixedSizeBufferSpec<Complex> {
override val unitSize: Int = 16
override fun ByteBuffer.readObject(): Complex {
val re = double
val im = double
return Complex(re, im)
}
override fun ByteBuffer.writeObject(value: Complex) {
putDouble(value.re)
putDouble(value.im)
}
}
/**
* Create a read-only/mutable buffer which ignores boxing
*/
fun Buffer.Companion.complex(size: Int): Buffer<Complex> =
ObjectBuffer.create(ComplexBufferSpec, size)
inline fun Buffer.Companion.complex(size: Int, crossinline initializer: (Int) -> Complex): Buffer<Complex> =
ObjectBuffer.create(ComplexBufferSpec, size, initializer)
fun MutableBuffer.Companion.complex(size: Int) =
ObjectBuffer.create(ComplexBufferSpec, size)
inline fun MutableBuffer.Companion.complex(size: Int, crossinline initializer: (Int) -> Complex) =
ObjectBuffer.create(ComplexBufferSpec, size, initializer)
fun NDField.Companion.complex(shape: IntArray) = ComplexNDField(shape)
fun NDElement.Companion.complex(shape: IntArray, initializer: ComplexField.(IntArray) -> Complex) =
NDField.complex(shape).produce(initializer)

View File

@ -1,39 +0,0 @@
package scientifik.kmath.structures
import java.nio.ByteBuffer
/**
* A non-boxing buffer based on [ByteBuffer] storage
*/
class ObjectBuffer<T : Any>(private val buffer: ByteBuffer, private val spec: FixedSizeBufferSpec<T>) :
MutableBuffer<T> {
override val size: Int
get() = buffer.limit() / spec.unitSize
override fun get(index: Int): T = with(spec) { buffer.readObject(index) }
override fun iterator(): Iterator<T> = (0 until size).asSequence().map { get(it) }.iterator()
override fun set(index: Int, value: T) = with(spec) { buffer.writeObject(index, value) }
override fun copy(): MutableBuffer<T> {
val dup = buffer.duplicate()
val copy = ByteBuffer.allocate(dup.capacity())
dup.rewind()
copy.put(dup)
copy.flip()
return ObjectBuffer(copy, spec)
}
companion object {
fun <T : Any> create(spec: FixedSizeBufferSpec<T>, size: Int) =
ObjectBuffer(ByteBuffer.allocate(size * spec.unitSize), spec)
inline fun <T : Any> create(spec: FixedSizeBufferSpec<T>, size: Int, crossinline initializer: (Int) -> T) =
ObjectBuffer(ByteBuffer.allocate(size * spec.unitSize), spec).also { buffer ->
(0 until size).forEach {
buffer[it] = initializer(it)
}
}
}
}

View File

@ -1,28 +0,0 @@
package scientifik.kmath.structures
import scientifik.kmath.operations.Real
import java.nio.ByteBuffer
object RealBufferSpec : FixedSizeBufferSpec<Real> {
override val unitSize: Int = 8
override fun ByteBuffer.readObject(): Real = Real(double)
override fun ByteBuffer.writeObject(value: Real) {
putDouble(value.value)
}
}
object DoubleBufferSpec : FixedSizeBufferSpec<Double> {
override val unitSize: Int = 8
override fun ByteBuffer.readObject() = double
override fun ByteBuffer.writeObject(value: Double) {
putDouble(value)
}
}
fun Double.Companion.createBuffer(size: Int) = ObjectBuffer.create(DoubleBufferSpec, size)
fun Real.Companion.createBuffer(size: Int) = ObjectBuffer.create(RealBufferSpec, size)

View File

@ -0,0 +1,50 @@
plugins {
kotlin("multiplatform")
}
val ioVersion: String by rootProject.extra
kotlin {
jvm()
js()
sourceSets {
val commonMain by getting {
dependencies {
api(kotlin("stdlib"))
}
}
val commonTest by getting {
dependencies {
implementation(kotlin("test-common"))
implementation(kotlin("test-annotations-common"))
}
}
val jvmMain by getting {
dependencies {
api(kotlin("stdlib-jdk8"))
}
}
val jvmTest by getting {
dependencies {
implementation(kotlin("test"))
implementation(kotlin("test-junit"))
}
}
val jsMain by getting {
dependencies {
api(kotlin("stdlib-js"))
}
}
val jsTest by getting {
dependencies {
implementation(kotlin("test-js"))
}
}
// mingwMain {
// }
// mingwTest {
// }
}
}

View File

@ -0,0 +1,71 @@
package scientifik.memory
interface Memory {
val size: Int
/**
* Get a projection of this memory (it reflects the changes in the parent memory block)
*/
fun view(offset: Int, length: Int): Memory
/**
* Create a copy of this memory, which does not know anything about this memory
*/
fun copy(): Memory
/**
* Create and possibly register a new reader
*/
fun reader(): MemoryReader
fun writer(): MemoryWriter
companion object {
}
}
interface MemoryReader {
val memory: Memory
fun readDouble(offset: Int): Double
fun readFloat(offset: Int): Float
fun readByte(offset: Int): Byte
fun readShort(offset: Int): Short
fun readInt(offset: Int): Int
fun readLong(offset: Int): Long
fun release()
}
/**
* Use the memory for read then release the reader
*/
inline fun Memory.read(block: MemoryReader.() -> Unit) {
reader().apply(block).apply { release() }
}
interface MemoryWriter {
val memory: Memory
fun writeDouble(offset: Int, value: Double)
fun writeFloat(offset: Int, value: Float)
fun writeByte(offset: Int, value: Byte)
fun writeShort(offset: Int, value: Short)
fun writeInt(offset: Int, value: Int)
fun writeLong(offset: Int, value: Long)
fun release()
}
/**
* Use the memory for write then release the writer
*/
inline fun Memory.write(block: MemoryWriter.() -> Unit) {
writer().apply(block).apply { release() }
}
/**
* Allocate the most effective platform-specific memory
*/
expect fun Memory.Companion.allocate(length: Int): Memory

View File

@ -0,0 +1,34 @@
package scientifik.memory
/**
* A specification to read or write custom objects with fixed size in bytes
*/
interface MemorySpec<T : Any> {
/**
* Size of [T] in bytes after serialization
*/
val objectSize: Int
fun MemoryReader.read(offset: Int): T
fun MemoryWriter.write(offset: Int, value: T)
}
fun <T : Any> MemoryReader.read(spec: MemorySpec<T>, offset: Int): T = spec.run { read(offset) }
fun <T : Any> MemoryWriter.write(spec: MemorySpec<T>, offset: Int, value: T) = spec.run { write(offset, value) }
inline fun <reified T : Any> MemoryReader.readArray(spec: MemorySpec<T>, offset: Int, size: Int) =
Array(size) { i ->
spec.run {
read(offset + i * objectSize)
}
}
fun <T : Any> MemoryWriter.writeArray(spec: MemorySpec<T>, offset: Int, array: Array<T>) {
spec.run {
for (i in 0 until array.size) {
write(offset + i * objectSize, array[i])
}
}
}
//TODO It is possible to add elastic MemorySpec with unknown object size

View File

@ -0,0 +1,91 @@
package scientifik.memory
import org.khronos.webgl.ArrayBuffer
import org.khronos.webgl.DataView
/**
* Allocate the most effective platform-specific memory
*/
actual fun Memory.Companion.allocate(length: Int): Memory {
val buffer = ArrayBuffer(length)
return DataViewMemory(DataView(buffer, 0, length))
}
class DataViewMemory(val view: DataView) : Memory {
override val size: Int get() = view.byteLength
override fun view(offset: Int, length: Int): Memory {
require(offset >= 0) { "offset shouldn't be negative: $offset" }
require(length >= 0) { "length shouldn't be negative: $length" }
if (offset + length > size) {
throw IndexOutOfBoundsException("offset + length > size: $offset + $length > $size")
}
return DataViewMemory(DataView(view.buffer, view.byteOffset + offset, length))
}
override fun copy(): Memory {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
private val reader = object : MemoryReader {
override val memory: Memory get() = this@DataViewMemory
override fun readDouble(offset: Int): Double = view.getFloat64(offset, false)
override fun readFloat(offset: Int): Float = view.getFloat32(offset, false)
override fun readByte(offset: Int): Byte = view.getInt8(offset)
override fun readShort(offset: Int): Short = view.getInt16(offset, false)
override fun readInt(offset: Int): Int = view.getInt32(offset, false)
override fun readLong(offset: Int): Long = (view.getInt32(offset, false).toLong() shl 32) or
view.getInt32(offset + 4, false).toLong()
override fun release() {
// does nothing on JS because of GC
}
}
override fun reader(): MemoryReader = reader
private val writer = object : MemoryWriter {
override val memory: Memory get() = this@DataViewMemory
override fun writeDouble(offset: Int, value: Double) {
view.setFloat64(offset, value, false)
}
override fun writeFloat(offset: Int, value: Float) {
view.setFloat32(offset, value, false)
}
override fun writeByte(offset: Int, value: Byte) {
view.setInt8(offset, value)
}
override fun writeShort(offset: Int, value: Short) {
view.setUint16(offset, value, false)
}
override fun writeInt(offset: Int, value: Int) {
view.setInt32(offset, value, false)
}
override fun writeLong(offset: Int, value: Long) {
view.setInt32(offset, (value shr 32).toInt(), littleEndian = false)
view.setInt32(offset + 4, (value and 0xffffffffL).toInt(), littleEndian = false)
}
override fun release() {
//does nothing on JS
}
}
override fun writer(): MemoryWriter = writer
}

View File

@ -0,0 +1,93 @@
package scientifik.memory
import java.nio.ByteBuffer
/**
* Allocate the most effective platform-specific memory
*/
actual fun Memory.Companion.allocate(length: Int): Memory {
val buffer = ByteBuffer.allocate(length)
return ByteBufferMemory(buffer)
}
class ByteBufferMemory(
val buffer: ByteBuffer,
val startOffset: Int = 0,
override val size: Int = buffer.limit()
) : Memory {
@Suppress("NOTHING_TO_INLINE")
private inline fun position(o: Int): Int = startOffset + o
override fun view(offset: Int, length: Int): Memory {
if (offset + length > size) error("Selecting a Memory view outside of memory range")
return ByteBufferMemory(buffer, position(offset), length)
}
override fun copy(): Memory {
val copy = ByteBuffer.allocate(buffer.capacity())
buffer.rewind()
copy.put(buffer)
copy.flip()
return ByteBufferMemory(copy)
}
private val reader = object : MemoryReader {
override val memory: Memory get() = this@ByteBufferMemory
override fun readDouble(offset: Int) = buffer.getDouble(position(offset))
override fun readFloat(offset: Int) = buffer.getFloat(position(offset))
override fun readByte(offset: Int) = buffer.get(position(offset))
override fun readShort(offset: Int) = buffer.getShort(position(offset))
override fun readInt(offset: Int) = buffer.getInt(position(offset))
override fun readLong(offset: Int) = buffer.getLong(position(offset))
override fun release() {
//does nothing on JVM
}
}
override fun reader(): MemoryReader = reader
private val writer = object : MemoryWriter {
override val memory: Memory get() = this@ByteBufferMemory
override fun writeDouble(offset: Int, value: Double) {
buffer.putDouble(position(offset), value)
}
override fun writeFloat(offset: Int, value: Float) {
buffer.putFloat(position(offset), value)
}
override fun writeByte(offset: Int, value: Byte) {
buffer.put(position(offset), value)
}
override fun writeShort(offset: Int, value: Short) {
buffer.putShort(position(offset), value)
}
override fun writeInt(offset: Int, value: Int) {
buffer.putInt(position(offset), value)
}
override fun writeLong(offset: Int, value: Long) {
buffer.putLong(position(offset), value)
}
override fun release() {
//does nothing on JVM
}
}
override fun writer(): MemoryWriter = writer
}

View File

@ -1,6 +1,7 @@
package scientifik.kmath.sequential
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.produce
import kotlinx.coroutines.isActive
@ -12,6 +13,7 @@ import scientifik.kmath.structures.BufferFactory
/**
* A processor that collects incoming elements into fixed size buffers
*/
@ExperimentalCoroutinesApi
class JoinProcessor<T>(
scope: CoroutineScope,
bufferSize: Int,
@ -68,9 +70,11 @@ class SplitProcessor<T>(scope: CoroutineScope) : AbstractProcessor<Buffer<T>, T>
}
}
@ExperimentalCoroutinesApi
fun <T> Producer<T>.chunked(chunkSize: Int, bufferFactory: BufferFactory<T>) =
JoinProcessor<T>(this, chunkSize, bufferFactory).also { connect(it) }
@ExperimentalCoroutinesApi
inline fun <reified T : Any> Producer<T>.chunked(chunkSize: Int) =
JoinProcessor<T>(this, chunkSize, Buffer.Companion::auto).also { connect(it) }

View File

@ -15,10 +15,11 @@ pluginManagement {
}
}
//enableFeaturePreview("GRADLE_METADATA")
enableFeaturePreview("GRADLE_METADATA")
rootProject.name = "kmath"
include(
":kmath-memory",
":kmath-core",
// ":kmath-io",
":kmath-coroutines",