fukuoka.go #20で言ってたjsonとProtocol Bufferの話

今日、僕じゃないセッションのQAでふわっとしてたJSONとProtocol Bufferを比較したときにProtocol Bufferのほうが速いだろうみたいな話があって、一瞬そうだろうなと僕も思ったけど、結構データの性質による気もしていて、ベンチマーク取ってみた。

  • fukuoka.proto
syntax = "proto3";

package main;

import "google/protobuf/timestamp.proto";

option go_package = "./;main";

message ProtoMessage {
    int64 id = 1;
    string name = 2;
    google.protobuf.Timestamp timestamp = 3;
}


  • fukuoka_test.go
package main

import (
        "testing"

        goccyjson "github.com/goccy/go-json"
        "google.golang.org/protobuf/proto"
        "google.golang.org/protobuf/types/known/timestamppb"
)

// SampleMessage is a sample message for both JSON and Protobuf
type SampleMessage struct {
        ID        int64  `json:"id"`
        Name      string `json:"name"`
        Timestamp int64  `json:"timestamp"`
}

func BenchmarkGoJSONEncoding(b *testing.B) {
        msg := SampleMessage{
                ID:        1,
                Name:      "Test",
                Timestamp: 1234567890,
        }
        b.ResetTimer()
        for i := 0; i < b.N; i++ {
                _, err := goccyjson.Marshal(&msg)
                if err != nil {
                        b.Fatalf("go-json encoding failed: %v", err)
                }
        }
}

func BenchmarkGoJSONDecoding(b *testing.B) {
        msg := SampleMessage{
                ID:        1,
                Name:      "Test",
                Timestamp: 1234567890,
        }
        data, err := goccyjson.Marshal(&msg)
        if err != nil {
                b.Fatalf("go-json encoding failed: %v", err)
        }
        b.ResetTimer()
        for i := 0; i < b.N; i++ {
                var decodedMsg SampleMessage
                err := goccyjson.Unmarshal(data, &decodedMsg)
                if err != nil {
                        b.Fatalf("go-json decoding failed: %v", err)
                }
        }
}

func BenchmarkProtobufEncoding(b *testing.B) {
        msg := &ProtoMessage{
                Id:        1,
                Name:      "Test",
                Timestamp: timestamppb.Now(),
        }
        b.ResetTimer()
        for i := 0; i < b.N; i++ {
                _, err := proto.Marshal(msg)
                if err != nil {
                        b.Fatalf("Protobuf encoding failed: %v", err)
                }
        }
}

func BenchmarkProtobufDecoding(b *testing.B) {
        msg := &ProtoMessage{
                Id:        1,
                Name:      "Test",
                Timestamp: timestamppb.Now(),
        }
        data, err := proto.Marshal(msg)
        if err != nil {
                b.Fatalf("Protobuf encoding failed: %v", err)
        }
        b.ResetTimer()
        for i := 0; i < b.N; i++ {
                var decodedMsg ProtoMessage
                err := proto.Unmarshal(data, &decodedMsg)
                if err != nil {
                        b.Fatalf("Protobuf decoding failed: %v", err)
                }
        }
}

こんなデータを準備して、実行する。

% protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative fukuoka.proto
% go mod init 
% go mod tidy
% go test -bench=.
goos: darwin
goarch: arm64
pkg: github.com/pyama86/fukuokago_20
cpu: Apple M3 Pro
BenchmarkGoJSONEncoding-11              21337369                55.00 ns/op
BenchmarkGoJSONDecoding-11              12696033                95.23 ns/op
BenchmarkProtobufEncoding-11            12079659                89.64 ns/op
BenchmarkProtobufDecoding-11             8756516               127.8 ns/op
PASS
ok      github.com/pyama86/fukuokago_20 6.026s

こう見ると単純なエンコード、デコードはJSONのほうが速そうに見えていて、ネットワークを介したときにデータサイズの差がどれくらい影響するかみたいなのは環境とデータサイズによりそうだから、必ずしもProtocolBufferのほうが優位とは言い切れないんじゃないかと思った。