-
Notifications
You must be signed in to change notification settings - Fork 624
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Resolves #195
- Loading branch information
Showing
25 changed files
with
678 additions
and
129 deletions.
There are no files selected for viewing
116 changes: 116 additions & 0 deletions
116
benchmark/src/jmh/kotlin/kotlinx/benchmarks/json/OmitNullBenchmark.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
package kotlinx.benchmarks.json | ||
|
||
import kotlinx.serialization.Serializable | ||
import kotlinx.serialization.decodeFromString | ||
import kotlinx.serialization.encodeToString | ||
import kotlinx.serialization.json.Json | ||
import org.openjdk.jmh.annotations.* | ||
import java.util.concurrent.TimeUnit | ||
|
||
@Warmup(iterations = 5, time = 1) | ||
@Measurement(iterations = 5, time = 1) | ||
@BenchmarkMode(Mode.Throughput) | ||
@OutputTimeUnit(TimeUnit.MILLISECONDS) | ||
@State(Scope.Benchmark) | ||
@Fork(2) | ||
open class OmitNullBenchmark { | ||
|
||
@Serializable | ||
data class Values( | ||
val field0: Int?, | ||
val field1: Int?, | ||
val field2: Int?, | ||
val field3: Int?, | ||
val field4: Int?, | ||
val field5: Int?, | ||
val field6: Int?, | ||
val field7: Int?, | ||
val field8: Int?, | ||
val field9: Int?, | ||
|
||
val field10: Int?, | ||
val field11: Int?, | ||
val field12: Int?, | ||
val field13: Int?, | ||
val field14: Int?, | ||
val field15: Int?, | ||
val field16: Int?, | ||
val field17: Int?, | ||
val field18: Int?, | ||
val field19: Int?, | ||
|
||
val field20: Int?, | ||
val field21: Int?, | ||
val field22: Int?, | ||
val field23: Int?, | ||
val field24: Int?, | ||
val field25: Int?, | ||
val field26: Int?, | ||
val field27: Int?, | ||
val field28: Int?, | ||
val field29: Int?, | ||
|
||
val field30: Int?, | ||
val field31: Int? | ||
) | ||
|
||
|
||
private val jsonOmitNull = Json { omitNull = true } | ||
|
||
private val valueWithNulls = Values( | ||
null, null, 2, null, null, null, null, null, null, null, | ||
null, null, null, null, 14, null, null, null, null, null, | ||
null, null, null, null, null, null, null, null, null, null, | ||
null, null | ||
) | ||
|
||
|
||
private val jsonWithNulls = """{"field0":null,"field1":null,"field2":2,"field3":null,"field4":null,"field5":null, | ||
|"field6":null,"field7":null,"field8":null,"field9":null,"field10":null,"field11":null,"field12":null, | ||
|"field13":null,"field14":14,"field15":null,"field16":null,"field17":null,"field18":null,"field19":null, | ||
|"field20":null,"field21":null,"field22":null,"field23":null,"field24":null,"field25":null,"field26":null, | ||
|"field27":null,"field28":null,"field29":null,"field30":null,"field31":null}""".trimMargin() | ||
|
||
private val jsonNoNulls = """{"field0":0,"field1":1,"field2":2,"field3":3,"field4":4,"field5":5, | ||
|"field6":6,"field7":7,"field8":8,"field9":9,"field10":10,"field11":11,"field12":12, | ||
|"field13":13,"field14":14,"field15":15,"field16":16,"field17":17,"field18":18,"field19":19, | ||
|"field20":20,"field21":21,"field22":22,"field23":23,"field24":24,"field25":25,"field26":26, | ||
|"field27":27,"field28":28,"field29":29,"field30":30,"field31":31}""".trimMargin() | ||
|
||
private val jsonWithAbsence = """{"field2":2, "field14":14}""" | ||
|
||
@Benchmark | ||
fun decodeNoNulls() { | ||
Json.decodeFromString<Values>(jsonNoNulls) | ||
} | ||
|
||
@Benchmark | ||
fun decodeNoNullsWithOmit() { | ||
jsonOmitNull.decodeFromString<Values>(jsonNoNulls) | ||
} | ||
|
||
@Benchmark | ||
fun decodeNulls() { | ||
Json.decodeFromString<Values>(jsonWithNulls) | ||
} | ||
|
||
@Benchmark | ||
fun decodeNullsWithOmit() { | ||
jsonOmitNull.decodeFromString<Values>(jsonWithNulls) | ||
} | ||
|
||
@Benchmark | ||
fun decodeAbsenceWithOmit() { | ||
jsonOmitNull.decodeFromString<Values>(jsonWithAbsence) | ||
} | ||
|
||
@Benchmark | ||
fun encodeNulls() { | ||
Json.encodeToString(valueWithNulls) | ||
} | ||
|
||
@Benchmark | ||
fun encodeNullsWithOmit() { | ||
jsonOmitNull.encodeToString(valueWithNulls) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
93 changes: 93 additions & 0 deletions
93
core/commonMain/src/kotlinx/serialization/internal/ElementMarker.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
package kotlinx.serialization.internal | ||
|
||
import kotlinx.serialization.ExperimentalSerializationApi | ||
import kotlinx.serialization.InternalSerializationApi | ||
import kotlinx.serialization.descriptors.SerialDescriptor | ||
import kotlinx.serialization.encoding.CompositeDecoder | ||
|
||
@InternalSerializationApi | ||
@OptIn(ExperimentalSerializationApi::class) | ||
public abstract class ElementMarker(private val descriptor: SerialDescriptor) { | ||
/* | ||
Element decoding marks from given bytes. | ||
The element number is the same as the bit position. | ||
Marks for the lowest 64 elements are always stored in a single Long value, higher elements stores in long array. | ||
*/ | ||
private var lowerMarks: Long | ||
private val highMarksArray: LongArray? | ||
|
||
init { | ||
val elementsCount = descriptor.elementsCount | ||
if (elementsCount <= Long.SIZE_BITS) { | ||
lowerMarks = if (elementsCount == Long.SIZE_BITS) { | ||
// number of bits in the mark is equal to the number of fields | ||
0L | ||
} else { | ||
// (1 - elementsCount) bits are always 1 since there are no fields for them | ||
-1L shl elementsCount | ||
} | ||
highMarksArray = null | ||
} else { | ||
lowerMarks = 0L | ||
// (elementsCount - 1) because only one Long value is needed to store 64 fields etc | ||
val slotsCount = (elementsCount - 1) / Long.SIZE_BITS | ||
val elementsInLastSlot = elementsCount % Long.SIZE_BITS | ||
val highMarks = LongArray(slotsCount) | ||
// (elementsCount % Long.SIZE_BITS) == 0 this means that the fields occupy all bits in mark | ||
if (elementsInLastSlot != 0) { | ||
// all marks except the higher are always 0 | ||
highMarks[highMarks.lastIndex] = -1L shl elementsCount | ||
} | ||
highMarksArray = highMarks | ||
} | ||
} | ||
|
||
protected abstract fun isPoppedElement(descriptor: SerialDescriptor, index: Int): Boolean | ||
|
||
public fun popUnmarkedIndex(): Int { | ||
val elementsCount = descriptor.elementsCount | ||
while (lowerMarks != -1L) { | ||
val index = lowerMarks.inv().countTrailingZeroBits() | ||
lowerMarks = lowerMarks or (1L shl index) | ||
|
||
if (isPoppedElement(descriptor, index)) { | ||
return index | ||
} | ||
} | ||
|
||
if (elementsCount > Long.SIZE_BITS) { | ||
val higherMarks = highMarksArray!! | ||
|
||
for (slot in higherMarks.indices) { | ||
// (slot + 1) because first element in high marks has index 64 | ||
val slotOffset = (slot + 1) * Long.SIZE_BITS | ||
// store in a variable so as not to frequently use the array | ||
var mark = higherMarks[slot] | ||
|
||
while (mark != -1L) { | ||
val indexInSlot = mark.inv().countTrailingZeroBits() | ||
mark = mark or (1L shl indexInSlot) | ||
|
||
val index = slotOffset + indexInSlot | ||
if (isPoppedElement(descriptor, index)) { | ||
higherMarks[slot] = mark | ||
return index | ||
} | ||
} | ||
higherMarks[slot] = mark | ||
} | ||
return CompositeDecoder.DECODE_DONE | ||
} | ||
return CompositeDecoder.DECODE_DONE | ||
} | ||
|
||
public fun mark(index: Int) { | ||
if (index < Long.SIZE_BITS) { | ||
lowerMarks = lowerMarks or (1L shl index) | ||
} else { | ||
val slot = (index / Long.SIZE_BITS) - 1 | ||
val offsetInSlot = index % Long.SIZE_BITS | ||
highMarksArray!![slot] = highMarksArray[slot] or (1L shl offsetInSlot) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.