Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions bench/src/sjsonnet/bench/LongRenderingBenchmark.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package sjsonnet.bench

import org.openjdk.jmh.annotations.*
import org.openjdk.jmh.infra.Blackhole
import sjsonnet.*

import java.io.ByteArrayOutputStream
import java.util.concurrent.TimeUnit

@BenchmarkMode(Array(Mode.AverageTime))
@Fork(1)
@Warmup(iterations = 5)
@Measurement(iterations = 5)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@State(Scope.Thread)
class LongRenderingBenchmark {
import LongRenderingBenchmark.*

@Benchmark
def baseCharRendererLongs(bh: Blackhole): Unit = {
val out = new StringBuilderWriter(BatchSize * 20)
val renderer = new ExposedCharRenderer(out)
var i = 0
while (i < BatchSize) {
renderer.write(Values(i & ValuesMask))
i += 1
}
renderer.flushCharBuilder()
bh.consume(out.getBuilder.length)
}

@Benchmark
def baseByteRendererLongs(bh: Blackhole): Unit = {
val out = new ByteArrayOutputStream(BatchSize * 20)
val renderer = new ExposedByteRenderer(out)
var i = 0
while (i < BatchSize) {
renderer.write(Values(i & ValuesMask))
i += 1
}
renderer.flushByteBuilder()
bh.consume(out.size)
}

@Benchmark
def tomlRendererExactLongDoubles(bh: Blackhole): Unit = {
val out = new StringBuilderWriter(BatchSize * 20)
val renderer = new TomlRenderer(out, "", " ")
var i = 0
while (i < BatchSize) {
renderer.visitFloat64(TomlValues(i & TomlValuesMask), -1)
i += 1
}
bh.consume(out.getBuilder.length)
}
}

