4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Go言語の圧縮ライブラリ比較

Last updated at Posted at 2024-03-12

Go言語における圧縮ライブラリの性能を比較検討したので、その結果を共有します。

この評価は2024年2月頃に実施しています。

背景

Apache Cassandraのデータ圧縮アルゴリズムをGo言語で実装する際にのライブラリ選定のためのものです。
そのため、Apache Cassandraのサポートする圧縮アルゴリズムを中心に検証しています。

また、Apache Cassandraではスループットが最も高いLZ4がデフォルトの圧縮フォーマットとして採用されています。
Go言語でもLZ4はデフォルトで適用して問題ないような負荷なのかを評価するのが目的なので、主に圧縮率よりもスループットに注目しています。

また、データサイズもDBMSが取り扱うようなもの、具体的には数KiB〜100KiB程度を想定しています。

結論

フォーマットが選べる場合のライブラリ選定

圧縮フォーマットを選べる場合、cgoを使ったライブラリが使えるかどうかによって2パターンに分けておすすめを紹介します。

cgoを使っても問題ない

  • バランス型
    • github.com/DataDog/zstd (Level 1)
  • 圧縮率重視
    • github.com/DataDog/zstd (Level 1~5)
  • 速度のみを求める
    • github.com/DataDog/golz4

cgoを使いたくない

  • バランス型
    • compress/gzip (gzip.BestSpeed)
  • 圧縮率重視
    • compress/gzip (gzip.DefaultCompression)
  • 速度のみを求める
    • github.com/golang/snappy

フォーマットが仕様で決まってる場合のライブラリ選定

圧縮フォーマットを自由に選べない場合(既にどのフォーマットを使うかが決まっている場合)、以下のライブラリがおすすめです。

  • Gzip
    • compress/gzip
  • Zlib
    • 今回比較していませんが、compress/zlibで最低限の性能が発揮できていそうです。
  • Zstd
    • github.com/DataDog/zstd
  • Snappy
    • github.com/golang/snappy
  • LZ4
    • github.com/DataDog/golz4

その他の知見

  • compress/gzipよりもgithub.com/klauspost/compress/gzipの方が少し良い性能を発揮する
    • ただ、標準であるcompress/gzipを置き換えるほどではないため、"おすすめ"にはcompress/gzipを採用
  • よりローレベルなAPIを使うと、スループットが2~3倍向上する
  • 圧縮率は差があったとしても10%程度。速度は1000倍の差が出る場合がある

cgo/アセンブラを持つようなライブラリは最適化が進んでいる

圧縮処理は実装によってスループットがかなり異なり、ライブラリの品質を事前に評価することは重要です。
全体的にやはりcgoやアセンブラ実装を持つものはスループットが高い傾向にあり、Pure Goのものは遅い傾向にあります。

この差はGo言語とよりネイティブなコードの速度差から来ている可能性も一部ありますが、それよりcgoやアセンブラコードを組み込むくらい最適化されているコードという意味でライブラリとしての速度が高いのだと思われます。(一般的に言われる例えばC言語とGo言語の速度差とは桁違いの差がライブラリ間にある場合があります)
プロダクションで動かすこと考えれば、cgoやアセンブラ実装を持つものを狙ってライブラリを探すと良さそうです。

どの程度のCPU負荷を許容できるか見積もる

今回は1コア(1 goroutine)でのスループットを求めています。例えばアプリケーションがリクエストを処理する上で1秒間に1コアあたり1MiBのデータを圧縮/解凍する必要がある場合、
スループットが100MiB/sのフォーマットを使えばCPUのうち1%は圧縮/解凍に使われることになります。
どの程度までは許容できそうかはこの方法で見積もることが出来るでしょう。

今回私の場合は最大で1 goroutine 10MiB/s程度を処理すると仮定しました。圧縮処理がCPUに占める割合を出来れば1%、最低でも10%に抑えたいとする場合、
1000MiB/s ~ 100MiB/s程度のスループットを持つライブラリを選定するのが良いということになります。

