🎨

Go 1.26で生まれ変わるimage/jpegパッケージ

に公開

2026年2月リリース予定のGo 1.26で、image/jpegパッケージの内部実装が大きく更新されます。この変更により後方互換性が崩れることはほぼないのですが、その裏側には興味深い変更が加えられていました。

本記事では、image/jpegパッケージがなぜ更新されるのか、何が改善されるのかを紹介します。

ライセンス問題

今回の更新の最大の動機は、長年存在していたライセンス問題の解決です。

image/jpegパッケージの中核をなすfdct.go(離散コサイン変換)とidct.go(逆離散コサイン変換)は、それぞれ IJG (Independent JPEG Group)MPEG-SSG(MPEG Software Simulation Group) のコードをベースにしています。

https://github.com/golang/go/blob/release-branch.go1.25/src/image/jpeg/fdct.go#L10

https://github.com/golang/go/blob/release-branch.go1.25/src/image/jpeg/idct.go#L13

そのため、これらのファイルにはGo本体のライセンス(The 3-Clause BSD License)とは異なるライセンスが付与されていました。Goチーム自身がCLでこの問題に言及しています。

The fdct.go and idct.go files were derived from the MPEG-SSG and JPEG-IJG reference code and therefore carry licenses specific to those groups. Various license checkers flag these files as potentially problematic.
fdct.goidct.goはMPEG-SSGとJPEG-IJGのリファレンスコードから派生したため、それらのグループ固有のライセンスが適用されています。多くのライセンスチェッカーがこれらのファイルを問題視しています。

https://go-review.googlesource.com/c/go/+/705518

この「ライセンスチェッカーに引っかかる」状態は、Goを利用する企業にとって深刻な問題でした。多くの企業は法務・コンプライアンスの観点から、自社製品に組み込むソフトウェアのライセンスを厳格に管理しています。

このような企業がGoを採用し、ライセンスチェッカーで自社製品をスキャンすると、Goの標準ライブラリであるimage/jpegパッケージから、許可されていないライセンスが検出されてしまうのです。

これが、今回の修正に至った直接的な理由でした。

更新による改善点

今回の更新はただ単にライセンス問題を解決しただけでなく、いくつか改善されたポイントがあります。

1. パフォーマンスの向上

新実装は旧実装より高速化されています。Goチームは以下の通りベンチマーク結果を公開していました。

FDCT IDCT
Apple M3 Pro(darwin/arm64) -5.34% -16.54%
Intel Xeon(linux/amd64) -15.11% -20.22%

https://go-review.googlesource.com/c/go/+/705518

なぜ速くなったか

興味深いことに、旧実装も新実装も、同じLoefflerアルゴリズムをベースにしています。 Go 1.25までのfdct.goが参考にしていたIJGのFDCTの実装jfdctint.cには、ヘッダーコメントに以下の記述があります。

This implementation is based on an algorithm described in C. Loeffler, A. Ligtenberg and G. Moschytz, "Practical Fast 1-D DCT Algorithms with 11 Multiplications", Proc. Int'l. Conf. on Acoustics, Speech, and Signal Processing 1989 (ICASSP '89), pp. 988-991.

これにより、性能差は「アルゴリズムの違い」ではなく、同じアルゴリズムの実装方法の違いから来ていることが分かります。

実装方法の違いがもたらす性能差

データ依存グラフの構造が異なる

旧実装では、同じ変数名が計算の途中で全く異なる意味に変わり、前の値との関係性が失われるような実装になっています。この複雑な依存関係により、CPUは次の計算を開始する前に他の計算の完了を待つ必要があります。

// 旧実装:同じ変数が異なる意味の計算で繰り返し再利用される

tmp10 := tmp0 + tmp3
tmp12 := tmp0 - tmp3
tmp11 := tmp1 + tmp2
tmp13 := tmp1 - tmp2

s[0] = (tmp10 + tmp11 - 8*centerJSample) << pass1Bits
s[4] = (tmp10 - tmp11) << pass1Bits
z1 := (tmp12 + tmp13) * fix_0_541196100
z1 += 1 << (constBits - pass1Bits - 1)
s[2] = (z1 + tmp12*fix_0_765366865) >> (constBits - pass1Bits)
s[6] = (z1 - tmp13*fix_1_847759065) >> (constBits - pass1Bits)

