Skip to content

Commit ab5bbb0

Browse files
authored
Optimize CBOR Integer encoding length (#1570)
by using unsigned numbers for determining buffer size
1 parent 48cf9e8 commit ab5bbb0

File tree

2 files changed

+225
-11
lines changed

2 files changed

+225
-11
lines changed

formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoding.kt

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -166,30 +166,29 @@ internal class CborEncoder(private val output: ByteArrayOutput) {
166166
}
167167

168168
private fun composeNumber(value: Long): ByteArray =
169-
if (value >= 0) composePositive(value) else composeNegative(value)
169+
if (value >= 0) composePositive(value.toULong()) else composeNegative(value)
170170

171-
private fun composePositive(value: Long): ByteArray = when (value) {
172-
in 0..23 -> byteArrayOf(value.toByte())
173-
in 24..Byte.MAX_VALUE -> byteArrayOf(24, value.toByte())
174-
in Byte.MAX_VALUE + 1..Short.MAX_VALUE -> encodeToByteArray(value, 2, 25)
175-
in Short.MAX_VALUE + 1..Int.MAX_VALUE -> encodeToByteArray(value, 4, 26)
176-
in (Int.MAX_VALUE.toLong() + 1..Long.MAX_VALUE) -> encodeToByteArray(value, 8, 27)
177-
else -> throw AssertionError("$value should be positive")
171+
private fun composePositive(value: ULong): ByteArray = when (value) {
172+
in 0u..23u -> byteArrayOf(value.toByte())
173+
in 24u..UByte.MAX_VALUE.toUInt() -> byteArrayOf(24, value.toByte())
174+
in (UByte.MAX_VALUE.toUInt() + 1u)..UShort.MAX_VALUE.toUInt() -> encodeToByteArray(value, 2, 25)
175+
in (UShort.MAX_VALUE.toUInt() + 1u)..UInt.MAX_VALUE -> encodeToByteArray(value, 4, 26)
176+
else -> encodeToByteArray(value, 8, 27)
178177
}
179178

180-
private fun encodeToByteArray(value: Long, bytes: Int, tag: Byte): ByteArray {
179+
private fun encodeToByteArray(value: ULong, bytes: Int, tag: Byte): ByteArray {
181180
val result = ByteArray(bytes + 1)
182181
val limit = bytes * 8 - 8
183182
result[0] = tag
184183
for (i in 0 until bytes) {
185-
result[i + 1] = ((value shr (limit - 8 * i)) and 0xFF).toByte()
184+
result[i + 1] = ((value shr (limit - 8 * i)) and 0xFFu).toByte()
186185
}
187186
return result
188187
}
189188

190189
private fun composeNegative(value: Long): ByteArray {
191190
val aVal = if (value == Long.MIN_VALUE) Long.MAX_VALUE else -1 - value
192-
val data = composePositive(aVal)
191+
val data = composePositive(aVal.toULong())
193192
data[0] = data[0] or HEADER_NEGATIVE
194193
return data
195194
}
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
package kotlinx.serialization.cbor
2+
3+
import kotlinx.serialization.decodeFromByteArray
4+
import kotlinx.serialization.encodeToByteArray
5+
import kotlin.test.Test
6+
import kotlin.test.assertEquals
7+
8+
class CborNumberEncodingTest {
9+
10+
// 0-23 packs into a single byte
11+
@Test
12+
fun testEncodingLengthOfTinyNumbers() {
13+
val tinyNumbers = listOf(0, 1, 23)
14+
for (number in tinyNumbers) {
15+
assertEquals(
16+
expected = 1,
17+
actual = Cbor.encodeToByteArray(number).size,
18+
"when encoding value '$number'"
19+
)
20+
}
21+
}
22+
23+
// 24..(2^8-1) packs into 2 bytes
24+
@Test
25+
fun testEncodingLengthOf8BitNumbers() {
26+
val tinyNumbers = listOf(24, 127, 128, 255)
27+
for (number in tinyNumbers) {
28+
assertEquals(
29+
expected = 2,
30+
actual = Cbor.encodeToByteArray(number).size,
31+
"when encoding value '$number'"
32+
)
33+
}
34+
}
35+
36+
// 2^8..(2^16-1) packs into 3 bytes
37+
@Test
38+
fun testEncodingLengthOf16BitNumbers() {
39+
val tinyNumbers = listOf(256, 32767, 32768, 65535)
40+
for (number in tinyNumbers) {
41+
assertEquals(
42+
expected = 3,
43+
actual = Cbor.encodeToByteArray(number).size,
44+
"when encoding value '$number'"
45+
)
46+
}
47+
}
48+
49+
// 2^16..(2^32-1) packs into 5 bytes
50+
@Test
51+
fun testEncodingLengthOf32BitNumbers() {
52+
val tinyNumbers = listOf(65536, 2147483647, 2147483648, 4294967295)
53+
for (number in tinyNumbers) {
54+
assertEquals(
55+
expected = 5,
56+
actual = Cbor.encodeToByteArray(number).size,
57+
"when encoding value '$number'"
58+
)
59+
}
60+
}
61+
62+
// 2^32+ packs into 9 bytes
63+
@Test
64+
fun testEncodingLengthOfLargeNumbers() {
65+
val tinyNumbers = listOf(4294967296, 8589934592)
66+
for (number in tinyNumbers) {
67+
assertEquals(
68+
expected = 9,
69+
actual = Cbor.encodeToByteArray(number).size,
70+
"when encoding value '$number'"
71+
)
72+
}
73+
}
74+
75+
@Test
76+
fun testEncodingLargestPositiveTinyNumber() {
77+
assertEquals(
78+
expected = byteArrayOf(23).toList(),
79+
actual = Cbor.encodeToByteArray(23).toList(),
80+
)
81+
}
82+
83+
@Test
84+
fun testDecodingLargestPositiveTinyNumber() {
85+
assertEquals(
86+
expected = 23,
87+
actual = Cbor.decodeFromByteArray(byteArrayOf(23)),
88+
)
89+
}
90+
91+
92+
@Test
93+
fun testEncodingLargestNegativeTinyNumber() {
94+
assertEquals(
95+
expected = byteArrayOf(55).toList(),
96+
actual = Cbor.encodeToByteArray(-24).toList(),
97+
)
98+
}
99+
100+
@Test
101+
fun testDecodingLargestNegativeTinyNumber() {
102+
assertEquals(
103+
expected = -24,
104+
actual = Cbor.decodeFromByteArray(byteArrayOf(55)),
105+
)
106+
}
107+
108+
@Test
109+
fun testEncodingLargestPositive8BitNumber() {
110+
val bytes = listOf(24, 255).map { it.toByte() }
111+
assertEquals(
112+
expected = bytes,
113+
actual = Cbor.encodeToByteArray(255).toList(),
114+
)
115+
}
116+
117+
@Test
118+
fun testDecodingLargestPositive8BitNumber() {
119+
val bytes = listOf(24, 255).map { it.toByte() }.toByteArray()
120+
assertEquals(
121+
expected = 255,
122+
actual = Cbor.decodeFromByteArray(bytes),
123+
)
124+
}
125+
126+
@Test
127+
fun testEncodingLargestNegative8BitNumber() {
128+
val bytes = listOf(56, 255).map { it.toByte() }
129+
assertEquals(
130+
expected = bytes,
131+
actual = Cbor.encodeToByteArray(-256).toList(),
132+
)
133+
}
134+
135+
@Test
136+
fun testDecodingLargestNegative8BitNumber() {
137+
val bytes = listOf(56, 255).map { it.toByte() }.toByteArray()
138+
assertEquals(
139+
expected = -256,
140+
actual = Cbor.decodeFromByteArray(bytes),
141+
)
142+
}
143+
144+
@Test
145+
fun testEncodingLargestPositive16BitNumber() {
146+
val bytes = listOf(25, 255, 255).map { it.toByte() }
147+
assertEquals(
148+
expected = bytes,
149+
actual = Cbor.encodeToByteArray(65535).toList(),
150+
)
151+
}
152+
153+
@Test
154+
fun testDecodingLargestPositive16BitNumber() {
155+
val bytes = listOf(25, 255, 255).map { it.toByte() }.toByteArray()
156+
assertEquals(
157+
expected = 65535,
158+
actual = Cbor.decodeFromByteArray(bytes),
159+
)
160+
}
161+
162+
@Test
163+
fun testEncodingLargestNegative16BitNumber() {
164+
val bytes = listOf(57, 255, 255).map { it.toByte() }
165+
assertEquals(
166+
expected = bytes,
167+
actual = Cbor.encodeToByteArray(-65536).toList(),
168+
)
169+
}
170+
171+
@Test
172+
fun testDecodingLargestNegative16BitNumber() {
173+
val bytes = listOf(57, 255, 255).map { it.toByte() }.toByteArray()
174+
assertEquals(
175+
expected = -65536,
176+
actual = Cbor.decodeFromByteArray(bytes),
177+
)
178+
}
179+
180+
@Test
181+
fun testEncodingLargestPositive32BitNumber() {
182+
val bytes = listOf(26, 255, 255, 255, 255).map { it.toByte() }
183+
assertEquals(
184+
expected = bytes,
185+
actual = Cbor.encodeToByteArray(4294967295).toList(),
186+
)
187+
}
188+
189+
@Test
190+
fun testDecodingLargestPositive32BitNumber() {
191+
val bytes = listOf(26, 255, 255, 255, 255).map { it.toByte() }.toByteArray()
192+
assertEquals(
193+
expected = 4294967295,
194+
actual = Cbor.decodeFromByteArray(bytes),
195+
)
196+
}
197+
198+
@Test
199+
fun testEncodingLargestNegative32BitNumber() {
200+
val bytes = listOf(58, 255, 255, 255, 255).map { it.toByte() }
201+
assertEquals(
202+
expected = bytes,
203+
actual = Cbor.encodeToByteArray(-4294967296).toList(),
204+
)
205+
}
206+
207+
@Test
208+
fun testDecodingLargestNegative32BitNumber() {
209+
val bytes = listOf(58, 255, 255, 255, 255).map { it.toByte() }.toByteArray()
210+
assertEquals(
211+
expected = -4294967296,
212+
actual = Cbor.decodeFromByteArray(bytes),
213+
)
214+
}
215+
}

0 commit comments

Comments
 (0)