それを考えると、私のケースで採用できそうなのは以下のフォーマット/ライブラリになります

  • ワークロードによらず採用できる
    • github.com/DataDog/golz4
    • github.com/DataDog/zstd (Level 1)
    • github.com/golang/snappy
  • Readが多いワークロードであれば採用できる
    • github.com/DataDog/zstd (Level 1~5くらいまで, Level10を超えるとWriteが遅いので難しそう)
    • compress/gzip (gzip.BestSpeed)

環境

  • Linux(KVM)
    • OS: Rocky Linux 8.8
    • CPU: AMD EPYC 7751 32-Core Processor (2.0GHz - 3.0GHz)
  • Go
    • go version go1.22.0 linux/amd64

ベンチマーク方法

testing.Benchmark関数を使ってベンチマークを取っています。
testing.Benchmarkは、実行した回数と所要時間が得られるので、それを元にN回の実行での平均のスループットを求めています。

var dataSize int // 今回処理するデータのサイズ
r := testing.Benchmark(func (b *testing.B) {
    // 何らかの準備があればここに
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        // 圧縮/解凍の処理を書く
    }
})
processedSize := float64(r.N) * float64(dataSize)
throughput := processedSize / r.T.Seconds()

対象ライブラリ

同一のフォーマットでもいくつかの実装が存在し、それぞれ測定を行いました。

名前 フォーマット cgo 備考
compress/gzip GZip Go言語標準
compress/zlib ZLib Go言語標準
github.com/DataDog/golz4 LZ4 liblz4をインストールする必要がある
github.com/DataDog/zstd Zstd
github.com/golang/snappy Snappy
github.com/klauspost/compress/gzip Gzip
github.com/klauspost/compress/s2 s2
github.com/klauspost/compress/snappy Snappy
github.com/klauspost/compress/Zstd Zstd
github.com/pierrec/lz4 LZ4

cgoにチェックマークが付いているライブラリは、CGO_ENABLED=0でビルドできないため注意が必要です。
github.com/DataDog/golz4は、別途liblz4をインストールする必要があります。それ以外はGo言語のシングルバイナリとしてビルド出来ます。

github.com/DataDog/zstdは特殊で、C言語のヘッダーのみのライブラリで、go getするだけでlibzstdのコードはgo build
でビルドされたバイナリに含まれます。
ただし、go build -tags=external_libzstdとすることで、外部のlibzstdを利用することも出来ます。

圧縮データ

私たちの環境で実際に利用されそうなデータを想定して用意しました。

名前 サイズ 説明
alice29.txt 152089 不思議の国のアリス(英語)
fireworks.jpeg 123093 花火の写真(JPEG)
geo.protodata 118588 地理情報データ(Protocol Buffers)
html_x_4 409600 HTMLファイルを4回繰り返して水増ししたもの(HTML)
status.json 390485 とあるDBMSのメトリック等が入ったJSON(JSON)
urls.10K 702088 URLリスト(10,000件)
wagahaiwa_nekodearu.html 1219970 吾輩は猫である(日本語, Shift_JIS, XHTML)
wagahaiwa_nekodearu.txt 749051 吾輩は猫である(日本語, Shift_JIS, txt)

パラメータ

Gzip, Zstd, LZ4には圧縮率と速度を調整するためのパラメータがあります。
すべての組み合わせを試すのは時間がかかるので、いくつか典型的だと思われるパターンを決め、名前をつけてあります。

名前 フォーマット ライブラリ パラメータ 備考
GzipBestSpeed Gzip compress/gzip level=1
GzipDefault Gzip compress/gzip gzip.DefaultCompression (level=6)
GzipBestCompression Gzip compress/gzip level=9
ZlibBestSpeed ZLib compress/zlib zlib.BestSpeed (level=1)
ZlibDefault ZLib compress/zlib zlib.DefaultCompression (level=6)
ZlibBestCompression ZLib compress/zlib zlib.BestCompression (level=9)
KlauspostGzipDefault Gzip github.com/klauspost/compress/gzip gzip.DefaultCompression (level=6)
KlauspostZstdFastest Zstd github.com/klauspost/compress/zstd zstd.SpeedFastest (level=1)
KlauspostZstdDefault Zstd github.com/klauspost/compress/zstd zstd.SpeedDefault (level=2)
KlauspostZstdBetter Zstd github.com/klauspost/compress/zstd zstd.SpeedBest (level=3)
KlauspostZstdBest Zstd github.com/klauspost/compress/zstd zstd.SpeedBest (level=4)
KlauspostSnappy Snappy github.com/klauspost/compress/snappy
DDLZ4 LZ4 github.com/DataDog/golz4
DDZstd1 Zstd github.com/DataDog/zstd level=1
DDZstd5 Zstd github.com/DataDog/zstd level=5
DDZstd10 Zstd github.com/DataDog/zstd level=10
DDZstd20 Zstd github.com/DataDog/zstd level=20
LZ4-1 LZ4 github.com/pierrec/lz4 level=1
Snappy Snappy github.com/golang/snappy
s2 s2 github.com/klauspost/compress/s2