tmp10 = tmp0 + tmp3  // tmp10を全く異なる計算で再利用
tmp11 = tmp1 + tmp2  // tmp11を全く異なる計算で再利用
tmp12 = tmp0 + tmp2  // tmp12を全く異なる計算で再利用
tmp13 = tmp1 + tmp3  // tmp13を全く異なる計算で再利用
z1 = (tmp12 + tmp13) * fix_1_175875602  // z1も異なる計算で再利用
z1 += 1 << (constBits - pass1Bits - 1)
tmp0 *= fix_1_501321110  // tmp0を更新
tmp1 *= fix_3_072711026  // tmp1を更新
// ...
実際のソースコード

対照的に、新実装は4ステージ構造で、データが一方向に流れます。

// 新実装:4ステージで一方向にデータが変換される

// ステージ1: バタフライ演算
x0, x7 = x0+x7, x0-x7
x1, x6 = x1+x6, x1-x6
x2, x5 = x2+x5, x2-x5
x3, x4 = x3+x4, x3-x4

// ステージ2: 回転演算とバタフライ
// ステージ1の結果を変換
x4, x7 = dctBox(x4, x7, cos3, sin3)
x5, x6 = dctBox(x5, x6, cos1, sin1)
x0, x3 = x0+x3, x0-x3
x1, x2 = x1+x2, x1-x2

// ステージ3: 回転演算とバタフライ
// ステージ2の結果を変換
x2, x3 = dctBox(x2, x3, sqrt2_cos6, sqrt2_sin6)
x0, x1 = x0+x1, x0-x1
x4, x6 = x4+x6, x4-x6
x7, x5 = x7+x5, x7-x5

// ステージ4: スケーリングとバタフライ
// ステージ3の結果を変換
x5 *= sqrt2
x6 *= sqrt2
x7, x4 = x7+x4, x7-x4
実際のソースコード

新実装では各変数が前のステージの結果を受け取り、次のステージへ渡す明確なデータフローを持ちます。各ステージ内の多くの演算が並列実行可能で、計算が一方向に進むため、CPUのアウトオブオーダー実行を最大限活用できます。旧実装の「変数名を使い回して異なる計算をする」パターンは、現代CPUの命令レベル並列実行(ILP)を阻害します。

興味深いことに、旧実装は8点DCTあたり10回の乗算、新実装は11回の乗算を使用しています。乗算が増えているにもかかわらず最大20%高速なのは、並列実行可能な命令が増えたためです。現代のCPUでは「演算の種類や回数」よりも「データ依存の深さ」が性能を左右します。

2. 計算精度の向上

パフォーマンスに加えて重要なのが計算精度の向上です。新しい実装では、計算結果の丸め誤差が大幅に減少しました。エンコード時(FDCT)の誤差発生率は8.6%から2.5%に、デコード時(IDCT)の誤差発生率は1.4%から1.2%に改善されています。この精度向上により、JPEGを繰り返し保存した際の画質劣化が抑制され、より元データに忠実な処理が可能になります。

3. 信頼性の高いテストによる品質保証

この高い計算精度は、厳密なテストプロセスで保証されています。Goチームは、速度を度外視して数学的正確さを追求した「真の正解」を計算するslowerFDCTというリファレンス実装を用意しました。それと簡易比較用のslowFDCTと比べて正しさを担保し、その上で本番用のfdctをテストする二重チェック機構を構築しています(IDCTも同様)。

この厳密なテストプロセスの過程で、驚くべき事実が明らかになりました。元々のリファレンス実装自体に、丸め処理(rounding)のバグが含まれていたのです。このバグにより、テストの基準となる「正解」自体が不正確だったため、本番コードとの誤差を「±2以内」という緩い基準で許容せざるを得ない状態でした。

Russ Cox氏が新しい実装を開発する過程でこの問題に気づき、リファレンス実装を修正した結果、コードを一切変更していないにもかかわらず、本番実装の誤差が「±1以内」に改善されることが判明しました。「テストコード自体のバグを発見・修正する」という、極めて地道で丁寧な作業を経て、初めて真に信頼できるテスト基盤が完成したのです。

https://go-review.googlesource.com/c/go/+/705517

4. 可読性とメンテナンス性の向上

コード自体の品質も向上しています。コミットメッセージには、新しいコードは「より可読性が高い(more readable)」と記されています。その証拠に、変数名の明確化や、計算手順や保証できる計算精度の証明にいたるまでコード中で示されています。複雑で文書化も不十分だった古いコードが、Goのスタイルに沿ったクリーンなコードに生まれ変わりました。

まとめ

Go 1.26で予定されているimage/jpegパッケージの更新では、ライセンス問題が解決されました。さらにそれを解決する過程で、より速く、より精度高く、より可読性の高いコードにすることができました。

Discussion