object LongRenderingBenchmark {
final val BatchSize = 4096

private val Values: Array[Long] = Array(
0L,
7L,
42L,
99L,
100L,
12345L,
99999999L,
100000000L,
123456789L,
999999999999L,
9007199254740991L,
9007199254740992L,
Long.MaxValue,
-1L,
-123456789L,
Long.MinValue
)
private val ValuesMask = Values.length - 1

private val TomlValues: Array[Double] = Array(
0.0, 7.0, 42.0, 99.0, 100.0, 12345.0, 99999999.0, 100000000.0, 123456789.0, 999999999999.0,
9007199254740991.0, 9007199254740992.0, -1.0, -123456789.0, -9007199254740991.0,
-9007199254740992.0
)
private val TomlValuesMask = TomlValues.length - 1

private final class ExposedCharRenderer(out: StringBuilderWriter)
extends BaseCharRenderer[StringBuilderWriter](out) {
def write(v: Long): Unit = writeLongDirect(v)
}

private final class ExposedByteRenderer(out: ByteArrayOutputStream)
extends BaseByteRenderer[ByteArrayOutputStream](out) {
def write(v: Long): Unit = writeLongDirect(v)
}
}
62 changes: 7 additions & 55 deletions sjsonnet/src/sjsonnet/BaseByteRenderer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class BaseByteRenderer[T <: java.io.OutputStream](

protected val elemBuilder = new upickle.core.ByteBuilder
private val unicodeCharBuilder = new upickle.core.CharBuilder
private val longScratchBuf: Array[Byte] = new Array[Byte](20)
private val longScratchBuf: Array[Byte] = new Array[Byte](FastLongRenderer.MaxLongChars)

def flushByteBuilder(): Unit = {
elemBuilder.writeOutToIfLongerThan(out, if (depth == 0) 0 else 8192)
Expand Down Expand Up @@ -166,49 +166,18 @@ class BaseByteRenderer[T <: java.io.OutputStream](

/**
* Write a long integer directly into elemBuilder without intermediate String allocation. Uses
* digit-pair lookup table for fast two-digits-at-a-time conversion.
* digit-pair lookup tables and 8-digit chunking to keep hardware long division out of the common
* int-sized path and to reduce it to at most two divisions for larger longs.
*/
protected def writeLongDirect(v: Long): Unit = {
flushBuffer()
if (v == 0L) {
elemBuilder.ensureLength(1)
elemBuilder.appendUnsafeC('0')
return
}
if (v == Long.MinValue) {
visitFloat64StringParts("-9223372036854775808", -1, -1, -1)
return
}
val negative = v < 0
var abs = if (negative) -v else v
// Write digits backward into a small local buffer, then bulk-copy.
// Max Long digits = 19, plus sign = 20.
val buf = longScratchBuf
var pos = 20
while (abs >= 100) {
val q = abs / 100
val r = (abs - q * 100L).toInt
abs = q
pos -= 2
buf(pos + 1) = BaseByteRenderer.DIGIT_ONES(r)
buf(pos) = BaseByteRenderer.DIGIT_TENS(r)
}
if (abs >= 10) {
val r = abs.toInt
pos -= 2
buf(pos + 1) = BaseByteRenderer.DIGIT_ONES(r)
buf(pos) = BaseByteRenderer.DIGIT_TENS(r)
} else {
pos -= 1
buf(pos) = ('0' + abs.toInt).toByte
}
if (negative) { pos -= 1; buf(pos) = '-'.toByte }
val totalLen = 20 - pos
elemBuilder.ensureLength(totalLen)
val len = FastLongRenderer.writeLong(v, buf, 0)
elemBuilder.ensureLength(len)
val bArr = elemBuilder.arr
val startPos = elemBuilder.length
System.arraycopy(buf, pos, bArr, startPos, totalLen)
elemBuilder.length = startPos + totalLen
System.arraycopy(buf, 0, bArr, startPos, len)
elemBuilder.length = startPos + len
}

def visitString(s: CharSequence, index: Int): T = {
Expand Down Expand Up @@ -545,21 +514,4 @@ object BaseByteRenderer {
'f'.toByte
)

/**
* Digit-pair lookup tables for two-digits-at-a-time integer rendering. DIGIT_TENS(i) gives the
* tens digit byte for value i (0..99). DIGIT_ONES(i) gives the ones digit byte for value i
* (0..99).
*/
private[sjsonnet] val DIGIT_TENS: Array[Byte] = {
val a = new Array[Byte](100)
var i = 0
while (i < 100) { a(i) = ('0' + i / 10).toByte; i += 1 }
a
}
private[sjsonnet] val DIGIT_ONES: Array[Byte] = {
val a = new Array[Byte](100)
var i = 0
while (i < 100) { a(i) = ('0' + i % 10).toByte; i += 1 }
a
}
}
67 changes: 14 additions & 53 deletions sjsonnet/src/sjsonnet/BaseCharRenderer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,6 @@ object BaseCharRenderer {
*/
final val MaxCachedDepth = 32

/** Digit-pair lookup tables for two-digits-at-a-time integer rendering. */
private[sjsonnet] val DIGIT_TENS: Array[Char] = {
val a = new Array[Char](100)
var i = 0
while (i < 100) { a(i) = ('0' + i / 10).toChar; i += 1 }
a
}
private[sjsonnet] val DIGIT_ONES: Array[Char] = {
val a = new Array[Char](100)
var i = 0
while (i < 100) { a(i) = ('0' + i % 10).toChar; i += 1 }
a
}

private[sjsonnet] val HEX_CHARS: Array[Char] =
Array('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f')
}
Expand All @@ -54,7 +40,7 @@ class BaseCharRenderer[T <: upickle.core.CharOps.Output](
elemBuilder.writeOutToIfLongerThan(out, if (depth == 0) 0 else 1000)
}

private val longScratchBuf: Array[Char] = new Array[Char](20)
private val longScratchBuf: Array[Byte] = new Array[Byte](FastLongRenderer.MaxLongChars)

protected var depth: Int = 0

Expand Down Expand Up @@ -218,49 +204,24 @@ class BaseCharRenderer[T <: upickle.core.CharOps.Output](

/**
* Write a long integer directly into elemBuilder without intermediate String allocation. Uses
* digit-pair lookup table for fast two-digits-at-a-time conversion.
* digit-pair lookup tables and 8-digit chunking to keep hardware long division out of the common
* int-sized path and to reduce it to at most two divisions for larger longs.
*/
protected def writeLongDirect(v: Long): Unit = {
flushBuffer()
if (v == 0L) {
elemBuilder.ensureLength(1)
elemBuilder.appendUnsafe('0')
return
}
if (v == Long.MinValue) {
visitFloat64StringParts("-9223372036854775808", -1, -1, -1)
return
}
val negative = v < 0
var abs = if (negative) -v else v
// Write digits backward into a small local buffer, then bulk-copy.
// Max Long digits = 19, plus sign = 20.
val buf = longScratchBuf
var pos = 20
while (abs >= 100) {
val q = abs / 100
val r = (abs - q * 100L).toInt
abs = q
pos -= 2
buf(pos + 1) = BaseCharRenderer.DIGIT_ONES(r)
buf(pos) = BaseCharRenderer.DIGIT_TENS(r)
}
if (abs >= 10) {
val r = abs.toInt
pos -= 2
buf(pos + 1) = BaseCharRenderer.DIGIT_ONES(r)
buf(pos) = BaseCharRenderer.DIGIT_TENS(r)
} else {
pos -= 1
buf(pos) = ('0' + abs.toInt).toChar
}
if (negative) { pos -= 1; buf(pos) = '-' }
val totalLen = 20 - pos
elemBuilder.ensureLength(totalLen)
val len = FastLongRenderer.writeLong(v, buf, 0)
elemBuilder.ensureLength(len)
val cbArr = elemBuilder.arr
val startPos = elemBuilder.getLength
System.arraycopy(buf, pos, cbArr, startPos, totalLen)
elemBuilder.length = startPos + totalLen
var pos = elemBuilder.getLength
val end = pos + len
var i = 0
while (pos < end) {
cbArr(pos) = buf(i).toChar
pos += 1
i += 1
}
elemBuilder.length = end
}

def visitString(s: CharSequence, index: Int): T = {
Expand Down
Loading
Loading