ベンチマークする操作

今回は Read, Write, Compress, Decompressの4つの操作をベンチマークしました。
ただ、簡単のために基本となる Read, Write APIを結果として記載しています。

Write, Read

io.Writer, io.ReaderをラップするようなAPIを使用し測定します。渡したストリームをブロックに分割して処理するので、任意のサイズのデータを処理できます。
実際のデータの形式は各フォーマット毎に違いますが、APIレベルでの性能を測定します。

Writeの例: github.com/DataDog/zstd を使ってストリームを圧縮

// import ddzstd "github.com/DataDog/zstd"
var plain io.Reader
encoded, _ := ddzstd.NewWriterLevel(plain, ddzstd.BestSpeed)
_, _ = io.Copy(io.Discard, encoded)

Readの例: github.com/DataDog/zstd を使ってストリームを解凍

// import ddzstd "github.com/DataDog/zstd"
var encoded io.Reader
decoded, _ := ddzstd.NewReader(encoded)
_, _ = io.Copy(io.Discard, decoded)

Compress, Decompress

基本的に []byteを渡すと処理して[]byteを返すAPIです。基本的に大きなデータを処理するのには向きません(~数KiBまで)
Write, Readと違い、ブロック分割したりする処理がなく、各ライブラリは受け取ったデータを圧縮、展開処理を行うのみなのでより純粋な性能を測定できます。
実用的ではないので通常は使用しないローレベルなAPIとなります。ただし、今回我々のユースケースではうまく使えるので測定しました。
また、すべてのライブラリで利用可能なAPIではないため、APIがない場合は対象外としています。

Compressの例: github.com/DataDog/zstd を使って[]byteを圧縮

// import ddzstd "github.com/DataDog/zstd"
var plain []byte
buf := make([]byte, ddzstd.CompressBound(len(plain))) // 圧縮後の最大サイズを求めてバッファを確保
encodedSize, _ := ddzstd.CompressLevel(buf, plain, ddzstd.BestSpeed)
encoded := buf[:encodedSize]
decodedSize := len(plain) // Decompressの際に必要

Decompressの例: github.com/DataDog/zstd を使って[]byteを解凍

// import ddzstd "github.com/DataDog/zstd"
var decodedSize int // 圧縮時得られたサイズを別途保存しておく
var encoded []byte
out := make([]byte, decodedSize)
_, _ = ddzstd.Decompress(out, encoded)

圧縮率とスループットの両面で評価

最終的な圧縮率とスループット両面での評価をまず記載します。
個別に圧縮率のみ(データによる違いを調査)、スループットのみ(データサイズ、実装による違いを調査)の結果は後に記載しています。

この表・図は、すべてのデータセットについて、10240ByteのデータをWrite, Readした場合の結果をプロットしたものです。

表: 圧縮率とスループット

名前 圧縮率 Writeスループット(MiB/s) Readスループット(MiB/s)
DDLZ4 0.656 212.0 1020.0
DDZstd1 0.528 180.8 372.9
DDZstd10 0.486 3.7 353.1
DDZstd20 0.466 0.2 102.3
DDZstd5 0.488 45.0 351.2
GzipBestCompression 0.476 14.9 33.0
GzipBestSpeed 0.502 15.8 100.6
GzipDefault 0.476 15.7 33.1
KlauspostGzipDefault 0.483 16.2 45.6
KlauspostSnappy 0.610 123.4 142.7
KlauspostZstdBest 0.477 0.2 61.2
KlauspostZstdBetter 0.495 0.7 65.8
KlauspostZstdDefault 0.496 3.3 61.2
KlauspostZstdFastest 0.521 7.4 64.9
LZ4-1 0.632 4.1 3.6
s2 0.659 27.5 18.9
snappy 0.633 126.0 94.1
ZlibBestComp 0.475 15.0 31.4
ZlibBestSpeed 0.501 15.9 83.3
ZlibDefault 0.475 16.0 31.7

