やりたいこと
以下のようなJSON
について、ids
(UUID
の配列)が数万 ~ 数十万件単位になる状況で、UUID
をBase64
にした場合性能に対してどのような影響が有るか確認します。
{
"ids": [
"6c9352f3-77b3-4f0e-a26f-5e496f00496b",
"76dfbbe7-5ebd-437d-97a2-708ae57416d1",
"12cde1cf-5711-4301-a17e-16fcc6d15461"
]
}
結論
UUID
が5万件の場合で、最大限頑張る(後述する3のパターンを採用する)と、以下のような改善が見られました。
-
JSON
の容量が55%まで削減される(1.95MB -> 1.07MB) -
JSON
処理のスループットは1.35倍に向上する(8ms程度短縮)
JSON
処理の差は小さく、容量削減もネットワーク帯域が1gbpsで8msしか差が出ないようなので、余程ネットワーク帯域が小さくなければあまり影響も無いような気がします。
比較
JSON
関係の処理にはJackson
を利用します。
以下3パターンについて、JSON
化した際の容量面と性能面で比較します。
- 生
UUID
文字列の配列 -
UUID
のBase64
文字列の配列 -
UUID
配列全体をBase64
化した文字列
1 ~ 3を表すDTO
は以下を用います。
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
public static class RawIdList {
private final List<UUID> ids;
@JsonCreator public RawIdList(@JsonProperty("ids") List<UUID> ids) { this.ids = ids; }
public Object getIds() { return ids; }
}
public static class Base64IdList {
private final List<String> ids;
@JsonCreator public Base64IdList(@JsonProperty("ids") List<String> ids) { this.ids = ids;}
public Object getIds() { return ids; }
}
public static class Base64String {
private final String ids;
@JsonCreator public Base64String(@JsonProperty("ids") String ids) { this.ids = ids; }
public Object getIds() { return ids; }
}
Base64
エンコード・デコードについて
まず、2と3で用いる変換処理について説明します。
以下がソースコードです。
import java.nio.ByteBuffer;
import java.util.*;
private static final int UUID_SIZE_BYTES = 16;
private static final Base64.Encoder ENC = Base64.getEncoder().withoutPadding();
private static final Base64.Decoder DEC = Base64.getDecoder();
public static String encode(UUID id) {
ByteBuffer buf = ByteBuffer.allocate(UUID_SIZE_BYTES)
.putLong(id.getMostSignificantBits())
.putLong(id.getLeastSignificantBits());
return new String(ENC.encode(buf.array()));
}
public static UUID decode(String id) {
ByteBuffer buf = ByteBuffer.wrap(DEC.decode(id));
return new UUID(buf.getLong(), buf.getLong());
}
public static String encodeAll(List<UUID> ids) {
if (ids.isEmpty()) return "";
ByteBuffer buf = ByteBuffer.allocate(UUID_SIZE_BYTES * ids.size());
for (UUID id : ids) {
buf.putLong(id.getMostSignificantBits()).putLong(id.getLeastSignificantBits());
}
return new String(ENC.encode(buf.array()));
}
public static List<UUID> decodeAll(String ids) {
List<UUID> result = new ArrayList<>();
if (ids.isEmpty()) return result;
ByteBuffer buf = ByteBuffer.wrap(DEC.decode(ids));
while (buf.hasRemaining()) {
result.add(new UUID(buf.getLong(), buf.getLong()));
}
return result;
}
encode
・decode
関数はUUID
単体のBase64
変換処理です。
これはUUID
を2つのlong
としてBase64
変換しています。
この変換処理を使った場合、1つのUUID
が36文字 -> 22文字に短縮されます。
encodeAll
・decodeAll
関数は、UUID
配列を1つのBase64
文字列に変換する処理です。
これはUUID
配列をlong
の配列のように扱い、1つのBase64
文字列に変換しています。
このやり方をすることで、文字列を囲む"
や区切りの,
が無くなるため、全体で見れば1UUID
当たり単体変換よりも更に3文字短縮されます。
また、Base64
変換は6bitずつ行われるため、詰めて変換できる分UUID
を1つずつBase64
変換するよりも利用効率が高くなります。
容量面の比較
件数が5万件だった場合のJSON
の文字数を比較すると以下のようになります。
カッコ内は、Base64
変換しなかった場合と比較したサイズです。
- 1950009(100%)
- 1250009(64.1%)
- 1066677(54.7%)
計測は以下のように行いました。
private static final ObjectMapper MAPPER = new ObjectMapper();
List<UUID> sourceIds = new ArrayList<>();
for (int i = 0; i < 50000; i++) sourceIds.add(UUID.randomUUID());
String raw = MAPPER.writeValueAsString(new RawIdList(sourceIds));
String base64List = MAPPER.writeValueAsString(new Base64IdList(sourceIds.stream().map(JsonBenchmark::encode).toList()));
String base64String = MAPPER.writeValueAsString(new Base64String(encodeAll(sourceIds)));
System.out.println(raw.length());
System.out.println(base64List.length());
System.out.println(base64String.length());
性能面の比較
「JSON
シリアライズ -> デシリアライズしてUUID
を返却」という手順のJMH
ベンチマークを作成し、性能面の比較を行いました。
結果は以下の通りです(高いほど良い、また1 ~ 3の順に並べ替えて整形済み)。
Benchmark Mode Cnt Score Error Units
JsonBenchmark.raw thrpt 4 32.791 ± 3.149 ops/s
JsonBenchmark.base64List thrpt 4 36.995 ± 3.387 ops/s
JsonBenchmark.base64String thrpt 4 44.262 ± 5.383 ops/s
ベンチマークのコードは以下の通りです。
package org.wrongwrong;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.openjdk.jmh.annotations.*;
import java.nio.ByteBuffer;
import java.util.*;
@State(Scope.Benchmark)
public class JsonBenchmark {
private static final int UUID_SIZE_BYTES = 16;
private static final Base64.Encoder ENC = Base64.getEncoder().withoutPadding();
private static final Base64.Decoder DEC = Base64.getDecoder();
public static String encode(UUID id) {
ByteBuffer buf = ByteBuffer.allocate(UUID_SIZE_BYTES)
.putLong(id.getMostSignificantBits())
.putLong(id.getLeastSignificantBits());
return new String(ENC.encode(buf.array()));
}
public static UUID decode(String id) {
ByteBuffer buf = ByteBuffer.wrap(DEC.decode(id));
return new UUID(buf.getLong(), buf.getLong());
}
public static String encodeAll(List<UUID> ids) {
if (ids.isEmpty()) return "";
ByteBuffer buf = ByteBuffer.allocate(UUID_SIZE_BYTES * ids.size());
for (UUID id : ids) {
buf.putLong(id.getMostSignificantBits()).putLong(id.getLeastSignificantBits());
}
return new String(ENC.encode(buf.array()));
}
public static List<UUID> decodeAll(String ids) {
List<UUID> result = new ArrayList<>();
if (ids.isEmpty()) return result;
ByteBuffer buf = ByteBuffer.wrap(DEC.decode(ids));
while (buf.hasRemaining()) {
result.add(new UUID(buf.getLong(), buf.getLong()));
}
return result;
}
public static class RawIdList {
private final List<UUID> ids;
@JsonCreator public RawIdList(@JsonProperty("ids") List<UUID> ids) { this.ids = ids; }
public Object getIds() { return ids; }
}
public static class Base64IdList {
private final List<String> ids;
@JsonCreator public Base64IdList(@JsonProperty("ids") List<String> ids) { this.ids = ids;}
public Object getIds() { return ids; }
}
public static class Base64String {
private final String ids;
@JsonCreator public Base64String(@JsonProperty("ids") String ids) { this.ids = ids; }
public Object getIds() { return ids; }
}
private static final ObjectMapper MAPPER = new ObjectMapper();
public List<UUID> sourceIds;
@Setup(Level.Trial)
public void setUp() {
sourceIds = new ArrayList<>();
for (int i = 0; i < 50000; i++) sourceIds.add(UUID.randomUUID());
}
@Benchmark
public List<UUID> raw() throws JsonProcessingException {
RawIdList src = new RawIdList(sourceIds);
String json = MAPPER.writeValueAsString(src);
RawIdList res = MAPPER.readValue(json, RawIdList.class);
return res.ids;
}
@Benchmark
public List<UUID> base64List() throws JsonProcessingException {
Base64IdList src = new Base64IdList(sourceIds.stream().map(JsonBenchmark::encode).toList());
String json = MAPPER.writeValueAsString(src);
Base64IdList res = MAPPER.readValue(json, Base64IdList.class);
return res.ids.stream().map(JsonBenchmark::decode).toList();
}
@Benchmark
public List<UUID> base64String() throws JsonProcessingException {
Base64String src = new Base64String(encodeAll(sourceIds));
String json = MAPPER.writeValueAsString(src);
Base64String res = MAPPER.readValue(json, Base64String.class);
return decodeAll(res.ids);
}
}
build.gradle.kts
は以下の通りです。
plugins {
id("java")
id("me.champeau.jmh") version "0.7.2"
}
group = "org.wrongwrong"
version = "1.0-SNAPSHOT"
repositories {
mavenCentral()
}
dependencies {
// https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind
implementation("com.fasterxml.jackson.core:jackson-databind:2.17.1")
testImplementation(platform("org.junit:junit-bom:5.10.0"))
testImplementation("org.junit.jupiter:junit-jupiter")
}
tasks.test {
useJUnitPlatform()
}
jmh {
warmupForks = 2
warmupBatchSize = 3
warmupIterations = 3
warmup = "1s"
fork = 2
batchSize = 3
iterations = 2
timeOnIteration = "1500ms"
failOnError = true
}