0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

お題は不問!Qiita Engineer Festa 2024で記事投稿!
Qiita Engineer Festa20242024年7月17日まで開催中!

【Java】大量のUUID配列をJSONでやり取りする状況で、UUIDをBase64化する影響を比較する

Last updated at Posted at 2024-06-10

やりたいこと

以下のようなJSONについて、idsUUIDの配列)が数万 ~ 数十万件単位になる状況で、UUIDBase64にした場合性能に対してどのような影響が有るか確認します。

{
  "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化した際の容量面と性能面で比較します。

  1. UUID文字列の配列
  2. UUIDBase64文字列の配列
  3. 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;
}

encodedecode関数はUUID単体のBase64変換処理です。
これはUUIDを2つのlongとしてBase64変換しています。
この変換処理を使った場合、1つのUUIDが36文字 -> 22文字に短縮されます。

encodeAlldecodeAll関数は、UUID配列を1つのBase64文字列に変換する処理です。
これはUUID配列をlongの配列のように扱い、1つのBase64文字列に変換しています。
このやり方をすることで、文字列を囲む"や区切りの,が無くなるため、全体で見れば1UUID当たり単体変換よりも更に3文字短縮されます。
また、Base64変換は6bitずつ行われるため、詰めて変換できる分UUIDを1つずつBase64変換するよりも利用効率が高くなります。

容量面の比較

件数が5万件だった場合のJSONの文字数を比較すると以下のようになります。
カッコ内は、Base64変換しなかった場合と比較したサイズです。

  1. 1950009(100%)
  2. 1250009(64.1%)
  3. 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
}
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?