プロットすると以下のようになります。縦軸が大きいほど高速、横軸が大きいほど高圧縮です。
つまり、右上ほど速度と圧縮率の両面で良いフォーマットと言えます。

図:Writeの圧縮率-スループット
横軸: 圧縮率(0.0=無圧縮), 縦軸: スループット(MiB/s)

ratio-thruput-write.png

図:Readの圧縮率-スループット
横軸: 圧縮率(0.0=無圧縮), 縦軸: スループット(MiB/s)

ratio-thruput-read.png

圧縮率

スループットを気にせず圧縮率だけを見た場合の結果を示します。

結果をざっくり言うならば

Zstd > Gzip = Zlib >> Snappy = LZ4

というようになっています

データによって傾向は違うものの、Zstdは比較的圧縮率に優れることがわかります。逆に、LZ4, Snappyは圧縮率には優れないことがわかります。
極端なパラメータは無視し、Zstdのデフォルト(DDzstd5)とLZ4のデフォルト(DDLZ4)やSnappy(snappy)
の間には20%近い差があり、この差が重要か重要でないかはワークロードに併せて検討が必要です。

alice29.txtのみは他のデータセットと少し違う傾向を示していて、Zlib, Gzip系が数%良い結果を示しています。

表は各フォーマットでの圧縮率を示しています。0.0の時元データと同じサイズで、1.0に近づくにつれて圧縮後のサイズが小さくなります。
よりよい結果のものを緑で、悪い結果のものを赤で示しています。
また、 wagaheiwa_nekodearu.html の圧縮率によってソートしています。

compression_ratio.png

圧縮速度

パラメーターの選択

今回我々は圧縮率よりもスループットを重視して選定を行っています。
そのため、基本的に各フォーマットの「高圧縮設定」は基本的に選択肢に入りません。

念のため事前調査として、パラメータによってどの程度スループットが変わるかを調査し、例としてZstdを掲載します。
グラフは縦軸が対数になっています。注意してください。

Readは最高圧縮と最低圧縮で2倍程度の差ですが、Writeでは100倍以上の差があります。

我々のワークロードではこの容量差は大きな問題にはならないと判断したため、基本的に最も高速なパラメータをベンチマーク対象としています。

表: 102400BytesのデータをWrite/Readした場合のスループット

Op DDZstd1 DDZstd5 DDZstd10 DDZstd20
Write 135.2MiB/s 49.5MiB/s 15.0MiB/s 1.1MiB/s
Read 232.3MiB/s 173.4MiB/s 186.7MiB/s 119.6MiB/s

横軸: 圧縮・解凍するデータサイズ、縦軸: スループット

zstd_parameters-write.png
zstd_parameters-reads.png

余談ですが、Zstdは解凍速度重視のフォーマットであることはWikipedia等を見れば書いてありますが、解凍速度自体よりもLevel=10程度までは解凍速度がほぼ変わらないというのはReadが多い
実システムでは非常に運用しやすい特性と言えるかも知れません。

Write

Writeにおいては、DDLZ4, DDZstd1, snappyが良好なスループットを達成しています。

表: 各フォーマットのWriteスループット。単位はMiB/s
すべてのデータセットについて5回計測し、すべての計測の平均値を採用しています。

Data size GzipBestSpeed GzipDefault ZlibBestSpeed ZlibDefault snappy DDZstd1 DDZstd5 DDLZ4
102 0.2 0.3 0.2 0.3 1.8 8.3 1.1 3.4
256 0.5 0.7 0.5 0.8 4.5 16.2 2.7 8.5
512 1.0 1.5 1.0 1.5 8.9 29.2 5.0 16.8
1024 1.9 2.8 2.0 2.9 17.7 51.5 9.3 32.6
2560 4.5 6.1 4.6 6.1 41.2 88.7 18.7 71.8
5120 8.8 10.2 8.8 10.3 76.1 132.1 30.6 129.5
10240 15.8 15.7 15.9 16.0 126.0 180.8 45.0 212.0
25600 32.9 24.0 33.0 23.6 220.6 249.3 69.9 357.4
51200 51.8 27.1 50.5 26.7 289.9 258.1 85.3 455.7
102400 68.6 30.1 66.3 30.2 324.4 274.4 96.3 375.0

