From 0e230a190e357826b844060fd4477539affaafcd Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Thu, 6 Feb 2025 16:49:44 +0100 Subject: [PATCH] More efficient encoding of primitives and `java.time._` values --- .../zio/json/internal/FastStringWrite.scala | 84 +++++ .../scala/zio/json/internal/SafeNumbers.scala | 286 +++++++++++++--- .../zio/json/internal/FastStringWrite.scala | 171 ++++++++++ .../scala/zio/json/internal/SafeNumbers.scala | 254 +++++++++++--- .../zio/json/internal/FastStringWrite.scala | 171 ++++++++++ .../scala/zio/json/internal/SafeNumbers.scala | 254 +++++++++++--- .../src/main/scala-2.x/zio/json/macros.scala | 72 ++-- .../src/main/scala-3/zio/json/macros.scala | 73 ++-- .../src/main/scala/zio/json/JsonEncoder.scala | 266 ++++++++++++--- .../scala/zio/json/internal/writers.scala | 67 +++- .../scala/zio/json/javatime/serializers.scala | 320 +++++++++++------- 11 files changed, 1658 insertions(+), 360 deletions(-) create mode 100644 zio-json/js/src/main/scala/zio/json/internal/FastStringWrite.scala create mode 100644 zio-json/jvm/src/main/scala/zio/json/internal/FastStringWrite.scala create mode 100644 zio-json/native/src/main/scala/zio/json/internal/FastStringWrite.scala diff --git a/zio-json/js/src/main/scala/zio/json/internal/FastStringWrite.scala b/zio-json/js/src/main/scala/zio/json/internal/FastStringWrite.scala new file mode 100644 index 000000000..c9dc72782 --- /dev/null +++ b/zio-json/js/src/main/scala/zio/json/internal/FastStringWrite.scala @@ -0,0 +1,84 @@ +package zio.json.internal + +final class FastStringWrite(initial: Int) extends Write { + require(initial >= 8) + private[this] var chars: String = "" + + @inline def reset(): Unit = chars = "" + + @inline private[internal] def length: Int = chars.length + + @inline private[internal] def getChars: Array[Char] = chars.toCharArray + + @inline def write(s: String): Unit = chars += s + + @inline def write(c: Char): Unit = chars += c + + @inline override def write(cs: Array[Char], from: Int, to: Int): Unit = { + var i = from + while (i < to) { + chars += cs(i) + i += 1 + } + } + + @inline override def write(c1: Char, c2: Char): Unit = { + chars += c1 + chars += c2 + } + + @inline override def write(c1: Char, c2: Char, c3: Char): Unit = { + chars += c1 + chars += c2 + chars += c3 + } + + @inline override def write(c1: Char, c2: Char, c3: Char, c4: Char): Unit = { + chars += c1 + chars += c2 + chars += c3 + chars += c4 + } + + @inline override def write(c1: Char, c2: Char, c3: Char, c4: Char, c5: Char): Unit = { + chars += c1 + chars += c2 + chars += c3 + chars += c4 + chars += c5 + } + + @inline override def write(s: Short): Unit = { + chars += (s & 0xff).toChar + chars += (s >> 8).toChar + } + + @inline override def write(s1: Short, s2: Short): Unit = { + chars += (s1 & 0xff).toChar + chars += (s1 >> 8).toChar + chars += (s2 & 0xff).toChar + chars += (s2 >> 8).toChar + } + + @inline override def write(s1: Short, s2: Short, s3: Short): Unit = { + chars += (s1 & 0xff).toChar + chars += (s1 >> 8).toChar + chars += (s2 & 0xff).toChar + chars += (s2 >> 8).toChar + chars += (s3 & 0xff).toChar + chars += (s3 >> 8).toChar + } + + @inline override def write(s1: Short, s2: Short, s3: Short, s4: Short): Unit = { + chars += (s1 & 0xff).toChar + chars += (s1 >> 8).toChar + chars += (s2 & 0xff).toChar + chars += (s2 >> 8).toChar + chars += (s3 & 0xff).toChar + chars += (s3 >> 8).toChar + chars += (s4 & 0xff).toChar + chars += (s4 >> 8).toChar + } + + @inline def buffer: CharSequence = chars +} diff --git a/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala b/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala index 0c9718229..c6545de37 100644 --- a/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala +++ b/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala @@ -72,21 +72,34 @@ object SafeNumbers { try Some(UnsafeNumbers.bigDecimal(num, max_bits)) catch { case _: UnexpectedEnd | UnsafeNumber => None } + def toString(x: Double): String = { + val out = new FastStringWrite(24) + write(x, out) + out.toString + } + + def toString(x: Float): String = { + val out = new FastStringWrite(16) + write(x, out) + out.toString + } + // Based on the amazing work of Raffaello Giulietti // "The Schubfach way to render doubles": https://drive.google.com/file/d/1luHhyQF9zKlM8yJ1nebU0OgVYhfC6CBN/view // Sources with the license are here: https://github.com/c4f7fcce9cb06515/Schubfach/blob/3c92d3c9b1fead540616c918cdfef432bca53dfa/todec/src/math/DoubleToDecimal.java - def toString(x: Double): String = { + def write(x: Double, out: Write): Unit = { val bits = java.lang.Double.doubleToLongBits(x) val ieeeExponent = (bits >> 52).toInt & 0x7ff val ieeeMantissa = bits & 0xfffffffffffffL if (ieeeExponent == 2047) { - if (x != x) """"NaN"""" - else if (bits < 0) """"-Infinity"""" - else """"Infinity"""" + out.write( + if (x != x) """"NaN"""" + else if (bits < 0) """"-Infinity"""" + else """"Infinity"""" + ) } else { - val s = new java.lang.StringBuilder(24) - if (bits < 0) s.append('-') - if (x == 0.0f) s.append('0').append('.').append('0') + if (bits < 0) out.write('-') + if (x == 0.0f) out.write('0', '.', '0') else { var e = ieeeExponent - 1075 var m = ieeeMantissa | 0x10000000000000L @@ -152,40 +165,55 @@ object SafeNumbers { val len = digitCount(dv) exp += len - 1 if (exp < -3 || exp >= 7) { - val dotOff = s.length + 1 - val sdv = stripTrailingZeros(dv) - s.append(sdv) - if (sdv < 10) s.append('0') - s.insert(dotOff, '.').append('E').append(exp) + val sdv = stripTrailingZeros(dv) + if (sdv < 10) out.write((sdv.toInt | '0').toChar, '.', '0', 'E') + else { + val w = writes.get + write(sdv, w) + val cs = w.getChars + out.write(cs(0), '.') + out.write(cs, 1, w.length) + out.write('E') + } + write(exp, out) } else if (exp < 0) { - s.append('0').append('.') + out.write('0', '.') while ({ exp += 1 exp != 0 - }) s.append('0') - s.append(stripTrailingZeros(dv)) - } else if (exp + 1 < len) { - val dotOff = s.length + exp + 1 - s.append(stripTrailingZeros(dv)) - s.insert(dotOff, '.') - } else s.append(dv.toInt).append('.').append('0') + }) out.write('0') + write(stripTrailingZeros(dv), out) + } else { + exp += 1 + if (exp < len) { + val w = writes.get + write(stripTrailingZeros(dv), w) + val cs = w.getChars + out.write(cs, 0, exp) + out.write('.') + out.write(cs, exp, w.length) + } else { + write(dv.toInt, out) + out.write('.', '0') + } + } } - s.toString } } - def toString(x: Float): String = { + def write(x: Float, out: Write): Unit = { val bits = java.lang.Float.floatToIntBits(x) val ieeeExponent = (bits >> 23) & 0xff val ieeeMantissa = bits & 0x7fffff if (ieeeExponent == 255) { - if (x != x) """"NaN"""" - else if (bits < 0) """"-Infinity"""" - else """"Infinity"""" + out.write( + if (x != x) """"NaN"""" + else if (bits < 0) """"-Infinity"""" + else """"Infinity"""" + ) } else { - val s = new java.lang.StringBuilder(16) - if (bits < 0) s.append('-') - if (x == 0.0f) s.append('0').append('.').append('0') + if (bits < 0) out.write('-') + if (x == 0.0f) out.write('0', '.', '0') else { var e = ieeeExponent - 150 var m = ieeeMantissa | 0x800000 @@ -239,30 +267,63 @@ object SafeNumbers { val len = digitCount(dv) exp += len - 1 if (exp < -3 || exp >= 7) { - val dotOff = s.length + 1 - val sdv = stripTrailingZeros(dv) - s.append(sdv) - if (sdv < 10) s.append('0') - s.insert(dotOff, '.').append('E').append(exp) + val sdv = stripTrailingZeros(dv) + if (sdv < 10) out.write((sdv | '0').toChar, '.', '0', 'E') + else { + val w = writes.get + write(sdv, w) + val cs = w.getChars + out.write(cs(0), '.') + out.write(cs, 1, w.length) + out.write('E') + } + write(exp, out) } else if (exp < 0) { - s.append('0').append('.') + out.write('0', '.') while ({ exp += 1 exp != 0 - }) s.append('0') - s.append(stripTrailingZeros(dv)) - } else if (exp + 1 < len) { - val dotOff = s.length + exp + 1 - s.append(stripTrailingZeros(dv)) - s.insert(dotOff, '.') - } else s.append(dv).append('.').append('0') + }) out.write('0') + write(stripTrailingZeros(dv), out) + } else { + exp += 1 + if (exp < len) { + val w = writes.get + write(stripTrailingZeros(dv), w) + val cs = w.getChars + out.write(cs, 0, exp) + out.write('.') + out.write(cs, exp, w.length) + } else { + write(dv, out) + out.write('.', '0') + } + } } - s.toString } } - @inline - private[this] def rop(g1: Long, g0: Long, cp: Long): Long = { + private[json] def writeNano(x: Int, out: Write): Unit = { + out.write('.') + var coeff = 100000000 + while (coeff > x) { + out.write('0') + coeff /= 10 + } + write(stripTrailingZeros(x), out) + } + + private[this] val writes = new ThreadLocal[FastStringWrite] { + override def initialValue(): FastStringWrite = new FastStringWrite(24) + + override def get: FastStringWrite = { + val w = super.get + w.reset() + w + } + } + + @inline private[this] def rop(g1: Long, g0: Long, cp: Long): Long = { val x = multiplyHigh(g0, cp) + (g1 * cp >>> 1) var y = multiplyHigh(g1, cp) if (x < 0) y += 1 @@ -270,14 +331,12 @@ object SafeNumbers { y } - @inline - private[this] def rop(g: Long, cp: Int): Int = { + @inline private[this] def rop(g: Long, cp: Int): Int = { val x = ((g & 0xffffffffL) * cp >>> 32) + (g >>> 32) * cp (x >>> 31).toInt | -x.toInt >>> 31 } - @inline - private[this] def multiplyHigh(x: Long, y: Long): Long = { + @inline private[this] def multiplyHigh(x: Long, y: Long): Long = { val x2 = x & 0xffffffffL val y2 = y & 0xffffffffL val b = x2 * y2 @@ -287,8 +346,7 @@ object SafeNumbers { (((b >>> 32) + (x1 + x2) * (y1 + y2) - b - a) >>> 32) + a } - @inline - private[this] def stripTrailingZeros(x: Long): Long = { + @inline private[this] def stripTrailingZeros(x: Long): Long = { var q0 = x.toInt if ( q0 == x || { @@ -319,7 +377,7 @@ object SafeNumbers { y } - private[this] def stripTrailingZeros(x: Int): Int = { + @inline private[this] def stripTrailingZeros(x: Int): Int = { var q0 = x var q1 = 0 while ({ @@ -331,6 +389,132 @@ object SafeNumbers { q0 } + @inline def write(a: Long, out: Write): Unit = { + var q0 = a + if (q0 < 0) { + q0 = -q0 + out.write('-') + if (q0 == a) { + out.write('9', '2', '2') + q0 = 3372036854775808L + } + } + var q = q0.toInt + if (q0 == q) write(q, out) + else { + var last: Char = 0 + if (q0 >= 1000000000000000000L) { + var z = q0 + q0 = (q0 >>> 1) + (q0 >>> 2) // Based upon the divu10() code from Hacker's Delight 2nd Edition by Henry Warren + q0 += q0 >>> 4 + q0 += q0 >>> 8 + q0 += q0 >>> 16 + q0 += q0 >>> 32 + z -= q0 & 0xfffffffffffffff8L + q0 >>>= 3 + var r = (z - (q0 << 1)).toInt + if (r >= 10) { + q0 += 1L + r -= 10 + } + last = (r | '0').toChar + } + val q1 = ((q0 >>> 8) * 2.56e-6).toLong // divide a medium positive long by 100000000 + q = q1.toInt + if (q1 == q) write(q, out) + else { + q = ((q1 >>> 8) * 1441151881L >>> 49).toInt // divide a small positive long by 100000000 + write(q, out) + write8Digits((q1 - q * 100000000L).toInt, out) + } + write8Digits((q0 - q1 * 100000000L).toInt, out) + if (last != 0) out.write(last) + } + } + + @inline def write(a: Int, out: Write): Unit = { + val ds = digits + var q0 = a + if (q0 < 0) { + q0 = -q0 + out.write('-') + if (q0 == a) { + out.write('2') + q0 = 147483648 + } + } + if (q0 < 100) { + if (q0 < 10) out.write((q0 | '0').toChar) + else out.write(ds(q0)) + } else if (q0 < 10000) { + val q1 = q0 * 5243 >> 19 // divide a small positive int by 100 + val d2 = ds(q0 - q1 * 100) + if (q0 < 1000) out.write((q1 | '0').toChar) + else out.write(ds(q1)) + out.write(d2) + } else if (q0 < 1000000) { + val q1 = q0 / 100 + val r1 = q0 - q1 * 100 + val q2 = q1 * 5243 >> 19 // divide a small positive int by 100 + val r2 = q1 - q2 * 100 + if (q0 < 100000) out.write((q2 | '0').toChar) + else out.write(ds(q2)) + out.write(ds(r2), ds(r1)) + } else if (q0 < 100000000) { + if (q0 < 10000000) { + val q1 = q0 / 100 + val r1 = q0 - q1 * 100 + val q2 = q1 / 100 + val r2 = q1 - q2 * 100 + val q3 = q2 * 5243 >> 19 // divide a small positive int by 100 + val r3 = q2 - q3 * 100 + out.write((q3 | '0').toChar) + out.write(ds(r3), ds(r2), ds(r1)) + } else write8Digits(q0, out) + } else { + val q1 = q0 / 100000000 + val r1 = q0 - q1 * 100000000 + if (q0 < 1000000000) out.write((q1 | '0').toChar) + else out.write(ds(q1)) + write8Digits(r1, out) + } + } + + @inline private[this] def write8Digits(x: Int, out: Write): Unit = { + val ds = digits + val q1 = x / 10000 + val q2 = q1 * 5243 >> 19 // divide a small positive int by 100 + out.write(ds(q2), ds(q1 - q2 * 100)) + val r1 = x - q1 * 10000 + val q3 = r1 * 5243 >> 19 // divide a small positive int by 100 + out.write(ds(q3), ds(r1 - q3 * 100)) + } + + @inline private[json] def write4Digits(x: Int, out: Write): Unit = { + val ds = digits + val q = x * 5243 >> 19 // divide a 4-digit positive int by 100 + out.write(ds(q), ds(x - q * 100)) + } + + @inline private[json] def write3Digits(x: Int, out: Write): Unit = { + val q = x * 1311 >> 17 // divide a 3-digit positive int by 100 + out.write((q + '0').toChar) + out.write(digits(x - q * 100)) + } + + @inline private[json] def write2Digits(x: Int, out: Write): Unit = + out.write(digits(x)) + + private[this] final val digits: Array[Short] = Array( + 12336, 12592, 12848, 13104, 13360, 13616, 13872, 14128, 14384, 14640, 12337, 12593, 12849, 13105, 13361, 13617, + 13873, 14129, 14385, 14641, 12338, 12594, 12850, 13106, 13362, 13618, 13874, 14130, 14386, 14642, 12339, 12595, + 12851, 13107, 13363, 13619, 13875, 14131, 14387, 14643, 12340, 12596, 12852, 13108, 13364, 13620, 13876, 14132, + 14388, 14644, 12341, 12597, 12853, 13109, 13365, 13621, 13877, 14133, 14389, 14645, 12342, 12598, 12854, 13110, + 13366, 13622, 13878, 14134, 14390, 14646, 12343, 12599, 12855, 13111, 13367, 13623, 13879, 14135, 14391, 14647, + 12344, 12600, 12856, 13112, 13368, 13624, 13880, 14136, 14392, 14648, 12345, 12601, 12857, 13113, 13369, 13625, + 13881, 14137, 14393, 14649 + ) + @inline private[this] def digitCount(x: Long): Int = if (x >= 1000000000000000L) { diff --git a/zio-json/jvm/src/main/scala/zio/json/internal/FastStringWrite.scala b/zio-json/jvm/src/main/scala/zio/json/internal/FastStringWrite.scala new file mode 100644 index 000000000..107d894d6 --- /dev/null +++ b/zio-json/jvm/src/main/scala/zio/json/internal/FastStringWrite.scala @@ -0,0 +1,171 @@ +package zio.json.internal + +import java.nio.CharBuffer +import java.util.Arrays + +final class FastStringWrite(initial: Int) extends Write { + require(initial >= 8) + private[this] var chars: Array[Char] = new Array[Char](initial) + private[this] var count: Int = 0 + + @inline def reset(): Unit = count = 0 + + @inline private[internal] def length: Int = count + + @inline private[internal] def getChars: Array[Char] = chars + + def write(s: String): Unit = { + val l = s.length + var cs = chars + val i = count + if (i + l >= cs.length) { + cs = Arrays.copyOf(cs, Math.max(cs.length << 1, i + l)) + chars = cs + } + s.getChars(0, l, cs, i) + count = i + l + } + + def write(c: Char): Unit = { + var cs = chars + val i = count + if (i + 1 >= cs.length) { + cs = Arrays.copyOf(cs, cs.length << 1) + chars = cs + } + cs(i) = c + count = i + 1 + } + + override def write(cs: Array[Char], from: Int, to: Int): Unit = { + var cs_ = chars + val from_ = count + val len = to - from + if (from_ + len >= cs_.length) { + cs_ = Arrays.copyOf(cs_, Math.max(cs_.length << 1, from_ + len)) + chars = cs_ + } + var i = 0 + while (i < len) { + cs_(from_ + i) = cs(from + i) + i += 1 + } + count = from_ + len + } + + override def write(c1: Char, c2: Char): Unit = { + var cs = chars + val i = count + if (i + 1 >= cs.length) { + cs = Arrays.copyOf(cs, cs.length << 1) + chars = cs + } + cs(i) = c1 + cs(i + 1) = c2 + count = i + 2 + } + + override def write(c1: Char, c2: Char, c3: Char): Unit = { + var cs = chars + val i = count + if (i + 2 >= cs.length) { + cs = Arrays.copyOf(cs, cs.length << 1) + chars = cs + } + cs(i) = c1 + cs(i + 1) = c2 + cs(i + 2) = c3 + count = i + 3 + } + + override def write(c1: Char, c2: Char, c3: Char, c4: Char): Unit = { + var cs = chars + val i = count + if (i + 3 >= cs.length) { + cs = Arrays.copyOf(cs, cs.length << 1) + chars = cs + } + cs(i) = c1 + cs(i + 1) = c2 + cs(i + 2) = c3 + cs(i + 3) = c4 + count = i + 4 + } + + override def write(c1: Char, c2: Char, c3: Char, c4: Char, c5: Char): Unit = { + var cs = chars + val i = count + if (i + 4 >= cs.length) { + cs = Arrays.copyOf(cs, cs.length << 1) + chars = cs + } + cs(i) = c1 + cs(i + 1) = c2 + cs(i + 2) = c3 + cs(i + 3) = c4 + cs(i + 4) = c5 + count = i + 5 + } + + override def write(s: Short): Unit = { + var cs = chars + val i = count + if (i + 1 >= cs.length) { + cs = Arrays.copyOf(cs, cs.length << 1) + chars = cs + } + cs(i) = (s & 0xff).toChar + cs(i + 1) = (s >> 8).toChar + count = i + 2 + } + + override def write(s1: Short, s2: Short): Unit = { + var cs = chars + val i = count + if (i + 3 >= cs.length) { + cs = Arrays.copyOf(cs, cs.length << 1) + chars = cs + } + cs(i) = (s1 & 0xff).toChar + cs(i + 1) = (s1 >> 8).toChar + cs(i + 2) = (s2 & 0xff).toChar + cs(i + 3) = (s2 >> 8).toChar + count = i + 4 + } + + override def write(s1: Short, s2: Short, s3: Short): Unit = { + var cs = chars + val i = count + if (i + 5 >= cs.length) { + cs = Arrays.copyOf(cs, cs.length << 1) + chars = cs + } + cs(i) = (s1 & 0xff).toChar + cs(i + 1) = (s1 >> 8).toChar + cs(i + 2) = (s2 & 0xff).toChar + cs(i + 3) = (s2 >> 8).toChar + cs(i + 4) = (s3 & 0xff).toChar + cs(i + 5) = (s3 >> 8).toChar + count = i + 6 + } + + override def write(s1: Short, s2: Short, s3: Short, s4: Short): Unit = { + var cs = chars + val i = count + if (i + 7 >= cs.length) { + cs = Arrays.copyOf(cs, cs.length << 1) + chars = cs + } + cs(i) = (s1 & 0xff).toChar + cs(i + 1) = (s1 >> 8).toChar + cs(i + 2) = (s2 & 0xff).toChar + cs(i + 3) = (s2 >> 8).toChar + cs(i + 4) = (s3 & 0xff).toChar + cs(i + 5) = (s3 >> 8).toChar + cs(i + 6) = (s4 & 0xff).toChar + cs(i + 7) = (s4 >> 8).toChar + count = i + 8 + } + + def buffer: CharSequence = CharBuffer.wrap(chars, 0, count) +} diff --git a/zio-json/jvm/src/main/scala/zio/json/internal/SafeNumbers.scala b/zio-json/jvm/src/main/scala/zio/json/internal/SafeNumbers.scala index 4a6489a39..67eec114c 100644 --- a/zio-json/jvm/src/main/scala/zio/json/internal/SafeNumbers.scala +++ b/zio-json/jvm/src/main/scala/zio/json/internal/SafeNumbers.scala @@ -72,21 +72,34 @@ object SafeNumbers { try Some(UnsafeNumbers.bigDecimal(num, max_bits)) catch { case _: UnexpectedEnd | UnsafeNumber => None } + def toString(x: Double): String = { + val out = new FastStringWrite(24) + write(x, out) + out.toString + } + + def toString(x: Float): String = { + val out = new FastStringWrite(16) + write(x, out) + out.toString + } + // Based on the amazing work of Raffaello Giulietti // "The Schubfach way to render doubles": https://drive.google.com/file/d/1luHhyQF9zKlM8yJ1nebU0OgVYhfC6CBN/view // Sources with the license are here: https://github.com/c4f7fcce9cb06515/Schubfach/blob/3c92d3c9b1fead540616c918cdfef432bca53dfa/todec/src/math/DoubleToDecimal.java - def toString(x: Double): String = { + def write(x: Double, out: Write): Unit = { val bits = java.lang.Double.doubleToLongBits(x) val ieeeExponent = (bits >> 52).toInt & 0x7ff val ieeeMantissa = bits & 0xfffffffffffffL if (ieeeExponent == 2047) { - if (x != x) """"NaN"""" - else if (bits < 0) """"-Infinity"""" - else """"Infinity"""" + out.write( + if (x != x) """"NaN"""" + else if (bits < 0) """"-Infinity"""" + else """"Infinity"""" + ) } else { - val s = new java.lang.StringBuilder(24) - if (bits < 0) s.append('-') - if (x == 0.0f) s.append('0').append('.').append('0') + if (bits < 0) out.write('-') + if (x == 0.0f) out.write('0', '.', '0') else { var e = ieeeExponent - 1075 var m = ieeeMantissa | 0x10000000000000L @@ -143,40 +156,55 @@ object SafeNumbers { val len = digitCount(dv) exp += len - 1 if (exp < -3 || exp >= 7) { - val dotOff = s.length + 1 - val sdv = stripTrailingZeros(dv) - s.append(sdv) - if (sdv < 10) s.append('0') - s.insert(dotOff, '.').append('E').append(exp) + val sdv = stripTrailingZeros(dv) + if (sdv < 10) out.write((sdv.toInt | '0').toChar, '.', '0', 'E') + else { + val w = writes.get + write(sdv, w) + val cs = w.getChars + out.write(cs(0), '.') + out.write(cs, 1, w.length) + out.write('E') + } + write(exp, out) } else if (exp < 0) { - s.append('0').append('.') + out.write('0', '.') while ({ exp += 1 exp != 0 - }) s.append('0') - s.append(stripTrailingZeros(dv)) - } else if (exp + 1 < len) { - val dotOff = s.length + exp + 1 - s.append(stripTrailingZeros(dv)) - s.insert(dotOff, '.') - } else s.append(dv.toInt).append('.').append('0') + }) out.write('0') + write(stripTrailingZeros(dv), out) + } else { + exp += 1 + if (exp < len) { + val w = writes.get + write(stripTrailingZeros(dv), w) + val cs = w.getChars + out.write(cs, 0, exp) + out.write('.') + out.write(cs, exp, w.length) + } else { + write(dv.toInt, out) + out.write('.', '0') + } + } } - s.toString } } - def toString(x: Float): String = { + def write(x: Float, out: Write): Unit = { val bits = java.lang.Float.floatToIntBits(x) val ieeeExponent = (bits >> 23) & 0xff val ieeeMantissa = bits & 0x7fffff if (ieeeExponent == 255) { - if (x != x) """"NaN"""" - else if (bits < 0) """"-Infinity"""" - else """"Infinity"""" + out.write( + if (x != x) """"NaN"""" + else if (bits < 0) """"-Infinity"""" + else """"Infinity"""" + ) } else { - val s = new java.lang.StringBuilder(16) - if (bits < 0) s.append('-') - if (x == 0.0f) s.append('0').append('.').append('0') + if (bits < 0) out.write('-') + if (x == 0.0f) out.write('0', '.', '0') else { var e = ieeeExponent - 150 var m = ieeeMantissa | 0x800000 @@ -230,25 +258,59 @@ object SafeNumbers { val len = digitCount(dv.toLong) exp += len - 1 if (exp < -3 || exp >= 7) { - val dotOff = s.length + 1 - val sdv = stripTrailingZeros(dv) - s.append(sdv) - if (sdv < 10) s.append('0') - s.insert(dotOff, '.').append('E').append(exp) + val sdv = stripTrailingZeros(dv) + if (sdv < 10) out.write((sdv | '0').toChar, '.', '0', 'E') + else { + val w = writes.get + write(sdv, w) + val cs = w.getChars + out.write(cs(0), '.') + out.write(cs, 1, w.length) + out.write('E') + } + write(exp, out) } else if (exp < 0) { - s.append('0').append('.') + out.write('0', '.') while ({ exp += 1 exp != 0 - }) s.append('0') - s.append(stripTrailingZeros(dv)) - } else if (exp + 1 < len) { - val dotOff = s.length + exp + 1 - s.append(stripTrailingZeros(dv)) - s.insert(dotOff, '.') - } else s.append(dv).append('.').append('0') + }) out.write('0') + write(stripTrailingZeros(dv), out) + } else { + exp += 1 + if (exp < len) { + val w = writes.get + write(stripTrailingZeros(dv), w) + val cs = w.getChars + out.write(cs, 0, exp) + out.write('.') + out.write(cs, exp, w.length) + } else { + write(dv, out) + out.write('.', '0') + } + } } - s.toString + } + } + + private[json] def writeNano(x: Int, out: Write): Unit = { + out.write('.') + var coeff = 100000000 + while (coeff > x) { + out.write('0') + coeff = (coeff * 3435973837L >> 35).toInt // divide a positive int by 10 + } + write(stripTrailingZeros(x), out) + } + + private[this] val writes = new ThreadLocal[FastStringWrite] { + override def initialValue(): FastStringWrite = new FastStringWrite(24) + + override def get: FastStringWrite = { + val w = super.get + w.reset() + w } } @@ -292,6 +354,114 @@ object SafeNumbers { q0 } + def write(a: Long, out: Write): Unit = { + var q0 = a + if (q0 < 0) { + q0 = -q0 + out.write('-') + if (q0 == a) { + out.write('9', '2', '2') + q0 = 3372036854775808L + } + } + val m1 = 100000000L + if (q0 < m1) write(q0.toInt, out) + else { + val m2 = 6189700196426901375L + val q1 = Math.multiplyHigh(q0, m2) >>> 25 // divide a positive long by 100000000 + if (q1 < m1) write(q1.toInt, out) + else { + val q2 = Math.multiplyHigh(q1, m2) >>> 25 // divide a small positive long by 100000000 + write(q2.toInt, out) + write8Digits((q1 - q2 * m1).toInt, out) + } + write8Digits((q0 - q1 * m1).toInt, out) + } + } + + def write(a: Int, out: Write): Unit = { + val ds = digits + var q0 = a + if (q0 < 0) { + q0 = -q0 + out.write('-') + if (q0 == a) { + out.write('2') + q0 = 147483648 + } + } + if (q0 < 100) { // Based on James Anhalt's algorithm: https://jk-jeon.github.io/posts/2022/02/jeaiii-algorithm/ + if (q0 < 10) out.write((q0 | '0').toChar) + else out.write(ds(q0)) + } else if (q0 < 10000) { + val q1 = q0 * 5243 >> 19 // divide a small positive int by 100 + val d2 = ds(q0 - q1 * 100) + if (q0 < 1000) out.write((q1 | '0').toChar) + else out.write(ds(q1)) + out.write(d2) + } else if (q0 < 1000000) { + val y1 = q0 * 429497L + val y2 = (y1 & 0xffffffffL) * 100 + val y3 = (y2 & 0xffffffffL) * 100 + if (q0 < 100000) out.write(((y1 >> 32).toInt | '0').toChar) + else out.write(ds((y1 >> 32).toInt)) + out.write(ds((y2 >> 32).toInt), ds((y3 >> 32).toInt)) + } else if (q0 < 100000000) { + val y1 = q0 * 140737489L + val y2 = (y1 & 0x7fffffffffffL) * 100 + val y3 = (y2 & 0x7fffffffffffL) * 100 + val y4 = (y3 & 0x7fffffffffffL) * 100 + if (q0 < 10000000) out.write(((y1 >> 47).toInt | '0').toChar) + else out.write(ds((y1 >> 47).toInt)) + out.write(ds((y2 >> 47).toInt), ds((y3 >> 47).toInt), ds((y4 >> 47).toInt)) + } else { + val y1 = q0 * 1441151881L + val y2 = (y1 & 0x1ffffffffffffffL) * 100 + val y3 = (y2 & 0x1ffffffffffffffL) * 100 + val y4 = (y3 & 0x1ffffffffffffffL) * 100 + val y5 = (y4 & 0x1ffffffffffffffL) * 100 + if (q0 < 1000000000) out.write(((y1 >>> 57).toInt | '0').toChar) + else out.write(ds((y1 >>> 57).toInt)) + out.write(ds((y2 >>> 57).toInt), ds((y3 >>> 57).toInt), ds((y4 >>> 57).toInt), ds((y5 >>> 57).toInt)) + } + } + + private[this] def write8Digits(x: Int, out: Write): Unit = { + val ds = digits // Based on James Anhalt's algorithm: https://jk-jeon.github.io/posts/2022/02/jeaiii-algorithm/ + val y1 = x * 140737489L + val m1 = 0x7fffffffffffL + val m2 = 100L + val y2 = (y1 & m1) * m2 + val y3 = (y2 & m1) * m2 + val y4 = (y3 & m1) * m2 + out.write(ds((y1 >> 47).toInt), ds((y2 >> 47).toInt), ds((y3 >> 47).toInt), ds((y4 >> 47).toInt)) + } + + @inline private[json] def write4Digits(x: Int, out: Write): Unit = { + val ds = digits + val q = x * 5243 >> 19 // divide a 4-digit positive int by 100 + out.write(ds(q), ds(x - q * 100)) + } + + @inline private[json] def write3Digits(x: Int, out: Write): Unit = { + val q = x * 1311 >> 17 // divide a 3-digit positive int by 100 + out.write((q + '0').toChar) + out.write(digits(x - q * 100)) + } + + @inline private[json] def write2Digits(x: Int, out: Write): Unit = + out.write(digits(x)) + + private[this] final val digits: Array[Short] = Array( + 12336, 12592, 12848, 13104, 13360, 13616, 13872, 14128, 14384, 14640, 12337, 12593, 12849, 13105, 13361, 13617, + 13873, 14129, 14385, 14641, 12338, 12594, 12850, 13106, 13362, 13618, 13874, 14130, 14386, 14642, 12339, 12595, + 12851, 13107, 13363, 13619, 13875, 14131, 14387, 14643, 12340, 12596, 12852, 13108, 13364, 13620, 13876, 14132, + 14388, 14644, 12341, 12597, 12853, 13109, 13365, 13621, 13877, 14133, 14389, 14645, 12342, 12598, 12854, 13110, + 13366, 13622, 13878, 14134, 14390, 14646, 12343, 12599, 12855, 13111, 13367, 13623, 13879, 14135, 14391, 14647, + 12344, 12600, 12856, 13112, 13368, 13624, 13880, 14136, 14392, 14648, 12345, 12601, 12857, 13113, 13369, 13625, + 13881, 14137, 14393, 14649 + ) + // Adoption of a nice trick form Daniel Lemire's blog that works for numbers up to 10^18: // https://lemire.me/blog/2021/06/03/computing-the-number-of-digits-of-an-integer-even-faster/ private[this] def digitCount(x: Long): Int = (offsets(java.lang.Long.numberOfLeadingZeros(x)) + x >> 58).toInt diff --git a/zio-json/native/src/main/scala/zio/json/internal/FastStringWrite.scala b/zio-json/native/src/main/scala/zio/json/internal/FastStringWrite.scala new file mode 100644 index 000000000..107d894d6 --- /dev/null +++ b/zio-json/native/src/main/scala/zio/json/internal/FastStringWrite.scala @@ -0,0 +1,171 @@ +package zio.json.internal + +import java.nio.CharBuffer +import java.util.Arrays + +final class FastStringWrite(initial: Int) extends Write { + require(initial >= 8) + private[this] var chars: Array[Char] = new Array[Char](initial) + private[this] var count: Int = 0 + + @inline def reset(): Unit = count = 0 + + @inline private[internal] def length: Int = count + + @inline private[internal] def getChars: Array[Char] = chars + + def write(s: String): Unit = { + val l = s.length + var cs = chars + val i = count + if (i + l >= cs.length) { + cs = Arrays.copyOf(cs, Math.max(cs.length << 1, i + l)) + chars = cs + } + s.getChars(0, l, cs, i) + count = i + l + } + + def write(c: Char): Unit = { + var cs = chars + val i = count + if (i + 1 >= cs.length) { + cs = Arrays.copyOf(cs, cs.length << 1) + chars = cs + } + cs(i) = c + count = i + 1 + } + + override def write(cs: Array[Char], from: Int, to: Int): Unit = { + var cs_ = chars + val from_ = count + val len = to - from + if (from_ + len >= cs_.length) { + cs_ = Arrays.copyOf(cs_, Math.max(cs_.length << 1, from_ + len)) + chars = cs_ + } + var i = 0 + while (i < len) { + cs_(from_ + i) = cs(from + i) + i += 1 + } + count = from_ + len + } + + override def write(c1: Char, c2: Char): Unit = { + var cs = chars + val i = count + if (i + 1 >= cs.length) { + cs = Arrays.copyOf(cs, cs.length << 1) + chars = cs + } + cs(i) = c1 + cs(i + 1) = c2 + count = i + 2 + } + + override def write(c1: Char, c2: Char, c3: Char): Unit = { + var cs = chars + val i = count + if (i + 2 >= cs.length) { + cs = Arrays.copyOf(cs, cs.length << 1) + chars = cs + } + cs(i) = c1 + cs(i + 1) = c2 + cs(i + 2) = c3 + count = i + 3 + } + + override def write(c1: Char, c2: Char, c3: Char, c4: Char): Unit = { + var cs = chars + val i = count + if (i + 3 >= cs.length) { + cs = Arrays.copyOf(cs, cs.length << 1) + chars = cs + } + cs(i) = c1 + cs(i + 1) = c2 + cs(i + 2) = c3 + cs(i + 3) = c4 + count = i + 4 + } + + override def write(c1: Char, c2: Char, c3: Char, c4: Char, c5: Char): Unit = { + var cs = chars + val i = count + if (i + 4 >= cs.length) { + cs = Arrays.copyOf(cs, cs.length << 1) + chars = cs + } + cs(i) = c1 + cs(i + 1) = c2 + cs(i + 2) = c3 + cs(i + 3) = c4 + cs(i + 4) = c5 + count = i + 5 + } + + override def write(s: Short): Unit = { + var cs = chars + val i = count + if (i + 1 >= cs.length) { + cs = Arrays.copyOf(cs, cs.length << 1) + chars = cs + } + cs(i) = (s & 0xff).toChar + cs(i + 1) = (s >> 8).toChar + count = i + 2 + } + + override def write(s1: Short, s2: Short): Unit = { + var cs = chars + val i = count + if (i + 3 >= cs.length) { + cs = Arrays.copyOf(cs, cs.length << 1) + chars = cs + } + cs(i) = (s1 & 0xff).toChar + cs(i + 1) = (s1 >> 8).toChar + cs(i + 2) = (s2 & 0xff).toChar + cs(i + 3) = (s2 >> 8).toChar + count = i + 4 + } + + override def write(s1: Short, s2: Short, s3: Short): Unit = { + var cs = chars + val i = count + if (i + 5 >= cs.length) { + cs = Arrays.copyOf(cs, cs.length << 1) + chars = cs + } + cs(i) = (s1 & 0xff).toChar + cs(i + 1) = (s1 >> 8).toChar + cs(i + 2) = (s2 & 0xff).toChar + cs(i + 3) = (s2 >> 8).toChar + cs(i + 4) = (s3 & 0xff).toChar + cs(i + 5) = (s3 >> 8).toChar + count = i + 6 + } + + override def write(s1: Short, s2: Short, s3: Short, s4: Short): Unit = { + var cs = chars + val i = count + if (i + 7 >= cs.length) { + cs = Arrays.copyOf(cs, cs.length << 1) + chars = cs + } + cs(i) = (s1 & 0xff).toChar + cs(i + 1) = (s1 >> 8).toChar + cs(i + 2) = (s2 & 0xff).toChar + cs(i + 3) = (s2 >> 8).toChar + cs(i + 4) = (s3 & 0xff).toChar + cs(i + 5) = (s3 >> 8).toChar + cs(i + 6) = (s4 & 0xff).toChar + cs(i + 7) = (s4 >> 8).toChar + count = i + 8 + } + + def buffer: CharSequence = CharBuffer.wrap(chars, 0, count) +} diff --git a/zio-json/native/src/main/scala/zio/json/internal/SafeNumbers.scala b/zio-json/native/src/main/scala/zio/json/internal/SafeNumbers.scala index fcecf03a4..6ca0c36b7 100644 --- a/zio-json/native/src/main/scala/zio/json/internal/SafeNumbers.scala +++ b/zio-json/native/src/main/scala/zio/json/internal/SafeNumbers.scala @@ -72,21 +72,34 @@ object SafeNumbers { try Some(UnsafeNumbers.bigDecimal(num, max_bits)) catch { case _: UnexpectedEnd | UnsafeNumber => None } + def toString(x: Double): String = { + val out = new FastStringWrite(24) + write(x, out) + out.toString + } + + def toString(x: Float): String = { + val out = new FastStringWrite(16) + write(x, out) + out.toString + } + // Based on the amazing work of Raffaello Giulietti // "The Schubfach way to render doubles": https://drive.google.com/file/d/1luHhyQF9zKlM8yJ1nebU0OgVYhfC6CBN/view // Sources with the license are here: https://github.com/c4f7fcce9cb06515/Schubfach/blob/3c92d3c9b1fead540616c918cdfef432bca53dfa/todec/src/math/DoubleToDecimal.java - def toString(x: Double): String = { + def write(x: Double, out: Write): Unit = { val bits = java.lang.Double.doubleToLongBits(x) val ieeeExponent = (bits >> 52).toInt & 0x7ff val ieeeMantissa = bits & 0xfffffffffffffL if (ieeeExponent == 2047) { - if (x != x) """"NaN"""" - else if (bits < 0) """"-Infinity"""" - else """"Infinity"""" + out.write( + if (x != x) """"NaN"""" + else if (bits < 0) """"-Infinity"""" + else """"Infinity"""" + ) } else { - val s = new java.lang.StringBuilder(24) - if (bits < 0) s.append('-') - if (x == 0.0f) s.append('0').append('.').append('0') + if (bits < 0) out.write('-') + if (x == 0.0f) out.write('0', '.', '0') else { var e = ieeeExponent - 1075 var m = ieeeMantissa | 0x10000000000000L @@ -143,40 +156,55 @@ object SafeNumbers { val len = digitCount(dv) exp += len - 1 if (exp < -3 || exp >= 7) { - val dotOff = s.length + 1 - val sdv = stripTrailingZeros(dv) - s.append(sdv) - if (sdv < 10) s.append('0') - s.insert(dotOff, '.').append('E').append(exp) + val sdv = stripTrailingZeros(dv) + if (sdv < 10) out.write((sdv.toInt | '0').toChar, '.', '0', 'E') + else { + val w = writes.get + write(sdv, w) + val cs = w.getChars + out.write(cs(0), '.') + out.write(cs, 1, w.length) + out.write('E') + } + write(exp, out) } else if (exp < 0) { - s.append('0').append('.') + out.write('0', '.') while ({ exp += 1 exp != 0 - }) s.append('0') - s.append(stripTrailingZeros(dv)) - } else if (exp + 1 < len) { - val dotOff = s.length + exp + 1 - s.append(stripTrailingZeros(dv)) - s.insert(dotOff, '.') - } else s.append(dv.toInt).append('.').append('0') + }) out.write('0') + write(stripTrailingZeros(dv), out) + } else { + exp += 1 + if (exp < len) { + val w = writes.get + write(stripTrailingZeros(dv), w) + val cs = w.getChars + out.write(cs, 0, exp) + out.write('.') + out.write(cs, exp, w.length) + } else { + write(dv.toInt, out) + out.write('.', '0') + } + } } - s.toString } } - def toString(x: Float): String = { + def write(x: Float, out: Write): Unit = { val bits = java.lang.Float.floatToIntBits(x) val ieeeExponent = (bits >> 23) & 0xff val ieeeMantissa = bits & 0x7fffff if (ieeeExponent == 255) { - if (x != x) """"NaN"""" - else if (bits < 0) """"-Infinity"""" - else """"Infinity"""" + out.write( + if (x != x) """"NaN"""" + else if (bits < 0) """"-Infinity"""" + else """"Infinity"""" + ) } else { - val s = new java.lang.StringBuilder(16) - if (bits < 0) s.append('-') - if (x == 0.0f) s.append('0').append('.').append('0') + if (bits < 0) out.write('-') + if (x == 0.0f) out.write('0', '.', '0') else { var e = ieeeExponent - 150 var m = ieeeMantissa | 0x800000 @@ -230,25 +258,59 @@ object SafeNumbers { val len = digitCount(dv.toLong) exp += len - 1 if (exp < -3 || exp >= 7) { - val dotOff = s.length + 1 - val sdv = stripTrailingZeros(dv) - s.append(sdv) - if (sdv < 10) s.append('0') - s.insert(dotOff, '.').append('E').append(exp) + val sdv = stripTrailingZeros(dv) + if (sdv < 10) out.write((sdv | '0').toChar, '.', '0', 'E') + else { + val w = writes.get + write(sdv, w) + val cs = w.getChars + out.write(cs(0), '.') + out.write(cs, 1, w.length) + out.write('E') + } + write(exp, out) } else if (exp < 0) { - s.append('0').append('.') + out.write('0', '.') while ({ exp += 1 exp != 0 - }) s.append('0') - s.append(stripTrailingZeros(dv)) - } else if (exp + 1 < len) { - val dotOff = s.length + exp + 1 - s.append(stripTrailingZeros(dv)) - s.insert(dotOff, '.') - } else s.append(dv).append('.').append('0') + }) out.write('0') + write(stripTrailingZeros(dv), out) + } else { + exp += 1 + if (exp < len) { + val w = writes.get + write(stripTrailingZeros(dv), w) + val cs = w.getChars + out.write(cs, 0, exp) + out.write('.') + out.write(cs, exp, w.length) + } else { + write(dv, out) + out.write('.', '0') + } + } } - s.toString + } + } + + private[json] def writeNano(x: Int, out: Write): Unit = { + out.write('.') + var coeff = 100000000 + while (coeff > x) { + out.write('0') + coeff = (coeff * 3435973837L >> 35).toInt // divide a positive int by 10 + } + write(stripTrailingZeros(x), out) + } + + private[this] val writes = new ThreadLocal[FastStringWrite] { + override def initialValue(): FastStringWrite = new FastStringWrite(24) + + override def get: FastStringWrite = { + val w = super.get + w.reset() + w } } @@ -292,6 +354,114 @@ object SafeNumbers { q0 } + def write(a: Long, out: Write): Unit = { + var q0 = a + if (q0 < 0) { + q0 = -q0 + out.write('-') + if (q0 == a) { + out.write('9', '2', '2') + q0 = 3372036854775808L + } + } + val m1 = 100000000L + if (q0 < m1) write(q0.toInt, out) + else { + val m2 = 6189700196426901375L + val q1 = NativeMath.multiplyHigh(q0, m2) >>> 25 // divide a positive long by 100000000 + if (q1 < m1) write(q1.toInt, out) + else { + val q2 = NativeMath.multiplyHigh(q1, m2) >>> 25 // divide a small positive long by 100000000 + write(q2.toInt, out) + write8Digits((q1 - q2 * m1).toInt, out) + } + write8Digits((q0 - q1 * m1).toInt, out) + } + } + + def write(a: Int, out: Write): Unit = { + val ds = digits + var q0 = a + if (q0 < 0) { + q0 = -q0 + out.write('-') + if (q0 == a) { + out.write('2') + q0 = 147483648 + } + } + if (q0 < 100) { // Based on James Anhalt's algorithm: https://jk-jeon.github.io/posts/2022/02/jeaiii-algorithm/ + if (q0 < 10) out.write((q0 | '0').toChar) + else out.write(ds(q0)) + } else if (q0 < 10000) { + val q1 = q0 * 5243 >> 19 // divide a small positive int by 100 + val d2 = ds(q0 - q1 * 100) + if (q0 < 1000) out.write((q1 | '0').toChar) + else out.write(ds(q1)) + out.write(d2) + } else if (q0 < 1000000) { + val y1 = q0 * 429497L + val y2 = (y1 & 0xffffffffL) * 100 + val y3 = (y2 & 0xffffffffL) * 100 + if (q0 < 100000) out.write(((y1 >> 32).toInt | '0').toChar) + else out.write(ds((y1 >> 32).toInt)) + out.write(ds((y2 >> 32).toInt), ds((y3 >> 32).toInt)) + } else if (q0 < 100000000) { + val y1 = q0 * 140737489L + val y2 = (y1 & 0x7fffffffffffL) * 100 + val y3 = (y2 & 0x7fffffffffffL) * 100 + val y4 = (y3 & 0x7fffffffffffL) * 100 + if (q0 < 10000000) out.write(((y1 >> 47).toInt | '0').toChar) + else out.write(ds((y1 >> 47).toInt)) + out.write(ds((y2 >> 47).toInt), ds((y3 >> 47).toInt), ds((y4 >> 47).toInt)) + } else { + val y1 = q0 * 1441151881L + val y2 = (y1 & 0x1ffffffffffffffL) * 100 + val y3 = (y2 & 0x1ffffffffffffffL) * 100 + val y4 = (y3 & 0x1ffffffffffffffL) * 100 + val y5 = (y4 & 0x1ffffffffffffffL) * 100 + if (q0 < 1000000000) out.write(((y1 >>> 57).toInt | '0').toChar) + else out.write(ds((y1 >>> 57).toInt)) + out.write(ds((y2 >>> 57).toInt), ds((y3 >>> 57).toInt), ds((y4 >>> 57).toInt), ds((y5 >>> 57).toInt)) + } + } + + private[this] def write8Digits(x: Int, out: Write): Unit = { + val ds = digits // Based on James Anhalt's algorithm: https://jk-jeon.github.io/posts/2022/02/jeaiii-algorithm/ + val y1 = x * 140737489L + val m1 = 0x7fffffffffffL + val m2 = 100L + val y2 = (y1 & m1) * m2 + val y3 = (y2 & m1) * m2 + val y4 = (y3 & m1) * m2 + out.write(ds((y1 >> 47).toInt), ds((y2 >> 47).toInt), ds((y3 >> 47).toInt), ds((y4 >> 47).toInt)) + } + + @inline private[json] def write4Digits(x: Int, out: Write): Unit = { + val ds = digits + val q = x * 5243 >> 19 // divide a 4-digit positive int by 100 + out.write(ds(q), ds(x - q * 100)) + } + + @inline private[json] def write3Digits(x: Int, out: Write): Unit = { + val q = x * 1311 >> 17 // divide a 3-digit positive int by 100 + out.write((q + '0').toChar) + out.write(digits(x - q * 100)) + } + + @inline private[json] def write2Digits(x: Int, out: Write): Unit = + out.write(digits(x)) + + private[this] final val digits: Array[Short] = Array( + 12336, 12592, 12848, 13104, 13360, 13616, 13872, 14128, 14384, 14640, 12337, 12593, 12849, 13105, 13361, 13617, + 13873, 14129, 14385, 14641, 12338, 12594, 12850, 13106, 13362, 13618, 13874, 14130, 14386, 14642, 12339, 12595, + 12851, 13107, 13363, 13619, 13875, 14131, 14387, 14643, 12340, 12596, 12852, 13108, 13364, 13620, 13876, 14132, + 14388, 14644, 12341, 12597, 12853, 13109, 13365, 13621, 13877, 14133, 14389, 14645, 12342, 12598, 12854, 13110, + 13366, 13622, 13878, 14134, 14390, 14646, 12343, 12599, 12855, 13111, 13367, 13623, 13879, 14135, 14391, 14647, + 12344, 12600, 12856, 13112, 13368, 13624, 13880, 14136, 14392, 14648, 12345, 12601, 12857, 13113, 13369, 13625, + 13881, 14137, 14393, 14649 + ) + // Adoption of a nice trick form Daniel Lemire's blog that works for numbers up to 10^18: // https://lemire.me/blog/2021/06/03/computing-the-number-of-digits-of-an-integer-even-faster/ private[this] def digitCount(x: Long): Int = (offsets(java.lang.Long.numberOfLeadingZeros(x)) + x >> 58).toInt diff --git a/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala b/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala index 1e8cc971a..32c4ff86a 100644 --- a/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala +++ b/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala @@ -652,13 +652,15 @@ private final class ArraySeq(p: Array[Any]) extends IndexedSeq[Any] { private[this] final class NestedWriter(out: Write, indent: Option[Int]) extends Write { private[this] var state = 2 - def write(c: Char): Unit = - if (state != 0) { - if (c == ' ' || c == '\n') { - () - } else if (state == 2 && c == '{') { - state = 1 - } else if (state == 1) { + @inline def write(c: Char): Unit = + if (state == 0) out.write(c) + else nonZeroStateWrite(c) + + @noinline private[this] def nonZeroStateWrite(c: Char): Unit = + if (c != ' ' && c != '\n') { + if (state == 2) { + if (c == '{') state = 1 + } else { state = 0 if (c != '}') { out.write(',') @@ -666,18 +668,20 @@ private[this] final class NestedWriter(out: Write, indent: Option[Int]) extends } out.write(c) } - } else out.write(c) - - def write(s: String): Unit = - if (state != 0) { - var i = 0 - while (i < s.length) { - val c = s.charAt(i) - if (c == ' ' || c == '\n') { - () - } else if (state == 2 && c == '{') { - state = 1 - } else if (state == 1) { + } + + @inline def write(s: String): Unit = + if (state == 0) out.write(s) + else nonZeroStateWrite(s) + + @noinline private[this] def nonZeroStateWrite(s: String): Unit = { + var i = 0 + while (i < s.length) { + val c = s.charAt(i) + if (c != ' ' && c != '\n') { + if (state == 2) { + if (c == '{') state = 1 + } else { state = 0 if (c != '}') { out.write(',') @@ -689,9 +693,35 @@ private[this] final class NestedWriter(out: Write, indent: Option[Int]) extends } return } - i += 1 } - } else out.write(s) + i += 1 + } + } + + @inline override def write(cs: Array[Char], from: Int, to: Int): Unit = + if (state == 0) out.write(cs, from, to) + else nonZeroStateWrite(cs, from, to) + + @noinline def nonZeroStateWrite(cs: Array[Char], from: Int, to: Int): Unit = { + var i = from + while (i < to) { + val c = cs(i) + if (c != ' ' && c != '\n') { + if (state == 2) { + if (c == '{') state = 1 + } else { + state = 0 + if (c != '}') { + out.write(',') + JsonEncoder.pad(indent, out) + } + out.write(cs, i, to) + return + } + } + i += 1 + } + } } object DeriveJsonCodec { diff --git a/zio-json/shared/src/main/scala-3/zio/json/macros.scala b/zio-json/shared/src/main/scala-3/zio/json/macros.scala index 5f56b3e2c..aadc1c31f 100644 --- a/zio-json/shared/src/main/scala-3/zio/json/macros.scala +++ b/zio-json/shared/src/main/scala-3/zio/json/macros.scala @@ -730,13 +730,15 @@ object DeriveJsonEncoder extends JsonEncoderDerivation(JsonCodecConfiguration.de private[json] final class NestedWriter(out: Write, indent: Option[Int]) extends Write { private var state = 2 - def write(c: Char): Unit = - if (state != 0) { - if (c == ' ' || c == '\n') { - () - } else if (state == 2 && c == '{') { - state = 1 - } else if (state == 1) { + @inline def write(c: Char): Unit = + if (state == 0) out.write(c) + else nonZeroStateWrite(c) + + @noinline private def nonZeroStateWrite(c: Char): Unit = { + if (c != ' ' && c != '\n') { + if (state == 2) { + if (c == '{') state = 1 + } else { state = 0 if (c != '}') { out.write(',') @@ -744,18 +746,21 @@ object DeriveJsonEncoder extends JsonEncoderDerivation(JsonCodecConfiguration.de } out.write(c) } - } else out.write(c) - - def write(s: String): Unit = - if (state != 0) { - var i = 0 - while (i < s.length) { - val c = s.charAt(i) - if (c == ' ' || c == '\n') { - () - } else if (state == 2 && c == '{') { - state = 1 - } else if (state == 1) { + } + } + + @inline def write(s: String): Unit = + if (state == 0) out.write(s) + else nonZeroStateWrite(s) + + @noinline private def nonZeroStateWrite(s: String): Unit = { + var i = 0 + while (i < s.length) { + val c = s.charAt(i) + if (c != ' ' && c != '\n') { + if (state == 2) { + if (c == '{') state = 1 + } else { state = 0 if (c != '}') { out.write(',') @@ -767,9 +772,35 @@ object DeriveJsonEncoder extends JsonEncoderDerivation(JsonCodecConfiguration.de } return } - i += 1 } - } else out.write(s) + i += 1 + } + } + + @inline override def write(cs: Array[Char], from: Int, to: Int): Unit = + if (state == 0) out.write(cs, from, to) + else nonZeroStateWrite(cs, from, to) + + @noinline def nonZeroStateWrite(cs: Array[Char], from: Int, to: Int): Unit = { + var i = from + while (i < to) { + val c = cs(i) + if (c != ' ' && c != '\n') { + if (state == 2) { + if (c == '{') state = 1 + } else { + state = 0 + if (c != '}') { + out.write(',') + JsonEncoder.pad(indent, out) + } + out.write(cs, i, to) + return + } + } + i += 1 + } + } } } diff --git a/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala b/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala index ac0866e3a..339bc83d5 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala @@ -130,20 +130,20 @@ object JsonEncoder extends GeneratedTupleEncoders with EncoderLowPriority1 with out.write('"') } - override final def toJsonAST(a: String): Either[String, Json] = new Right(Json.Str(a)) + override final def toJsonAST(a: String): Either[String, Json] = new Right(new Json.Str(a)) private[this] def writeEncoded(a: String, out: Write): Unit = { val len = a.length var i = 0 while (i < len) { (a.charAt(i): @switch) match { - case '"' => out.write("\\\"") - case '\\' => out.write("\\\\") - case '\b' => out.write("\\b") - case '\f' => out.write("\\f") - case '\n' => out.write("\\n") - case '\r' => out.write("\\r") - case '\t' => out.write("\\t") + case '"' => out.write('\\', '"') + case '\\' => out.write('\\', '\\') + case '\b' => out.write('\\', 'b') + case '\f' => out.write('\\', 'f') + case '\n' => out.write('\\', 'n') + case '\r' => out.write('\\', 'r') + case '\t' => out.write('\\', 't') case c => if (c < ' ') out.write("\\u%04x".format(c.toInt)) else out.write(c) @@ -158,13 +158,13 @@ object JsonEncoder extends GeneratedTupleEncoders with EncoderLowPriority1 with override def unsafeEncode(a: Char, indent: Option[Int], out: Write): Unit = { out.write('"') (a: @switch) match { - case '"' => out.write("\\\"") - case '\\' => out.write("\\\\") - case '\b' => out.write("\\b") - case '\f' => out.write("\\f") - case '\n' => out.write("\\n") - case '\r' => out.write("\\r") - case '\t' => out.write("\\t") + case '"' => out.write('\\', '"') + case '\\' => out.write('\\', '\\') + case '\b' => out.write('\\', 'b') + case '\f' => out.write('\\', 'f') + case '\n' => out.write('\\', 'n') + case '\r' => out.write('\\', 'r') + case '\t' => out.write('\\', 't') case c => if (c < ' ') out.write("\\u%04x".format(c.toInt)) else out.write(c) @@ -172,7 +172,7 @@ object JsonEncoder extends GeneratedTupleEncoders with EncoderLowPriority1 with out.write('"') } - override final def toJsonAST(a: Char): Either[String, Json] = new Right(Json.Str(a.toString)) + override final def toJsonAST(a: Char): Either[String, Json] = new Right(new Json.Str(a.toString)) } private[json] def explicit[A](f: A => String, g: A => Json): JsonEncoder[A] = new JsonEncoder[A] { @@ -188,7 +188,7 @@ object JsonEncoder extends GeneratedTupleEncoders with EncoderLowPriority1 with out.write('"') } - override final def toJsonAST(a: A): Either[String, Json] = new Right(Json.Str(f(a))) + override final def toJsonAST(a: A): Either[String, Json] = new Right(new Json.Str(f(a))) } def suspend[A](encoder0: => JsonEncoder[A]): JsonEncoder[A] = @@ -204,22 +204,52 @@ object JsonEncoder extends GeneratedTupleEncoders with EncoderLowPriority1 with override def toJsonAST(a: A): Either[String, Json] = encoder.toJsonAST(a) } - implicit val boolean: JsonEncoder[Boolean] = explicit(_.toString, Json.Bool.apply) - implicit val symbol: JsonEncoder[Symbol] = string.contramap(_.name) - implicit val byte: JsonEncoder[Byte] = explicit(_.toString, Json.Num.apply) - implicit val short: JsonEncoder[Short] = explicit(_.toString, Json.Num.apply) - implicit val int: JsonEncoder[Int] = explicit(_.toString, Json.Num.apply) - implicit val long: JsonEncoder[Long] = explicit(_.toString, Json.Num.apply) + implicit val boolean: JsonEncoder[Boolean] = new JsonEncoder[Boolean] { + def unsafeEncode(a: Boolean, indent: Option[Int], out: Write): Unit = + if (a) out.write('t', 'r', 'u', 'e') + else out.write('f', 'a', 'l', 's', 'e') + + override final def toJsonAST(a: Boolean): Either[String, Json] = new Right(Json.Bool(a)) + } + implicit val symbol: JsonEncoder[Symbol] = string.contramap(_.name) + implicit val byte: JsonEncoder[Byte] = new JsonEncoder[Byte] { + def unsafeEncode(a: Byte, indent: Option[Int], out: Write): Unit = SafeNumbers.write(a.toInt, out) + + override def toJsonAST(a: Byte): Either[String, Json] = new Right(Json.Num(a)) + } + implicit val short: JsonEncoder[Short] = new JsonEncoder[Short] { + def unsafeEncode(a: Short, indent: Option[Int], out: Write): Unit = SafeNumbers.write(a.toInt, out) + + override def toJsonAST(a: Short): Either[String, Json] = new Right(Json.Num(a)) + } + implicit val int: JsonEncoder[Int] = new JsonEncoder[Int] { + def unsafeEncode(a: Int, indent: Option[Int], out: Write): Unit = SafeNumbers.write(a, out) + + override def toJsonAST(a: Int): Either[String, Json] = new Right(Json.Num(a)) + } + implicit val long: JsonEncoder[Long] = new JsonEncoder[Long] { + def unsafeEncode(a: Long, indent: Option[Int], out: Write): Unit = SafeNumbers.write(a, out) + + override def toJsonAST(a: Long): Either[String, Json] = new Right(Json.Num(a)) + } implicit val bigInteger: JsonEncoder[java.math.BigInteger] = explicit(_.toString, Json.Num.apply) implicit val scalaBigInt: JsonEncoder[BigInt] = explicit(_.toString, Json.Num.apply) - implicit val double: JsonEncoder[Double] = explicit(SafeNumbers.toString, Json.Num.apply) - implicit val float: JsonEncoder[Float] = explicit(SafeNumbers.toString, Json.Num.apply) + implicit val double: JsonEncoder[Double] = new JsonEncoder[Double] { + def unsafeEncode(a: Double, indent: Option[Int], out: Write): Unit = SafeNumbers.write(a, out) + + override def toJsonAST(a: Double): Either[String, Json] = new Right(Json.Num(a)) + } + implicit val float: JsonEncoder[Float] = new JsonEncoder[Float] { + def unsafeEncode(a: Float, indent: Option[Int], out: Write): Unit = SafeNumbers.write(a, out) + + override def toJsonAST(a: Float): Either[String, Json] = new Right(Json.Num(a)) + } implicit val bigDecimal: JsonEncoder[java.math.BigDecimal] = explicit(_.toString, n => new Json.Num(n)) implicit val scalaBigDecimal: JsonEncoder[BigDecimal] = explicit(_.toString, Json.Num.apply) implicit def option[A](implicit A: JsonEncoder[A]): JsonEncoder[Option[A]] = new JsonEncoder[Option[A]] { def unsafeEncode(oa: Option[A], indent: Option[Int], out: Write): Unit = - if (oa eq None) out.write("null") + if (oa eq None) out.write('n', 'u', 'l', 'l') else A.unsafeEncode(oa.get, indent, out) override def isNothing(oa: Option[A]): Boolean = (oa eq None) || A.isNothing(oa.get) @@ -238,7 +268,7 @@ object JsonEncoder extends GeneratedTupleEncoders with EncoderLowPriority1 with out.write('\n') var i = indent.get while (i > 0) { - out.write(" ") + out.write(' ', ' ') i -= 1 } } @@ -291,6 +321,7 @@ object JsonEncoder extends GeneratedTupleEncoders with EncoderLowPriority1 with case Right(b) => B.unsafeEncode(b, indent, out) } } + } private[json] trait EncoderLowPriority1 extends EncoderLowPriority2 { @@ -301,7 +332,7 @@ private[json] trait EncoderLowPriority1 extends EncoderLowPriority2 { override def isEmpty(as: Array[A]): Boolean = as.isEmpty def unsafeEncode(as: Array[A], indent: Option[Int], out: Write): Unit = - if (as.isEmpty) out.write("[]") + if (as.isEmpty) out.write('[', ']') else { out.write('[') if (indent.isDefined) unsafeEncodePadded(as, indent, out) @@ -362,7 +393,7 @@ private[json] trait EncoderLowPriority1 extends EncoderLowPriority2 { override def isEmpty(as: List[A]): Boolean = as eq Nil def unsafeEncode(as: List[A], indent: Option[Int], out: Write): Unit = - if (as eq Nil) out.write("[]") + if (as eq Nil) out.write('[', ']') else { out.write('[') if (indent.isDefined) unsafeEncodePadded(as, indent, out) @@ -438,7 +469,7 @@ private[json] trait EncoderLowPriority2 extends EncoderLowPriority3 { override def isEmpty(as: T[A]): Boolean = as.isEmpty def unsafeEncode(as: T[A], indent: Option[Int], out: Write): Unit = - if (as.isEmpty) out.write("[]") + if (as.isEmpty) out.write('[', ']') else { out.write('[') if (indent.isDefined) unsafeEncodePadded(as, indent, out) @@ -487,7 +518,7 @@ private[json] trait EncoderLowPriority2 extends EncoderLowPriority3 { override def isEmpty(a: T[K, A]): Boolean = a.isEmpty def unsafeEncode(kvs: T[K, A], indent: Option[Int], out: Write): Unit = - if (kvs.isEmpty) out.write("{}") + if (kvs.isEmpty) out.write('{', '}') else { out.write('{') if (indent.isDefined) unsafeEncodePadded(kvs, indent, out) @@ -553,22 +584,163 @@ private[json] trait EncoderLowPriority3 extends EncoderLowPriority4 { import java.time._ - implicit val dayOfWeek: JsonEncoder[DayOfWeek] = stringify(_.toString) - implicit val duration: JsonEncoder[Duration] = stringify(serializers.toString) - implicit val instant: JsonEncoder[Instant] = stringify(serializers.toString) - implicit val localDate: JsonEncoder[LocalDate] = stringify(serializers.toString) - implicit val localDateTime: JsonEncoder[LocalDateTime] = stringify(serializers.toString) - implicit val localTime: JsonEncoder[LocalTime] = stringify(serializers.toString) - implicit val month: JsonEncoder[Month] = stringify(_.toString) - implicit val monthDay: JsonEncoder[MonthDay] = stringify(serializers.toString) - implicit val offsetDateTime: JsonEncoder[OffsetDateTime] = stringify(serializers.toString) - implicit val offsetTime: JsonEncoder[OffsetTime] = stringify(serializers.toString) - implicit val period: JsonEncoder[Period] = stringify(serializers.toString) - implicit val year: JsonEncoder[Year] = stringify(serializers.toString) - implicit val yearMonth: JsonEncoder[YearMonth] = stringify(serializers.toString) - implicit val zonedDateTime: JsonEncoder[ZonedDateTime] = stringify(serializers.toString) - implicit val zoneId: JsonEncoder[ZoneId] = stringify(serializers.toString) - implicit val zoneOffset: JsonEncoder[ZoneOffset] = stringify(serializers.toString) + implicit val dayOfWeek: JsonEncoder[DayOfWeek] = stringify(_.toString) + + implicit val duration: JsonEncoder[Duration] = new JsonEncoder[Duration] { + def unsafeEncode(a: Duration, indent: Option[Int], out: Write): Unit = { + out.write('"') + serializers.write(a, out) + out.write('"') + } + + override final def toJsonAST(a: Duration): Either[String, Json] = + new Right(new Json.Str(serializers.toString(a))) + } + + implicit val instant: JsonEncoder[Instant] = new JsonEncoder[Instant] { + def unsafeEncode(a: Instant, indent: Option[Int], out: Write): Unit = { + out.write('"') + serializers.write(a, out) + out.write('"') + } + + override final def toJsonAST(a: Instant): Either[String, Json] = + new Right(new Json.Str(serializers.toString(a))) + } + + implicit val localDate: JsonEncoder[LocalDate] = new JsonEncoder[LocalDate] { + def unsafeEncode(a: LocalDate, indent: Option[Int], out: Write): Unit = { + out.write('"') + serializers.write(a, out) + out.write('"') + } + + override final def toJsonAST(a: LocalDate): Either[String, Json] = + new Right(new Json.Str(serializers.toString(a))) + } + + implicit val localDateTime: JsonEncoder[LocalDateTime] = new JsonEncoder[LocalDateTime] { + def unsafeEncode(a: LocalDateTime, indent: Option[Int], out: Write): Unit = { + out.write('"') + serializers.write(a, out) + out.write('"') + } + + override final def toJsonAST(a: LocalDateTime): Either[String, Json] = + new Right(new Json.Str(serializers.toString(a))) + } + + implicit val localTime: JsonEncoder[LocalTime] = new JsonEncoder[LocalTime] { + def unsafeEncode(a: LocalTime, indent: Option[Int], out: Write): Unit = { + out.write('"') + serializers.write(a, out) + out.write('"') + } + + override final def toJsonAST(a: LocalTime): Either[String, Json] = + new Right(new Json.Str(serializers.toString(a))) + } + + implicit val month: JsonEncoder[Month] = stringify(_.toString) + + implicit val monthDay: JsonEncoder[MonthDay] = new JsonEncoder[MonthDay] { + def unsafeEncode(a: MonthDay, indent: Option[Int], out: Write): Unit = { + out.write('"') + serializers.write(a, out) + out.write('"') + } + + override final def toJsonAST(a: MonthDay): Either[String, Json] = + new Right(new Json.Str(serializers.toString(a))) + } + + implicit val offsetDateTime: JsonEncoder[OffsetDateTime] = new JsonEncoder[OffsetDateTime] { + def unsafeEncode(a: OffsetDateTime, indent: Option[Int], out: Write): Unit = { + out.write('"') + serializers.write(a, out) + out.write('"') + } + + override final def toJsonAST(a: OffsetDateTime): Either[String, Json] = + new Right(new Json.Str(serializers.toString(a))) + } + + implicit val offsetTime: JsonEncoder[OffsetTime] = new JsonEncoder[OffsetTime] { + def unsafeEncode(a: OffsetTime, indent: Option[Int], out: Write): Unit = { + out.write('"') + serializers.write(a, out) + out.write('"') + } + + override final def toJsonAST(a: OffsetTime): Either[String, Json] = + new Right(new Json.Str(serializers.toString(a))) + } + + implicit val period: JsonEncoder[Period] = new JsonEncoder[Period] { + def unsafeEncode(a: Period, indent: Option[Int], out: Write): Unit = { + out.write('"') + serializers.write(a, out) + out.write('"') + } + + override final def toJsonAST(a: Period): Either[String, Json] = + new Right(new Json.Str(serializers.toString(a))) + } + + implicit val year: JsonEncoder[Year] = new JsonEncoder[Year] { + def unsafeEncode(a: Year, indent: Option[Int], out: Write): Unit = { + out.write('"') + serializers.write(a, out) + out.write('"') + } + + override final def toJsonAST(a: Year): Either[String, Json] = + new Right(new Json.Str(serializers.toString(a))) + } + + implicit val yearMonth: JsonEncoder[YearMonth] = new JsonEncoder[YearMonth] { + def unsafeEncode(a: YearMonth, indent: Option[Int], out: Write): Unit = { + out.write('"') + serializers.write(a, out) + out.write('"') + } + + override final def toJsonAST(a: YearMonth): Either[String, Json] = + new Right(new Json.Str(serializers.toString(a))) + } + + implicit val zonedDateTime: JsonEncoder[ZonedDateTime] = new JsonEncoder[ZonedDateTime] { + def unsafeEncode(a: ZonedDateTime, indent: Option[Int], out: Write): Unit = { + out.write('"') + serializers.write(a, out) + out.write('"') + } + + override final def toJsonAST(a: ZonedDateTime): Either[String, Json] = + new Right(new Json.Str(serializers.toString(a))) + } + + implicit val zoneId: JsonEncoder[ZoneId] = new JsonEncoder[ZoneId] { + def unsafeEncode(a: ZoneId, indent: Option[Int], out: Write): Unit = { + out.write('"') + serializers.write(a, out) + out.write('"') + } + + override final def toJsonAST(a: ZoneId): Either[String, Json] = + new Right(new Json.Str(serializers.toString(a))) + } + + implicit val zoneOffset: JsonEncoder[ZoneOffset] = new JsonEncoder[ZoneOffset] { + def unsafeEncode(a: ZoneOffset, indent: Option[Int], out: Write): Unit = { + out.write('"') + serializers.write(a, out) + out.write('"') + } + + override final def toJsonAST(a: ZoneOffset): Either[String, Json] = + new Right(new Json.Str(serializers.toString(a))) + } implicit val uuid: JsonEncoder[UUID] = stringify(_.toString) diff --git a/zio-json/shared/src/main/scala/zio/json/internal/writers.scala b/zio-json/shared/src/main/scala/zio/json/internal/writers.scala index 997e85530..b4130ba08 100644 --- a/zio-json/shared/src/main/scala/zio/json/internal/writers.scala +++ b/zio-json/shared/src/main/scala/zio/json/internal/writers.scala @@ -25,6 +25,63 @@ import java.util.Arrays trait Write { def write(c: Char): Unit def write(s: String): Unit + def write(cs: Array[Char], from: Int, to: Int): Unit = { + var i = from + while (i < to) { + write(cs(i)) + i += 1 + } + } + @inline def write(c1: Char, c2: Char): Unit = { + write(c1) + write(c2) + } + @inline def write(c1: Char, c2: Char, c3: Char): Unit = { + write(c1) + write(c2) + write(c3) + } + @inline def write(c1: Char, c2: Char, c3: Char, c4: Char): Unit = { + write(c1) + write(c2) + write(c3) + write(c4) + } + @inline def write(c1: Char, c2: Char, c3: Char, c4: Char, c5: Char): Unit = { + write(c1) + write(c2) + write(c3) + write(c4) + write(c5) + } + @inline def write(s: Short): Unit = { + write((s & 0xff).toChar) + write((s >> 8).toChar) + } + @inline def write(s1: Short, s2: Short): Unit = { + write((s1 & 0xff).toChar) + write((s1 >> 8).toChar) + write((s2 & 0xff).toChar) + write((s2 >> 8).toChar) + } + @inline def write(s1: Short, s2: Short, s3: Short): Unit = { + write((s1 & 0xff).toChar) + write((s1 >> 8).toChar) + write((s2 & 0xff).toChar) + write((s2 >> 8).toChar) + write((s3 & 0xff).toChar) + write((s3 >> 8).toChar) + } + @inline def write(s1: Short, s2: Short, s3: Short, s4: Short): Unit = { + write((s1 & 0xff).toChar) + write((s1 >> 8).toChar) + write((s2 & 0xff).toChar) + write((s2 >> 8).toChar) + write((s3 & 0xff).toChar) + write((s3 >> 8).toChar) + write((s4 & 0xff).toChar) + write((s4 >> 8).toChar) + } } // wrapper to implement the legacy Java API @@ -33,16 +90,6 @@ final class WriteWriter(out: java.io.Writer) extends Write { def write(c: Char): Unit = out.write(c.toInt) } -final class FastStringWrite(initial: Int) extends Write { - private[this] val sb: java.lang.StringBuilder = new java.lang.StringBuilder(initial) - - def write(s: String): Unit = sb.append(s): Unit - - def write(c: Char): Unit = sb.append(c): Unit - - def buffer: CharSequence = sb -} - // FIXME: remove in the next major version private[zio] final class FastStringBuilder(initial: Int) { private[this] var chars: Array[Char] = new Array[Char](initial) diff --git a/zio-json/shared/src/main/scala/zio/json/javatime/serializers.scala b/zio-json/shared/src/main/scala/zio/json/javatime/serializers.scala index 4f58364c6..74ac25c51 100644 --- a/zio-json/shared/src/main/scala/zio/json/javatime/serializers.scala +++ b/zio-json/shared/src/main/scala/zio/json/javatime/serializers.scala @@ -15,15 +15,22 @@ */ package zio.json.javatime +import zio.json.internal.{ FastStringWrite, SafeNumbers, Write } + import java.time._ private[json] object serializers { def toString(x: Duration): String = { - val s = new java.lang.StringBuilder(16) - s.append('P').append('T') + val out = writes.get + write(x, out) + out.buffer.toString + } + + def write(x: Duration, out: Write): Unit = { + out.write('P', 'T') val totalSecs = x.getSeconds var nano = x.getNano - if ((totalSecs | nano) == 0) s.append('0').append('S') + if ((totalSecs | nano) == 0) out.write('0', 'S') else { var effectiveTotalSecs = totalSecs if (totalSecs < 0 && nano > 0) effectiveTotalSecs += 1 @@ -31,28 +38,33 @@ private[json] object serializers { val secsOfHour = (effectiveTotalSecs - hours * 3600).toInt val minutes = secsOfHour / 60 val seconds = secsOfHour - minutes * 60 - if (hours != 0) s.append(hours).append('H') - if (minutes != 0) s.append(minutes).append('M') + if (hours != 0) { + SafeNumbers.write(hours, out) + out.write('H') + } + if (minutes != 0) { + SafeNumbers.write(minutes, out) + out.write('M') + } if ((seconds | nano) != 0) { - if (totalSecs < 0 && seconds == 0) s.append('-').append('0') - else s.append(seconds) + if (totalSecs < 0 && seconds == 0) out.write('-', '0') + else SafeNumbers.write(seconds, out) if (nano != 0) { if (totalSecs < 0) nano = 1000000000 - nano - val dotPos = s.length - s.append(nano + 1000000000) - var i = s.length - 1 - while (s.charAt(i) == '0') i -= 1 - s.setLength(i + 1) - s.setCharAt(dotPos, '.') + SafeNumbers.writeNano(nano, out) } - s.append('S') + out.write('S') } } - s.toString } def toString(x: Instant): String = { - val s = new java.lang.StringBuilder(32) + val out = writes.get + write(x, out) + out.buffer.toString + } + + def write(x: Instant, out: Write): Unit = { val epochSecond = x.getEpochSecond val epochDay = (if (epochSecond >= 0) epochSecond @@ -82,187 +94,243 @@ private[json] object serializers { val secsOfHour = secsOfDay - hour * 3600 val minute = secsOfHour * 17477 >> 20 // divide a small positive int by 60 val second = secsOfHour - minute * 60 - appendYear(year, s) - append2Digits(month, s.append('-')) - append2Digits(day, s.append('-')) - append2Digits(hour, s.append('T')) - append2Digits(minute, s.append(':')) - append2Digits(second, s.append(':')) + writeYear(year, out) + out.write('-') + SafeNumbers.write2Digits(month, out) + out.write('-') + SafeNumbers.write2Digits(day, out) + out.write('T') + SafeNumbers.write2Digits(hour, out) + out.write(':') + SafeNumbers.write2Digits(minute, out) + out.write(':') + SafeNumbers.write2Digits(second, out) val nano = x.getNano if (nano != 0) { - s.append('.') + out.write('.') val q1 = nano / 1000000 val r1 = nano - q1 * 1000000 - append3Digits(q1, s) + SafeNumbers.write3Digits(q1, out) if (r1 != 0) { val q2 = r1 / 1000 val r2 = r1 - q2 * 1000 - append3Digits(q2, s) - if (r2 != 0) append3Digits(r2, s) + SafeNumbers.write3Digits(q2, out) + if (r2 != 0) SafeNumbers.write3Digits(r2, out) } } - s.append('Z').toString + out.write('Z') } def toString(x: LocalDate): String = { - val s = new java.lang.StringBuilder(16) - appendLocalDate(x, s) - s.toString + val out = writes.get + write(x, out) + out.buffer.toString + } + + def write(x: LocalDate, out: Write): Unit = { + writeYear(x.getYear, out) + out.write('-') + SafeNumbers.write2Digits(x.getMonthValue, out) + out.write('-') + SafeNumbers.write2Digits(x.getDayOfMonth, out) } def toString(x: LocalDateTime): String = { - val s = new java.lang.StringBuilder(32) - appendLocalDate(x.toLocalDate, s) - appendLocalTime(x.toLocalTime, s.append('T')) - s.toString + val out = writes.get + write(x, out) + write(x.toLocalDate, out) + out.buffer.toString + } + + def write(x: LocalDateTime, out: Write): Unit = { + write(x.toLocalDate, out) + out.write('T') + write(x.toLocalTime, out) } def toString(x: LocalTime): String = { - val s = new java.lang.StringBuilder(24) - appendLocalTime(x, s) - s.toString + val out = writes.get + write(x, out) + out.buffer.toString + } + + def write(x: LocalTime, out: Write): Unit = { + SafeNumbers.write2Digits(x.getHour, out) + out.write(':') + SafeNumbers.write2Digits(x.getMinute, out) + out.write(':') + SafeNumbers.write2Digits(x.getSecond, out) + val nano = x.getNano + if (nano != 0) SafeNumbers.writeNano(nano, out) } def toString(x: MonthDay): String = { - val s = new java.lang.StringBuilder(8) - append2Digits(x.getMonthValue, s.append('-').append('-')) - append2Digits(x.getDayOfMonth, s.append('-')) - s.toString + val out = writes.get + write(x, out) + out.buffer.toString + } + + def write(x: MonthDay, out: Write): Unit = { + out.write('-', '-') + SafeNumbers.write2Digits(x.getMonthValue, out) + out.write('-') + SafeNumbers.write2Digits(x.getDayOfMonth, out) } def toString(x: OffsetDateTime): String = { - val s = new java.lang.StringBuilder(48) - appendLocalDate(x.toLocalDate, s) - appendLocalTime(x.toLocalTime, s.append('T')) - appendZoneOffset(x.getOffset, s) - s.toString + val out = writes.get + write(x, out) + out.buffer.toString + } + + def write(x: OffsetDateTime, out: Write): Unit = { + write(x.toLocalDate, out) + out.write('T') + write(x.toLocalTime, out) + write(x.getOffset, out) } def toString(x: OffsetTime): String = { - val s = new java.lang.StringBuilder(32) - appendLocalTime(x.toLocalTime, s) - appendZoneOffset(x.getOffset, s) - s.toString + val out = writes.get + write(x, out) + out.buffer.toString + } + + def write(x: OffsetTime, out: Write): Unit = { + write(x.toLocalTime, out) + write(x.getOffset, out) } def toString(x: Period): String = { - val s = new java.lang.StringBuilder(16) - s.append('P') - if (x.isZero) s.append('0').append('D') + val out = writes.get + write(x, out) + out.buffer.toString + } + + def write(x: Period, out: Write): Unit = { + out.write('P') + if (x.isZero) out.write('0', 'D') else { val years = x.getYears val months = x.getMonths val days = x.getDays - if (years != 0) s.append(years).append('Y') - if (months != 0) s.append(months).append('M') - if (days != 0) s.append(days).append('D') + if (years != 0) { + SafeNumbers.write(years, out) + out.write('Y') + } + if (months != 0) { + SafeNumbers.write(months, out) + out.write('M') + } + if (days != 0) { + SafeNumbers.write(days, out) + out.write('D') + } } - s.toString } def toString(x: Year): String = { - val s = new java.lang.StringBuilder(16) - appendYear(x.getValue, s) - s.toString + val out = writes.get + write(x, out) + out.buffer.toString } + @inline def write(x: Year, out: Write): Unit = writeYear(x.getValue, out) + def toString(x: YearMonth): String = { - val s = new java.lang.StringBuilder(16) - appendYear(x.getYear, s) - append2Digits(x.getMonthValue, s.append('-')) - s.toString + val out = writes.get + write(x, out) + out.buffer.toString } - def toString(x: ZonedDateTime): String = { - val s = new java.lang.StringBuilder(48) - appendLocalDate(x.toLocalDate, s) - appendLocalTime(x.toLocalTime, s.append('T')) - appendZoneOffset(x.getOffset, s) - val zone = x.getZone - if (!zone.isInstanceOf[ZoneOffset]) s.append('[').append(zone.getId).append(']') - s.toString + def write(x: YearMonth, out: Write): Unit = { + writeYear(x.getYear, out) + out.write('-') + SafeNumbers.write2Digits(x.getMonthValue, out) } - def toString(x: ZoneId): String = x.getId - - def toString(x: ZoneOffset): String = { - val s = new java.lang.StringBuilder(16) - appendZoneOffset(x, s) - s.toString + def toString(x: ZonedDateTime): String = { + val out = writes.get + write(x, out) + out.buffer.toString } - private[this] def appendLocalDate(x: LocalDate, s: java.lang.StringBuilder): Unit = { - appendYear(x.getYear, s) - append2Digits(x.getMonthValue, s.append('-')) - append2Digits(x.getDayOfMonth, s.append('-')) + def write(x: ZonedDateTime, out: Write): Unit = { + write(x.toLocalDate, out) + out.write('T') + write(x.toLocalTime, out) + write(x.getOffset, out) + val zone = x.getZone + if (!zone.isInstanceOf[ZoneOffset]) { + out.write('[') + out.write(zone.getId) + out.write(']') + } } - private[this] def appendLocalTime(x: LocalTime, s: java.lang.StringBuilder): Unit = { - append2Digits(x.getHour, s) - append2Digits(x.getMinute, s.append(':')) - append2Digits(x.getSecond, s.append(':')) - val nano = x.getNano - if (nano != 0) { - val dotPos = s.length - s.append(nano + 1000000000) - var i = s.length - 1 - while (s.charAt(i) == '0') i -= 1 - s.setLength(i + 1) - s.setCharAt(dotPos, '.') - } + @inline def toString(x: ZoneId): String = x.getId + + @inline def write(x: ZoneId, out: Write): Unit = out.write(x.getId) + + def toString(x: ZoneOffset): String = { + val out = writes.get + write(x, out) + out.buffer.toString } - private[this] def appendZoneOffset(x: ZoneOffset, s: java.lang.StringBuilder): Unit = { + def write(x: ZoneOffset, out: Write): Unit = { val totalSeconds = x.getTotalSeconds - if (totalSeconds == 0) s.append('Z'): Unit + if (totalSeconds == 0) out.write('Z'): Unit else { val q0 = if (totalSeconds > 0) { - s.append('+') + out.write('+') totalSeconds } else { - s.append('-') + out.write('-') -totalSeconds } val q1 = q0 * 37283 >>> 27 // divide a small positive int by 3600 val r1 = q0 - q1 * 3600 - append2Digits(q1, s) - s.append(':') + SafeNumbers.write2Digits(q1, out) + out.write(':') val q2 = r1 * 17477 >> 20 // divide a small positive int by 60 val r2 = r1 - q2 * 60 - append2Digits(q2, s) - if (r2 != 0) append2Digits(r2, s.append(':')) + SafeNumbers.write2Digits(q2, out) + if (r2 != 0) { + out.write(':') + SafeNumbers.write2Digits(r2, out) + } } } - private[this] def appendYear(x: Int, s: java.lang.StringBuilder): Unit = + private[this] def writeYear(x: Int, out: Write): Unit = if (x >= 0) { - if (x < 10000) append4Digits(x, s) - else s.append('+').append(x): Unit - } else if (x > -10000) append4Digits(-x, s.append('-')) - else s.append(x): Unit - - private[this] def append4Digits(x: Int, s: java.lang.StringBuilder): Unit = { - val q = x * 5243 >> 19 // divide a 4-digit positive int by 100 - append2Digits(q, s) - append2Digits(x - q * 100, s) - } - - private[this] def append3Digits(x: Int, s: java.lang.StringBuilder): Unit = { - val q = x * 1311 >> 17 // divide a 3-digit positive int by 100 - append2Digits(x - q * 100, s.append((q + '0').toChar)) - } - - private[this] def append2Digits(x: Int, s: java.lang.StringBuilder): Unit = { - val q = x * 103 >> 10 // divide a 2-digit positive int by 10 - s.append((q + '0').toChar).append((x + '0' - q * 10).toChar): Unit - } + if (x < 10000) SafeNumbers.write4Digits(x, out) + else { + out.write('+') + SafeNumbers.write(x, out): Unit + } + } else if (x > -10000) { + out.write('-') + SafeNumbers.write4Digits(-x, out) + } else SafeNumbers.write(x, out): Unit - private[this] def to400YearCycle(day: Long): Int = + @inline private[this] def to400YearCycle(day: Long): Int = (day / 146097).toInt // 146097 == number of days in a 400 year cycle - private[this] def toMarchDayOfYear(marchZeroDay: Long, year: Int): Int = { + @inline private[this] def toMarchDayOfYear(marchZeroDay: Long, year: Int): Int = { val century = year / 100 (marchZeroDay - year * 365L).toInt - (year >> 2) + century - (century >> 2) } + + private[this] val writes = new ThreadLocal[FastStringWrite] { + override def initialValue(): FastStringWrite = new FastStringWrite(64) + + override def get: FastStringWrite = { + val w = super.get + w.reset() + w + } + } }