図:Writeのスループット
横軸: 圧縮・解凍するデータサイズ(Bytes)、縦軸: スループット(MiB/s)

throughput-write.png

Read

Readにおいては、DDLZ4が全域で良いスループットを実現しています。
次点でDDZstd1, DDZstd5ですが、両者はReadにおいて差異はないようです(ちなみにDecompressでも同様の傾向になっています)

また、102400Bytes以降も測定すると、SnappyはDDZstd1, DDZstd5と同等のスループットまでは追いつきます。

表: 各フォーマットのReadスループット。単位はMiB/s
すべてのデータセットについて5回計測し、すべての計測の平均値を採用しています。

Data size GzipBestSpeed GzipDefault ZlibBestSpeed ZlibDefault snappy DDZstd1 DDZstd5 DDLZ4
102 6.0 5.7 5.4 5.1 2.0 17.3 17.2 37.3
256 8.6 8.9 8.1 8.4 4.4 25.8 28.0 91.2
512 13.2 14.4 12.8 13.7 8.3 48.3 52.7 175.6
1024 23.3 19.0 22.5 18.3 14.6 92.4 87.6 301.6
2560 41.7 25.8 38.8 24.8 31.6 155.7 154.9 585.5
5120 62.6 29.8 59.3 28.6 54.3 244.7 231.7 817.8
10240 100.6 33.1 83.3 31.7 94.1 372.9 351.2 1020.0
25600 192.4 40.6 128.7 38.0 169.4 580.0 553.3 1265.6
51200 288.6 54.5 160.1 49.3 237.4 738.8 705.2 1417.1
102400 395.1 78.4 186.3 68.0 343.5 836.2 804.4 1435.3

図:Readのスループット
横軸: 圧縮・解凍するデータサイズ(Bytes)、縦軸: スループット(MiB/s)

throughput-read.png

Compress, Decompress

Write, Readと同様にCompress, Decompressも対応しているフォーマットについては計測しています。
snappy(github.com/golang/snappy)はCompress, DecompressのAPIがないため、KlauspostSnappy(
github.com/klauspost/compress/snappy)を使用しています。

全体としてWriteに比べCompressは3倍程度速く、Readに比べDecompressは2倍程度速い傾向にあります。
処理するデータサイズがある程度固定出来る等、条件が合う場合はCompress, Decompressを使用することでスループットを向上させることが出来るかもしれません。

フォーマット毎の順序はWrite, Readと大まかには変わらず、DDLZ4が比較的良好な結果となっていて、次点でDDZstd1, DDZstd5となっています。
ただし、以下の特筆すべき点があります

  • DDLZ4が小さなデータに対してスループットの悪化が抑えられている
  • DecompressにおいてKlauspostSnappyが非常に良好な結果を残している

図:Compressのスループット
横軸: 圧縮・解凍するデータサイズ(Bytes)、縦軸: スループット(MiB/s)

throughput-comp.png

図:Decompressのスループット
横軸: 圧縮・解凍するデータサイズ(Bytes)、縦軸: スループット(MiB/s)

throughput-decomp.png

アーキテクチャによる違い

ライブラリはアーキテクチャ毎に実装を持っている場合があるため、アーキテクチャが変われば性能が変わる可能性があります。
今回はlinux/amd64の環境を基本的に使用していますが、別途とdarwin/arm64の2つの環境でベンチマークを行いました。

darwin/arm64環境

  • Apple MacBook Pro (13-inch, M2, 2023)
    • OS: macOS Sonoma
    • CPU: Apple M2 Pro (3.3GHz - 3.7GHz)
  • Go
    • go version go1.21.3 darwin/arm64

darwin/arm64環境での結果

linux/amd64環境と比較して、CPUの単コア性能はクロック等からも高いことが想定され、実際に全体的に高い結果になっています。
ただ、今回は環境の性能を測定するのが目的ではないので、相対的などのような変化があったかを確認し、以下のような差異がありました。

  • 特にlinux/amd64で最も良好な結果を残したDDLZ4がdarwin/arm64ではReadにおいてあまり良い結果を残していない
  • Snappyはdarwin/arm64の方が相対的に良好な結果を残している

原因についてまで深掘りをしていませんが、例えばliblz4はARM用のコードがそれほど最適化されていない等などが考えられます。

表: 圧縮率とスループット(darwin/arm64)

名前 圧縮率 Writeスループット(MiB/s) Readスループット(MiB/s)
DDLZ4 0.44 825.1 114.6
DDZstd1 0.54 548.6 487.3
DDZstd10 0.57 29.6 117.5
DDZstd20 0.58 1.1 142.3
DDZstd5 0.57 57.0 130.7
GzipBestCompression 0.57 46.9 78.9
GzipBestSpeed 0.55 60.3 358.4
GzipDefault 0.57 50.1 78.9
KlauspostGzipDefault 0.57 71.3 122.5
KlauspostSnappy 0.47 255.1 695.9
KlauspostZstdBest 0.57 0.5 200.4
KlauspostZstdBetter 0.56 5.3 210.9
KlauspostZstdDefault 0.56 16.4 207.5
KlauspostZstdFastest 0.55 36.7 222.2
LZ4-1 0.45 26.2 27.2
s2 0.43 191.0 129.2
snappy 0.45 500.7 519.2
ZlibBestComp 0.58 46.3 75.1
ZlibBestSpeed 0.55 59.5 243.3
ZlibDefault 0.58 49.1 75.1

図:Writeの圧縮率-スループット (darwin/arm64)
横軸: 圧縮率(0.0=無圧縮), 縦軸: スループット(MiB/s)

ratio-thruput-write-darwin.png

図:Readの圧縮率-スループット (darwin/arm64)
横軸: 圧縮率(0.0=無圧縮), 縦軸: スループット(MiB/s)

ratio-thruput-read-darwin.png

実装による違い

同じフォーマット、大体同じパラメータでも実装が違うと大幅にスループットが異なるケースがあります。
詳細な値まで記載しませんが、いくつか抽出してグラフを掲載します。
データサイズは102400Bytes(100KiB)で統一されていますが、すべてのデータセットにおいてスループットを算出して、その平均を取って比較しています。

Gzip

標準のGzipよりもgithub.com/klauspost/compress/gzipの方がスループットが少し良い結果となっています。

impl-gzip.png

ちなみに、このグラフはどちらもgzip.BestSpeed(level=1)で比較しています。gzip.DefaultCompressionを使う場合は注意が必要で、
(github.com/klauspost/compress/gzip).DefaultCompressionはLevel=6で
(compress/gzip).DefaultCompressionはLevel=5です。
なので、gzip.DefaultCompressionで比較するとcompress/gzipの方が速いですがその理由は圧縮パラメータの差にあります。

Zstd

github.com/DataDog/zstdの方がgithub.com/klauspost/compress/zstdよりもスループットが良い結果となっています。
github.com/DataDog/zstdおそらく元々のzstdの実装を持っているので速いと思いますが、github.com/klauspost/compress/zstdもアセンブラのコード自体はあるものの、速度には差が出てしまっている状態です。

impl-zstd.png

Snappy

Writeにおいてのみ、github.com/golang/snappyの方がgithub.com/klauspost/compress/snappyよりもスループットが良い結果となっています。

impl-snappy.png

LZ4

github.com/DataDog/golz4の方がgithub.com/pierrec/lz4よりもスループットが良い結果となっています。
かなり差が開いているので、LZ4の"スループットを求める"という特徴を考えれば、プロダクションで動かすコードはgithub.com/DataDog/golz4を採用したいところです。

impl-lz4.png

最後に

圧縮という基礎的ジャンルですが、改めてライブラリや実装は一つ一つ丁寧に検証することの重要さを再認識しました。
以上です。ライブラリやフォーマット選定の一助になれば幸いです。

4
1
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
4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?