[インデックス 14042] ファイルの概要
このコミットは、Go言語の標準ライブラリ image/jpeg
パッケージにおけるJPEG画像のDCT(離散コサイン変換)およびIDCT(逆離散コサイン変換)処理に関する改善とテスト追加を目的としています。具体的には、IDCTの実装において共通部分式除去(Common Sub-expression Elimination, CSE)による最適化が施され、その効果を検証するためのベンチマークと、DCT/IDCTの正確性を検証するための新しいテストが追加されています。
コミット
commit 12e343f372c03b6fec9d5da6cd83833c79812bc9
Author: Nigel Tao <nigeltao@golang.org>
Date: Sun Oct 7 10:21:17 2012 +1100
image/jpeg: add DCT tests, do a small optimization (common sub-expression
elimination) in idct.go.
benchmark old ns/op new ns/op delta
BenchmarkIDCT 5649 5610 -0.69%
BenchmarkDecodeRGBOpaque 2948607 2941051 -0.26%
The "type block" declaration moved so that idct.go is compilable
as a stand-alone file: "go tool 6g -S idct.go" works.
R=r
CC=golang-dev
https://golang.org/cl/6619056
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/12e343f372c03b6fec9d5da6cd83833c79812bc9
元コミット内容
image/jpeg
パッケージにDCTテストを追加し、idct.go
内で共通部分式除去(Common Sub-expression Elimination)による小さな最適化を行いました。
ベンチマーク結果は以下の通りです。
BenchmarkIDCT
: 5649 ns/op から 5610 ns/op へ -0.69% の改善BenchmarkDecodeRGBOpaque
: 2948607 ns/op から 2941051 ns/op へ -0.26% の改善
type block
の宣言が移動され、idct.go
がスタンドアロンファイルとしてコンパイル可能になりました(go tool 6g -S idct.go
が動作するようになります)。
変更の背景
このコミットの背景には、JPEGデコード処理のパフォーマンス向上と、その正確性を保証するためのテストカバレッジの拡充があります。
- パフォーマンス最適化: JPEG画像のデコードにおいて、IDCT(逆離散コサイン変換)は計算負荷の高い処理の一つです。この処理の効率を改善することは、JPEGデコード全体の速度向上に直結します。共通部分式除去は、コンパイラ最適化の基本的な手法であり、手動で適用することで、より効率的なコード生成を促すことができます。特に、Go言語の初期のコンパイラでは、このような低レベルな最適化を手動で行うことがパフォーマンス改善に寄与するケースがありました。
- テストカバレッジの拡充: 画像処理ライブラリにおいて、変換アルゴリズムの正確性は非常に重要です。DCTとIDCTは互いに逆変換の関係にあり、これらの処理が正しく機能していることを検証するテストは不可欠です。既存のテストが不十分であった場合、新たなテストを追加することで、将来的な変更によるデグレードを防ぎ、コードの信頼性を高めることができます。
- モジュール性の向上:
type block
の宣言をidct.go
に移動することで、idct.go
ファイルが他のファイルに依存することなく単独でコンパイルできるようになります。これは、開発者が個々のコンポーネントを独立してテストしたり、デバッグしたりする際に役立ち、コードベース全体のモジュール性と保守性を向上させます。
これらの変更は、Go言語の標準ライブラリとしての image/jpeg
パッケージの品質と性能を向上させるための継続的な取り組みの一環として行われました。
前提知識の解説
JPEG圧縮とDCT/IDCT
JPEG(Joint Photographic Experts Group)は、主に写真などの連続階調画像を圧縮するための標準的な方式です。JPEG圧縮は、人間の視覚特性を利用した非可逆圧縮であり、高い圧縮率と比較的良好な画質を両立させます。
JPEG圧縮の主要なステップには、以下のものが含まれます。
- 色空間変換: RGB画像をYCrCb色空間に変換します。Yは輝度(明るさ)、CrとCbは色差(色の情報)を表します。人間の目は輝度情報に敏感で、色差情報には比較的鈍感であるため、色差情報を間引く(サブサンプリング)ことで圧縮率を高めます。
- ブロック分割: 各Y, Cr, Cb成分を8x8ピクセルのブロックに分割します。
- レベルシフト: 各ブロックのピクセル値(0-255)を-128から127の範囲にシフトします。これはDCTの計算を容易にするためです。
- DCT(Discrete Cosine Transform - 離散コサイン変換): 各8x8ブロックに対してDCTを適用します。DCTは、空間領域のピクセルデータを周波数領域の係数に変換する数学的な操作です。低周波成分(画像の滑らかな変化)はブロックの左上隅に集中し、高周波成分(画像の細かいディテール)は右下隅に分布します。人間の視覚は高周波成分に鈍感であるため、この変換によって高周波成分を効率的に間引くことが可能になります。
- DCTの出力は、DC成分(ブロック全体の平均輝度)と63個のAC成分(周波数成分)からなる8x8の係数ブロックです。
- 量子化: DCT係数を量子化テーブルで割って丸めます。これにより、高周波成分の多くがゼロになり、データ量を大幅に削減できます。このステップが非可逆圧縮の主要な原因です。量子化テーブルは、画質と圧縮率のバランスを調整するために使用されます。
- エントロピー符号化: 量子化された係数をハフマン符号化や算術符号化などのエントロピー符号化によってさらに圧縮します。
JPEGデコードは、これらのステップを逆に行います。
- エントロピー復号化: 符号化されたデータを復号し、量子化されたDCT係数を取得します。
- 逆量子化: 量子化された係数に量子化テーブルを掛けて、元のDCT係数に近い値に戻します。
- IDCT(Inverse Discrete Cosine Transform - 逆離散コサイン変換): 逆量子化されたDCT係数に対してIDCTを適用し、周波数領域の係数を空間領域のピクセルデータに戻します。これにより、8x8ピクセルの画像ブロックが再構築されます。
- レベルシフト解除: ピクセル値を元の0-255の範囲に戻します。
- 色空間逆変換: YCrCb画像をRGB画像に戻します。
共通部分式除去 (Common Sub-expression Elimination, CSE)
共通部分式除去は、コンパイラ最適化の一種で、同じ値に評価される式が複数回出現する場合に、その式を一度だけ計算し、その結果を再利用することで計算量を削減する手法です。
例えば、以下のようなコードがあるとします。
a = (b * c) + d
e = (b * c) + f
ここで (b * c)
は共通部分式です。CSEを適用すると、コンパイラは b * c
を一度だけ計算し、その結果を一時変数に格納し、その一時変数を a
と e
の計算の両方で再利用します。
temp = b * c
a = temp + d
e = temp + f
これにより、乗算の回数を減らし、プログラムの実行速度を向上させることができます。特に、ループ内で共通部分式が出現する場合、その効果は大きくなります。
このコミットでは、idct.go
内の計算で、繰り返し出現する src[y*8+x]
のような配列アクセスが共通部分式として認識され、y8 := y * 8
や yStride := y * stride
のように一時変数に格納することで、インデックス計算の繰り返しを避けています。
技術的詳細
このコミットは、主に image/jpeg
パッケージ内のIDCT実装の最適化とテストの追加に焦点を当てています。
IDCTの最適化 (idct.go
)
idct.go
ファイルでは、IDCTアルゴリズムが実装されています。IDCTは、JPEGデコードにおいて周波数領域の係数から空間領域のピクセル値を再構築する重要なステップです。このコミットでは、IDCT関数の内部で、配列アクセスにおける共通部分式除去が行われています。
具体的には、以下の変更が加えられています。
-
y*8
の共通化: 変更前:src[y*8+1]
,src[y*8+2]
, ...,src[y*8+7]
のように、ループ内でy*8
の計算が繰り返し行われていました。 変更後:y8 := y * 8
という一時変数を導入し、src[y8+1]
,src[y8+2]
のようにy8
を再利用することで、乗算の回数を削減しています。これは、特にループの各イテレーションで同じベースアドレス計算が繰り返される場合に有効です。 -
y*stride
の共通化: 変更前:dst[y*stride+x]
のように、ループ内でy*stride
の計算が繰り返し行われていました。 変更後:yStride := y * stride
という一時変数を導入し、dst[yStride+x]
のようにyStride
を再利用することで、乗算の回数を削減しています。
これらの変更は、Goコンパイラが当時、このような単純な共通部分式を自動的に最適化しない可能性があったため、手動で適用されたものと考えられます。これにより、わずかながらもパフォーマンスの向上が見られました(ベンチマーク結果の-0.69%および-0.26%)。
DCTテストの追加 (dct_test.go
)
新しく追加された src/pkg/image/jpeg/dct_test.go
ファイルは、DCTおよびIDCTの実装の正確性を検証するための包括的なテストスイートを提供します。
主なテスト内容は以下の通りです。
-
BenchmarkFDCT
とBenchmarkIDCT
:fdct
(Forward DCT) とidct
(Inverse DCT) 関数のパフォーマンスを測定するためのベンチマークです。testBlocks
という事前定義されたブロックデータを使用して、実際の処理に近い条件でベンチマークを実行します。
-
TestDCT
:- DCTとIDCTの正確性を検証する主要なテスト関数です。
- FDCTとIDCTの逆変換性の検証:
- ランダムに生成されたブロックを含む
testBlocks
を使用します。 - 元のブロックにレベルシフトとスケーリングを適用し、
slowFDCT
(遅いFDCT実装) を実行し、その後slowIDCT
(遅いIDCT実装) を実行します。 - 結果を元のブロックと比較し、
differ
関数(許容誤差を考慮)を使って差異がないことを確認します。これにより、FDCTとIDCTが互いに逆変換であることを検証します。
- ランダムに生成されたブロックを含む
- 最適化されたFDCTと遅いFDCTの一致性の検証:
fdct
(最適化されたFDCT) の結果とslowFDCT
の結果を比較し、両者が一致することを確認します。
- 最適化されたIDCTと遅いIDCTの一致性の検証:
idct
(最適化されたIDCT) の結果とslowIDCT
の結果を比較し、両者が一致することを確認します。
slowFDCT
とslowIDCT
は、DCT/IDCTの数学的定義に忠実な、より直接的な(しかし非効率な)実装であり、最適化された実装の参照として機能します。
-
differ
関数:- 2つのブロック間の差異をチェックするヘルパー関数です。
- JPEGのIDCTは浮動小数点演算と丸め誤差を含むため、厳密な一致ではなく、±2の許容誤差を設けています。これは、異なるIDCT実装間でわずかな差異が生じることが許容されるためです。
-
alpha
関数:- DCT/IDCTの計算で使用される係数
alpha(i)
を計算するヘルパー関数です。i=0
の場合は1、それ以外の場合は√2
を返します。
- DCT/IDCTの計算で使用される係数
-
slowFDCT
とslowIDCT
関数:- DCT/IDCTの数学的な定義に基づいた、直接的で理解しやすいが、最適化されていない実装です。これらは、最適化された
fdct
およびidct
関数の参照実装として、テストの正確性検証に用いられます。
- DCT/IDCTの数学的な定義に基づいた、直接的で理解しやすいが、最適化されていない実装です。これらは、最適化された
-
block
型の移動:src/pkg/image/jpeg/reader.go
からsrc/pkg/image/jpeg/idct.go
へtype block [blockSize]int
の宣言が移動されました。これにより、idct.go
がスタンドアロンでコンパイル可能になり、開発やテストの柔軟性が向上します。
ベンチマークの追加 (writer_test.go
)
src/pkg/image/jpeg/writer_test.go
に BenchmarkDecodeRGBOpaque
が追加されました。これは、JPEGデコード処理全体のパフォーマンスを測定するためのベンチマークであり、IDCTの最適化がデコード処理全体に与える影響を評価するために使用されます。
コアとなるコードの変更箇所
src/pkg/image/jpeg/idct.go
--- a/src/pkg/image/jpeg/idct.go
+++ b/src/pkg/image/jpeg/idct.go
@@ -37,6 +37,10 @@ package jpeg
*
*/
+const blockSize = 64 // A DCT block is 8x8.
+
+type block [blockSize]int
+
const (
w1 = 2841 // 2048*sqrt(2)*cos(1*pi/16)
w2 = 2676 // 2048*sqrt(2)*cos(2*pi/16)
@@ -70,30 +74,31 @@ const (
func idct(dst []byte, stride int, src *block) {
// Horizontal 1-D IDCT.
for y := 0; y < 8; y++ {\n+\t\ty8 := y * 8
\t// If all the AC components are zero, then the IDCT is trivial.
-\t\tif src[y*8+1] == 0 && src[y*8+2] == 0 && src[y*8+3] == 0 &&
-\t\t\tsrc[y*8+4] == 0 && src[y*8+5] == 0 && src[y*8+6] == 0 && src[y*8+7] == 0 {
-\t\t\tdc := src[y*8+0] << 3
-\t\t\tsrc[y*8+0] = dc
-\t\t\tsrc[y*8+1] = dc
-\t\t\tsrc[y*8+2] = dc
-\t\t\tsrc[y*8+3] = dc
-\t\t\tsrc[y*8+4] = dc
-\t\t\tsrc[y*8+5] = dc
-\t\t\tsrc[y*8+6] = dc
-\t\t\tsrc[y*8+7] = dc
+\t\tif src[y8+1] == 0 && src[y8+2] == 0 && src[y8+3] == 0 &&
+\t\t\tsrc[y8+4] == 0 && src[y8+5] == 0 && src[y8+6] == 0 && src[y8+7] == 0 {
+\t\t\tdc := src[y8+0] << 3
+\t\t\tsrc[y8+0] = dc
+\t\t\tsrc[y8+1] = dc
+\t\t\tsrc[y8+2] = dc
+\t\t\tsrc[y8+3] = dc
+\t\t\tsrc[y8+4] = dc
+\t\t\tsrc[y8+5] = dc
+\t\t\tsrc[y8+6] = dc
+\t\t\tsrc[y8+7] = dc
\t\tcontinue
\t}
\t// Prescale.
-\t\tx0 := (src[y*8+0] << 11) + 128
-\t\tx1 := src[y*8+4] << 11
-\t\tx2 := src[y*8+6]
-\t\tx3 := src[y*8+2]
-\t\tx4 := src[y*8+1]
-\t\tx5 := src[y*8+7]
-\t\tx6 := src[y*8+5]
-\t\tx7 := src[y*8+3]
+\t\tx0 := (src[y8+0] << 11) + 128
+\t\tx1 := src[y8+4] << 11
+\t\tx2 := src[y8+6]
+\t\tx3 := src[y8+2]
+\t\tx4 := src[y8+1]
+\t\tx5 := src[y8+7]
+\t\tx6 := src[y8+5]
+\t\tx7 := src[y8+3]
\t// Stage 1.
\tx8 := w7 * (x4 + x5)
@@ -123,14 +128,14 @@ func idct(dst []byte, stride int, src *block) {
\tx4 = (r2*(x4-x5) + 128) >> 8
\t// Stage 4.
-\t\tsrc[8*y+0] = (x7 + x1) >> 8
-\t\tsrc[8*y+1] = (x3 + x2) >> 8
-\t\tsrc[8*y+2] = (x0 + x4) >> 8
-\t\tsrc[8*y+3] = (x8 + x6) >> 8
-\t\tsrc[8*y+4] = (x8 - x6) >> 8
-\t\tsrc[8*y+5] = (x0 - x4) >> 8
-\t\tsrc[8*y+6] = (x3 - x2) >> 8
-\t\tsrc[8*y+7] = (x7 - x1) >> 8
+\t\tsrc[y8+0] = (x7 + x1) >> 8
+\t\tsrc[y8+1] = (x3 + x2) >> 8
+\t\tsrc[y8+2] = (x0 + x4) >> 8
+\t\tsrc[y8+3] = (x8 + x6) >> 8
+\t\tsrc[y8+4] = (x8 - x6) >> 8
+\t\tsrc[y8+5] = (x0 - x4) >> 8
+\t\tsrc[y8+6] = (x3 - x2) >> 8
+\t\tsrc[y8+7] = (x7 - x1) >> 8
}\n \n // Vertical 1-D IDCT.
@@ -189,8 +194,10 @@ func idct(dst []byte, stride int, src *block) {
// Level shift by +128, clip to [0, 255], and write to dst.
for y := 0; y < 8; y++ {\n+\t\ty8 := y * 8
+\t\tyStride := y * stride
\tfor x := 0; x < 8; x++ {\n-\t\t\tc := src[y*8+x]
+\t\t\tc := src[y8+x]
\t\tif c < -128 {
\t\t\tc = 0
\t\t} else if c > 127 {
@@ -198,7 +205,7 @@ func idct(dst []byte, stride int, src *block) {
\t\t} else {
\t\t\tc += 128
\t\t}
-\t\t\tdst[y*stride+x] = uint8(c)
+\t\t\tdst[yStride+x] = uint8(c)
\t}
}
}
src/pkg/image/jpeg/reader.go
--- a/src/pkg/image/jpeg/reader.go
+++ b/src/pkg/image/jpeg/reader.go
@@ -35,11 +35,7 @@ type component struct {
tq uint8 // Quantization table destination selector.
}
-type block [blockSize]int
-
const (
-\tblockSize = 64 // A DCT block is 8x8.
-\n dcTable = 0
acTable = 1
maxTc = 1
src/pkg/image/jpeg/dct_test.go
(新規ファイル)
--- /dev/null
+++ b/src/pkg/image/jpeg/dct_test.go
@@ -0,0 +1,297 @@
+// Copyright 2012 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package jpeg
+
+import (
+ "bytes"
+ "fmt"
+ "math"
+ "math/rand"
+ "testing"
+)
+
+func BenchmarkFDCT(b *testing.B) {
+ b.StopTimer()
+ blocks := make([]block, 0, b.N*len(testBlocks))
+ for i := 0; i < b.N; i++ {
+ blocks = append(blocks, testBlocks[:]...)
+ }
+ b.StartTimer()
+ for i := range blocks {
+ fdct(&blocks[i])
+ }
+}
+
+func BenchmarkIDCT(b *testing.B) {
+ b.StopTimer()
+ dummy := make([]byte, 64)
+ blocks := make([]block, 0, b.N*len(testBlocks))
+ for i := 0; i < b.N; i++ {
+ blocks = append(blocks, testBlocks[:]...)
+ }
+ b.StartTimer()
+ for i := range blocks {
+ idct(dummy, 8, &blocks[i])
+ }
+}
+
+func TestDCT(t *testing.T) {
+ blocks := make([]block, len(testBlocks))
+ copy(blocks, testBlocks[:])
+
+ // Append some randomly generated blocks of varying sparseness.
+ r := rand.New(rand.NewSource(123))
+ for i := 0; i < 100; i++ {
+ b := block{}
+ n := r.Int() % 64
+ for j := 0; j < n; j++ {
+ b[r.Int()%len(b)] = r.Int() % 256
+ }
+ blocks = append(blocks, b)
+ }
+
+ // Check that the FDCT and IDCT functions are inverses, after a scale and
+ // level shift. Scaling reduces the rounding errors in the conversion from
+ // floats to ints.
+ for i, b := range blocks {
+ got, want := b, b
+ for j := range got {
+ got[j] = (got[j] - 128) * 8
+ }
+ slowFDCT(&got)
+ slowIDCT(&got)
+ for j := range got {
+ got[j] = got[j]/8 + 128
+ }
+ if differ(&got, &want) {
+ t.Errorf("i=%d: IDCT(FDCT)\nsrc\n%s\ngot\n%s\nwant\n%s\n", i, &b, &got, &want)
+ }
+ }
+
+ // Check that the optimized and slow FDCT implementations agree.
+ // The fdct function already does a scale and level shift.
+ for i, b := range blocks {
+ got, want := b, b
+ fdct(&got)
+ for j := range want {
+ want[j] = (want[j] - 128) * 8
+ }
+ slowFDCT(&want)
+ if differ(&got, &want) {
+ t.Errorf("i=%d: FDCT\nsrc\n%s\ngot\n%s\nwant\n%s\n", i, &b, &got, &want)
+ }
+ }
+
+ // Check that the optimized and slow IDCT implementations agree.
+ dummy := make([]byte, 64)
+ for i, b := range blocks {
+ got, want := b, b
+ idct(dummy, 8, &got)
+ slowIDCT(&want)
+ if differ(&got, &want) {
+ t.Errorf("i=%d: IDCT\nsrc\n%s\ngot\n%s\nwant\n%s\n", i, &b, &got, &want)
+ }
+ }
+}
+
+// differ returns whether any pair-wise elements in b0 and b1 differ by 2 or
+// more. That tolerance is because there isn't a single definitive decoding of
+// a given JPEG image, even before the YCbCr to RGB conversion; implementations
+// can have different IDCT rounding errors.
+func differ(b0, b1 *block) bool {
+ for i := range b0 {
+ delta := b0[i] - b1[i]
+ if delta < -2 || +2 < delta {
+ return true
+ }
+ }
+ return false
+}
+
+// alpha returns 1 if i is 0 and returns √2 otherwise.
+func alpha(i int) float64 {
+ if i == 0 {
+ return 1
+ }
+ return math.Sqrt2
+}
+
+// slowFDCT performs the 8*8 2-dimensional forward discrete cosine transform:
+//
+// dst[u,v] = (1/8) * Σ_x Σ_y alpha(u) * alpha(v) * src[x,y] *
+// cos((π/2) * (2*x + 1) * u / 8) *
+// cos((π/2) * (2*y + 1) * v / 8)
+//
+// x and y are in pixel space, and u and v are in transform space.
+//
+// b acts as both dst and src.
+func slowFDCT(b *block) {
+ var dst [blockSize]float64
+ for v := 0; v < 8; v++ {
+ for u := 0; u < 8; u++ {
+ sum := 0.0
+ for y := 0; y < 8; y++ {
+ for x := 0; x < 8; x++ {
+ sum += alpha(u) * alpha(v) * float64(b[8*y+x]) *
+ math.Cos(math.Pi*float64((2*x+1)*u)/16) *
+ math.Cos(math.Pi*float64((2*y+1)*v)/16)
+ }
+ }
+ dst[8*v+u] = sum / 8
+ }
+ }
+ // Convert from float64 to int.
+ for i := range dst {
+ b[i] = int(dst[i] + 0.5)
+ }
+}
+
+// slowIDCT performs the 8*8 2-dimensional inverse discrete cosine transform:
+//
+// dst[x,y] = (1/8) * Σ_u Σ_v alpha(u) * alpha(v) * src[u,v] *
+// cos((π/2) * (2*x + 1) * u / 8) *
+// cos((π/2) * (2*y + 1) * v / 8)
+//
+// x and y are in pixel space, and u and v are in transform space.
+//
+// b acts as both dst and src.
+func slowIDCT(b *block) {
+ var dst [blockSize]float64
+ for y := 0; y < 8; y++ {
+ for x := 0; x < 8; x++ {
+ sum := 0.0
+ for v := 0; v < 8; v++ {
+ for u := 0; u < 8; u++ {
+ sum += alpha(u) * alpha(v) * float64(b[8*v+u]) *
+ math.Cos(math.Pi*float64((2*x+1)*u)/16) *
+ math.Cos(math.Pi*float64((2*y+1)*v)/16)
+ }
+ }
+ dst[8*y+x] = sum / 8
+ }
+ }
+ // Convert from float64 to int.
+ for i := range dst {
+ b[i] = int(dst[i] + 0.5)
+ }
+}
+
+func (b *block) String() string {
+ s := bytes.NewBuffer(nil)
+ fmt.Fprintf(s, "{\n")
+ for y := 0; y < 8; y++ {
+ fmt.Fprintf(s, "\t")
+ for x := 0; x < 8; x++ {
+ fmt.Fprintf(s, "0x%04x, ", uint16(b[8*y+x]))
+ }
+ fmt.Fprintln(s)
+ }
+ fmt.Fprintf(s, "}")
+ return s.String()
+}
+
+// testBlocks are the first 10 pre-IDCT blocks from ../testdata/video-001.jpeg.
+var testBlocks = [10]block{
+ {
+ 0x7f, 0xf6, 0x01, 0x07, 0xff, 0x00, 0x00, 0x00,
+ 0xf5, 0x01, 0xfa, 0x01, 0xfe, 0x00, 0x01, 0x00,
+ 0x05, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x01, 0xff, 0xf8, 0x00, 0x01, 0xff, 0x00, 0x00,
+ 0x00, 0x01, 0x00, 0x01, 0x00, 0xff, 0xff, 0x00,
+ 0xff, 0x0c, 0x00, 0x00, 0x00, 0x00, 0xff, 0x01,
+ 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
+ 0x01, 0x00, 0x00, 0x01, 0xff, 0x01, 0x00, 0xfe,
+ },
+ {
+ 0x29, 0x07, 0x00, 0xfc, 0x01, 0x01, 0x00, 0x00,
+ 0x07, 0x00, 0x03, 0x00, 0x01, 0x00, 0xff, 0xff,
+ 0xff, 0xfd, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x04, 0x00, 0xff, 0x01, 0x00, 0x00,
+ 0x01, 0x00, 0x01, 0xff, 0x00, 0x00, 0x00, 0x00,
+ 0x01, 0xfa, 0x01, 0x00, 0x01, 0x00, 0x01, 0xff,
+ 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0xff, 0x00, 0xff, 0x00, 0x02,
+ },
+ {
+ 0xc5, 0xfa, 0x01, 0x00, 0x00, 0x01, 0x00, 0xff,
+ 0x02, 0xff, 0x01, 0x00, 0x01, 0x00, 0xff, 0x00,
+ 0xff, 0xff, 0x00, 0xff, 0x01, 0x00, 0x00, 0x00,
+ 0xff, 0x00, 0x01, 0x00, 0x00, 0x00, 0xff, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff,
+ 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ },
+ {
+ 0x86, 0x05, 0x00, 0x02, 0x00, 0x00, 0x01, 0x00,
+ 0xf2, 0x06, 0x00, 0x00, 0x01, 0x02, 0x00, 0x00,
+ 0xf6, 0xfa, 0xf9, 0x00, 0xff, 0x01, 0x00, 0x00,
+ 0xf9, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0xff, 0x00, 0xff, 0xff, 0xff, 0x00, 0x00,
+ 0xff, 0x00, 0x00, 0x01, 0x00, 0xff, 0x01, 0x00,
+ 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0x01,
+ 0x00, 0x01, 0xff, 0x01, 0x00, 0xff, 0x00, 0x00,
+ },
+ {
+ 0x24, 0xfe, 0x00, 0xff, 0x00, 0xff, 0xff, 0x00,
+ 0x08, 0xfd, 0x00, 0x01, 0x01, 0x00, 0x01, 0x00,
+ 0x06, 0x03, 0x03, 0xff, 0x00, 0x00, 0x00, 0x00,
+ 0x04, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff,
+ 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01,
+ 0x01, 0x00, 0x01, 0xff, 0x00, 0x01, 0x00, 0x00,
+ 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0xff, 0x01,
+ },
+ {
+ 0xcd, 0xff, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01,
+ 0x03, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff,
+ 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00,
+ 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00,
+ 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0xff,
+ },
+ {
+ 0x81, 0xfe, 0x05, 0xff, 0x01, 0xff, 0x01, 0x00,
+ 0xef, 0xf9, 0x00, 0xf9, 0x00, 0xff, 0x00, 0xff,
+ 0x05, 0xf9, 0x00, 0xf8, 0x01, 0xff, 0x01, 0xff,
+ 0x00, 0xff, 0x07, 0x00, 0x01, 0x00, 0x00, 0x00,
+ 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00,
+ 0x01, 0x00, 0x00, 0x00, 0xff, 0xff, 0x00, 0x01,
+ 0xff, 0x01, 0x01, 0x00, 0xff, 0x00, 0x00, 0x00,
+ 0x01, 0x01, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff,
+ },
+ {
+ 0x28, 0x00, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x0b, 0x02, 0x01, 0x03, 0x00, 0xff, 0x00, 0x01,
+ 0xfe, 0x02, 0x01, 0x03, 0xff, 0x00, 0x00, 0x00,
+ 0x01, 0x00, 0xfd, 0x00, 0x01, 0x00, 0xff, 0x00,
+ 0x01, 0xff, 0x00, 0xff, 0x01, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0xff, 0x01, 0x01, 0x00, 0xff,
+ 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0xff, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x01,
+ },
+ {
+ 0xdf, 0xf9, 0xfe, 0x00, 0x03, 0x01, 0xff, 0xff,
+ 0x04, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
+ 0xff, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x01,
+ 0x00, 0x00, 0xfe, 0x01, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0xff, 0x01, 0x00, 0x00, 0x00, 0x01,
+ 0xff, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00,
+ 0x00, 0xff, 0x00, 0xff, 0x01, 0x00, 0x00, 0x01,
+ 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00,
+ },
+ {
+ 0x88, 0xfd, 0x00, 0x00, 0xff, 0x00, 0x01, 0xff,
+ 0xe1, 0x06, 0x06, 0x01, 0xff, 0x00, 0x01, 0x00,
+ 0x08, 0x00, 0xfa, 0x00, 0xff, 0xff, 0xff, 0xff,
+ 0x08, 0x01, 0x00, 0xff, 0x01, 0xff, 0x00, 0x00,
+ 0xf5, 0xff, 0x00, 0x01, 0xff, 0x01, 0x01, 0x00,
+ 0xff, 0xff, 0x01, 0xff, 0x01, 0x00, 0x01, 0x00,
+ 0x00, 0x01, 0x01, 0xff, 0x00, 0xff, 0x00, 0x01,
+ 0x02, 0x00, 0x00, 0xff, 0xff, 0x00, 0xff, 0x00,
+ },
+}
src/pkg/image/jpeg/writer_test.go
--- a/src/pkg/image/jpeg/writer_test.go
+++ b/src/pkg/image/jpeg/writer_test.go
@@ -171,6 +171,23 @@ func TestWriter(t *testing.T) {
}\n }\n \n+func BenchmarkDecodeRGBOpaque(b *testing.B) {
+\tb.StopTimer()
+\tdata, err := ioutil.ReadFile("../testdata/video-001.jpeg")
+\tif err != nil {
+\t\tb.Fatal(err)
+\t}
+\tcfg, err := DecodeConfig(bytes.NewReader(data))
+\tif err != nil {
+\t\tb.Fatal(err)
+\t}
+\tb.SetBytes(int64(cfg.Width * cfg.Height * 4))
+\tb.StartTimer()
+\tfor i := 0; i < b.N; i++ {
+\t\tDecode(bytes.NewReader(data))
+\t}
+}\n+\n func BenchmarkEncodeRGBOpaque(b *testing.B) {
\tb.StopTimer()
\timg := image.NewRGBA(image.Rect(0, 0, 640, 480))
コアとなるコードの解説
idct.go
の最適化
idct
関数は、JPEGの逆離散コサイン変換(IDCT)を実行します。この関数は、8x8のブロックデータに対して水平方向と垂直方向の1次元IDCTを連続して適用することで、2次元IDCTを実現しています。
最適化の核心は、ループ内で繰り返し計算される配列インデックスの共通部分式を一時変数に格納することです。
-
y8 := y * 8
:idct
関数内のfor y := 0; y < 8; y++
ループにおいて、src
配列へのアクセス(例:src[y*8+1]
,src[y*8+0]
) でy*8
という計算が頻繁に行われていました。このy*8
をy8
という変数に一度計算して格納することで、以降のアクセスではsrc[y8+1]
のようにy8
を再利用し、乗算の回数を削減しています。これは、特にループの各イテレーションで同じベースアドレス計算が繰り返される場合に、CPUのサイクルを節約し、パフォーマンスを向上させます。 -
yStride := y * stride
: 同様に、最終的な出力dst
配列への書き込み(例:dst[y*stride+x]
) においても、y*stride
という計算が繰り返し行われていました。これをyStride
という変数に格納し再利用することで、同様の最適化効果を得ています。stride
は、画像の行のバイト数を表し、ピクセルデータをメモリに配置する際のオフセット計算に用いられます。
これらの変更は、コードの可読性をわずかに向上させつつ、コンパイラが生成するアセンブリコードをより効率的にすることを意図しています。Go言語の初期のコンパイラは、C/C++のコンパイラほど高度な最適化を自動で行わない場合があったため、このような手動での最適化が有効でした。
dct_test.go
の新規追加
このファイルは、image/jpeg
パッケージのDCTおよびIDCT実装の正確性と性能を保証するための重要なテストスイートです。
block
型の定義:idct.go
に移動されたtype block [blockSize]int
は、8x8のDCTブロックを表す型です。この型は、DCT/IDCTの入力および出力として使用されます。testBlocks
: JPEGのテストデータから抽出された実際のブロックデータです。これにより、現実的な入力に対するテストが可能になります。BenchmarkFDCT
とBenchmarkIDCT
:fdct
とidct
関数の実行時間を測定します。これにより、最適化の効果を数値的に確認できます。b.StopTimer()
とb.StartTimer()
を使用して、テストデータの準備時間を測定から除外しています。
TestDCT
:- 逆変換性の検証:
slowFDCT
とslowIDCT
を連続して適用し、元のデータがほぼ正確に復元されることを確認します。これは、DCTとIDCTが互いに逆変換であるという数学的性質を利用した基本的な検証です。 - 最適化された実装と参照実装の比較:
fdct
とslowFDCT
、idct
とslowIDCT
の結果を比較します。これにより、最適化された実装が、数学的に正しい(しかし非効率な)参照実装と同じ結果を生成することを確認し、最適化による正確性の低下がないことを保証します。 differ
関数は、浮動小数点演算の丸め誤差を考慮し、厳密な一致ではなく、ある程度の許容誤差(±2)を設けています。これは、JPEGのIDCTが浮動小数点演算を伴うため、異なる実装間でわずかな数値的な差異が生じることが一般的であるためです。
- 逆変換性の検証:
slowFDCT
とslowIDCT
:- これらは、DCTおよびIDCTの数学的な定義(コサイン関数の二重和)を直接実装したものです。計算コストは高いですが、アルゴリズムの正確な参照として機能します。テストにおいて、最適化された実装の出力がこれらの「遅い」実装の出力と一致するかどうかを検証することで、最適化が正しく行われたことを確認します。
これらのテストの追加により、image/jpeg
パッケージのDCT/IDCT処理の信頼性が大幅に向上し、将来の変更に対する安全網が提供されます。
関連リンク
- Go言語の
image/jpeg
パッケージのドキュメント: https://pkg.go.dev/image/jpeg - JPEG圧縮の概要 (Wikipedia): https://ja.wikipedia.org/wiki/JPEG
- 離散コサイン変換 (DCT) (Wikipedia): https://ja.wikipedia.org/wiki/%E9%9B%A2%E6%95%A3%E3%82%B3%E3%82%B5%E3%82%A4%E3%83%B3%E5%A4%89%E6%8F%9B
- 共通部分式除去 (Wikipedia): https://ja.wikipedia.org/wiki/%E5%85%B1%E9%80%9A%E9%83%A8%E5%88%86%E5%BC%8F%E9%99%A4%E5%8E%BB
参考にした情報源リンク
- Go言語の公式リポジトリ: https://github.com/golang/go
- Go言語のコードレビューシステム (Gerrit): https://go.dev/cl/6619056 (コミットメッセージに記載されているChange-ID)
- JPEG File Interchange Format (JFIF) Specification: https://www.w3.org/Graphics/JPEG/jfif3.pdf (JPEGの技術的な詳細に関する公式ドキュメント)
- 画像処理に関する一般的な書籍やオンラインリソース(DCT/IDCTの実装詳細や最適化手法について)
- コンパイラ最適化に関する書籍やオンラインリソース(共通部分式除去について)
[インデックス 14042] ファイルの概要
このコミットは、Go言語の標準ライブラリ image/jpeg
パッケージにおけるJPEG画像のDCT(離散コサイン変換)およびIDCT(逆離散コサイン変換)処理に関する改善とテスト追加を目的としています。具体的には、IDCTの実装において共通部分式除去(Common Sub-expression Elimination, CSE)による最適化が施され、その効果を検証するためのベンチマークと、DCT/IDCTの正確性を検証するための新しいテストが追加されています。
コミット
commit 12e343f372c03b6fec9d5da6cd83833c79812bc9
Author: Nigel Tao <nigeltao@golang.org>
Date: Sun Oct 7 10:21:17 2012 +1100
image/jpeg: add DCT tests, do a small optimization (common sub-expression
elimination) in idct.go.
benchmark old ns/op new ns/op delta
BenchmarkIDCT 5649 5610 -0.69%
BenchmarkDecodeRGBOpaque 2948607 2941051 -0.26%
The "type block" declaration moved so that idct.go is compilable
as a stand-alone file: "go tool 6g -S idct.go" works.
R=r
CC=golang-dev
https://golang.org/cl/6619056
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/12e343f372c03b6fec9d5da6cd83833c79812bc9
元コミット内容
image/jpeg
パッケージにDCTテストを追加し、idct.go
内で共通部分式除去(Common Sub-expression Elimination)による小さな最適化を行いました。
ベンチマーク結果は以下の通りです。
BenchmarkIDCT
: 5649 ns/op から 5610 ns/op へ -0.69% の改善BenchmarkDecodeRGBOpaque
: 2948607 ns/op から 2941051 ns/op へ -0.26% の改善
type block
の宣言が移動され、idct.go
がスタンドアロンファイルとしてコンパイル可能になりました(go tool 6g -S idct.go
が動作するようになります)。
変更の背景
このコミットの背景には、JPEGデコード処理のパフォーマンス向上と、その正確性を保証するためのテストカバレッジの拡充があります。
- パフォーマンス最適化: JPEG画像のデコードにおいて、IDCT(逆離散コサイン変換)は計算負荷の高い処理の一つです。この処理の効率を改善することは、JPEGデコード全体の速度向上に直結します。共通部分式除去は、コンパイラ最適化の基本的な手法であり、手動で適用することで、より効率的なコード生成を促すことができます。特に、Go言語の初期のコンパイラでは、このような低レベルな最適化を手動で行うことがパフォーマンス改善に寄与するケースがありました。
- テストカバレッジの拡充: 画像処理ライブラリにおいて、変換アルゴリズムの正確性は非常に重要です。DCTとIDCTは互いに逆変換の関係にあり、これらの処理が正しく機能していることを検証するテストは不可欠です。既存のテストが不十分であった場合、新たなテストを追加することで、将来的な変更によるデグレードを防ぎ、コードの信頼性を高めることができます。
- モジュール性の向上:
type block
の宣言をidct.go
に移動することで、idct.go
ファイルが他のファイルに依存することなく単独でコンパイルできるようになります。これは、開発者が個々のコンポーネントを独立してテストしたり、デバッグしたりする際に役立ち、コードベース全体のモジュール性と保守性を向上させます。
これらの変更は、Go言語の標準ライブラリとしての image/jpeg
パッケージの品質と性能を向上させるための継続的な取り組みの一環として行われました。
前提知識の解説
JPEG圧縮とDCT/IDCT
JPEG(Joint Photographic Experts Group)は、主に写真などの連続階調画像を圧縮するための標準的な方式です。JPEG圧縮は、人間の視覚特性を利用した非可逆圧縮であり、高い圧縮率と比較的良好な画質を両立させます。
JPEG圧縮の主要なステップには、以下のものが含まれます。
- 色空間変換: RGB画像をYCrCb色空間に変換します。Yは輝度(明るさ)、CrとCbは色差(色の情報)を表します。人間の目は輝度情報に敏感で、色差情報には比較的鈍感であるため、色差情報を間引く(サブサンプリング)ことで圧縮率を高めます。
- ブロック分割: 各Y, Cr, Cb成分を8x8ピクセルのブロックに分割します。
- レベルシフト: 各ブロックのピクセル値(0-255)を-128から127の範囲にシフトします。これはDCTの計算を容易にするためです。
- DCT(Discrete Cosine Transform - 離散コサイン変換): 各8x8ブロックに対してDCTを適用します。DCTは、空間領域のピクセルデータを周波数領域の係数に変換する数学的な操作です。低周波成分(画像の滑らかな変化)はブロックの左上隅に集中し、高周波成分(画像の細かいディテール)は右下隅に分布します。人間の視覚は高周波成分に鈍感であるため、この変換によって高周波成分を効率的に間引くことが可能になります。
- DCTの出力は、DC成分(ブロック全体の平均輝度)と63個のAC成分(周波数成分)からなる8x8の係数ブロックです。
- 量子化: DCT係数を量子化テーブルで割って丸めます。これにより、高周波成分の多くがゼロになり、データ量を大幅に削減できます。このステップが非可逆圧縮の主要な原因です。量子化テーブルは、画質と圧縮率のバランスを調整するために使用されます。
- エントロピー符号化: 量子化された係数をハフマン符号化や算術符号化などのエントロピー符号化によってさらに圧縮します。
JPEGデコードは、これらのステップを逆に行います。
- エントロピー復号化: 符号化されたデータを復号し、量子化されたDCT係数を取得します。
- 逆量子化: 量子化された係数に量子化テーブルを掛けて、元のDCT係数に近い値に戻します。
- IDCT(Inverse Discrete Cosine Transform - 逆離散コサイン変換): 逆量子化されたDCT係数に対してIDCTを適用し、周波数領域の係数を空間領域のピクセルデータに戻します。これにより、8x8ピクセルの画像ブロックが再構築されます。
- レベルシフト解除: ピクセル値を元の0-255の範囲に戻します。
- 色空間逆変換: YCrCb画像をRGB画像に戻します。
共通部分式除去 (Common Sub-expression Elimination, CSE)
共通部分式除去は、コンパイラ最適化の一種で、同じ値に評価される式が複数回出現する場合に、その式を一度だけ計算し、その結果を再利用することで計算量を削減する手法です。
例えば、以下のようなコードがあるとします。
a = (b * c) + d
e = (b * c) + f
ここで (b * c)
は共通部分式です。CSEを適用すると、コンパイラは b * c
を一度だけ計算し、その結果を一時変数に格納し、その一時変数を a
と e
の計算の両方で再利用します。
temp = b * c
a = temp + d
e = temp + f
これにより、乗算の回数を減らし、プログラムの実行速度を向上させることができます。特に、ループ内で共通部分式が出現する場合、その効果は大きくなります。
このコミットでは、idct.go
内の計算で、繰り返し出現する src[y*8+x]
のような配列アクセスが共通部分式として認識され、y8 := y * 8
や yStride := y * stride
のように一時変数に格納することで、インデックス計算の繰り返しを避けています。
技術的詳細
このコミットは、主に image/jpeg
パッケージ内のIDCT実装の最適化とテストの追加に焦点を当てています。
IDCTの最適化 (idct.go
)
idct.go
ファイルでは、IDCTアルゴリズムが実装されています。IDCTは、JPEGデコードにおいて周波数領域の係数から空間領域のピクセル値を再構築する重要なステップです。このコミットでは、IDCT関数の内部で、配列アクセスにおける共通部分式除去が行われています。
具体的には、以下の変更が加えられています。
-
y*8
の共通化: 変更前:src[y*8+1]
,src[y*8+2]
, ...,src[y*8+7]
のように、ループ内でy*8
の計算が繰り返し行われていました。 変更後:y8 := y * 8
という一時変数を導入し、src[y8+1]
,src[y8+2]
のようにy8
を再利用することで、乗算の回数を削減しています。これは、特にループの各イテレーションで同じベースアドレス計算が繰り返される場合に有効です。 -
y*stride
の共通化: 変更前:dst[y*stride+x]
のように、ループ内でy*stride
の計算が繰り返し行われていました。 変更後:yStride := y * stride
という一時変数を導入し、yStride
を再利用することで、乗算の回数を削減しています。
これらの変更は、Goコンパイラが当時、このような単純な共通部分式を自動的に最適化しない可能性があったため、手動で適用されたものと考えられます。これにより、わずかながらもパフォーマンスの向上が見られました(ベンチマーク結果の-0.69%および-0.26%)。
DCTテストの追加 (dct_test.go
)
新しく追加された src/pkg/image/jpeg/dct_test.go
ファイルは、DCTおよびIDCTの実装の正確性を検証するための包括的なテストスイートを提供します。
主なテスト内容は以下の通りです。
-
BenchmarkFDCT
とBenchmarkIDCT
:fdct
(Forward DCT) とidct
(Inverse DCT) 関数のパフォーマンスを測定するためのベンチマークです。testBlocks
という事前定義されたブロックデータを使用して、実際の処理に近い条件でベンチマークを実行します。
-
TestDCT
:- DCTとIDCTの正確性を検証する主要なテスト関数です。
- FDCTとIDCTの逆変換性の検証:
- ランダムに生成されたブロックを含む
testBlocks
を使用します。 - 元のブロックにレベルシフトとスケーリングを適用し、
slowFDCT
(遅いFDCT実装) を実行し、その後slowIDCT
(遅いIDCT実装) を実行します。 - 結果を元のブロックと比較し、
differ
関数(許容誤差を考慮)を使って差異がないことを確認します。これにより、FDCTとIDCTが互いに逆変換であることを検証します。
- ランダムに生成されたブロックを含む
- 最適化されたFDCTと遅いFDCTの一致性の検証:
fdct
(最適化されたFDCT) の結果とslowFDCT
の結果を比較し、両者が一致することを確認します。
- 最適化されたIDCTと遅いIDCTの一致性の検証:
idct
(最適化されたIDCT) の結果とslowIDCT
の結果を比較し、両者が一致することを確認します。
slowFDCT
とslowIDCT
は、DCT/IDCTの数学的定義に忠実な、より直接的な(しかし非効率な)実装であり、最適化された実装の参照として機能します。
-
differ
関数:- 2つのブロック間の差異をチェックするヘルパー関数です。
- JPEGのIDCTは浮動小数点演算と丸め誤差を含むため、厳密な一致ではなく、±2の許容誤差を設けています。これは、異なるIDCT実装間でわずかな差異が生じることが許容されるためです。
-
alpha
関数:- DCT/IDCTの計算で使用される係数
alpha(i)
を計算するヘルパー関数です。i=0
の場合は1、それ以外の場合は√2
を返します。
- DCT/IDCTの計算で使用される係数
-
slowFDCT
とslowIDCT
関数:- これらは、DCT/IDCTの数学的な定義に基づいた、直接的で理解しやすいが、最適化されていない実装です。これらは、最適化された
fdct
およびidct
関数の参照実装として、テストの正確性検証に用いられます。
- これらは、DCT/IDCTの数学的な定義に基づいた、直接的で理解しやすいが、最適化されていない実装です。これらは、最適化された
-
block
型の移動:src/pkg/image/jpeg/reader.go
からsrc/pkg/image/jpeg/idct.go
へtype block [blockSize]int
の宣言が移動されました。これにより、idct.go
がスタンドアロンでコンパイル可能になり、開発やテストの柔軟性が向上します。
ベンチマークの追加 (writer_test.go
)
src/pkg/image/jpeg/writer_test.go
に BenchmarkDecodeRGBOpaque
が追加されました。これは、JPEGデコード処理全体のパフォーマンスを測定するためのベンチマークであり、IDCTの最適化がデコード処理全体に与える影響を評価するために使用されます。
コアとなるコードの変更箇所
src/pkg/image/jpeg/idct.go
--- a/src/pkg/image/jpeg/idct.go
+++ b/src/pkg/image/jpeg/idct.go
@@ -37,6 +37,10 @@ package jpeg
*
*/
+const blockSize = 64 // A DCT block is 8x8.
+
+type block [blockSize]int
+
const (
w1 = 2841 // 2048*sqrt(2)*cos(1*pi/16)
w2 = 2676 // 2048*sqrt(2)*cos(2*pi/16)
@@ -70,30 +74,31 @@ const (
func idct(dst []byte, stride int, src *block) {
// Horizontal 1-D IDCT.
for y := 0; y < 8; y++ {\n+\t\ty8 := y * 8
\t// If all the AC components are zero, then the IDCT is trivial.
-\t\tif src[y*8+1] == 0 && src[y*8+2] == 0 && src[y*8+3] == 0 &&
-\t\t\tsrc[y*8+4] == 0 && src[y*8+5] == 0 && src[y*8+6] == 0 && src[y*8+7] == 0 {
-\t\t\tdc := src[y*8+0] << 3
-\t\t\tsrc[y*8+0] = dc
-\t\t\tsrc[y*8+1] = dc
-\t\t\tsrc[y*8+2] = dc
-\t\t\tsrc[y*8+3] = dc
-\t\t\tsrc[y*8+4] = dc
-\t\t\tsrc[y*8+5] = dc
-\t\t\tsrc[y*8+6] = dc
-\t\t\tsrc[y*8+7] = dc
+\t\tif src[y8+1] == 0 && src[y8+2] == 0 && src[y8+3] == 0 &&
+\t\t\tsrc[y8+4] == 0 && src[y8+5] == 0 && src[y8+6] == 0 && src[y8+7] == 0 {
+\t\t\tdc := src[y8+0] << 3
+\t\t\tsrc[y8+0] = dc
+\t\t\tsrc[y8+1] = dc
+\t\t\tsrc[y8+2] = dc
+\t\t\tsrc[y8+3] = dc
+\t\t\tsrc[y8+4] = dc
+\t\t\tsrc[y8+5] = dc
+\t\t\tsrc[y8+6] = dc
+\t\t\tsrc[y8+7] = dc
\t\tcontinue
\t}
\t// Prescale.
-\t\tx0 := (src[y*8+0] << 11) + 128
-\t\tx1 := src[y*8+4] << 11
-\t\tx2 := src[y*8+6]
-\t\tx3 := src[y*8+2]
-\t\tx4 := src[y*8+1]
-\t\tx5 := src[y*8+7]
-\t\tx6 := src[y*8+5]
-\t\tx7 := src[y*8+3]
+\t\tx0 := (src[y8+0] << 11) + 128
+\t\tx1 := src[y8+4] << 11
+\t\tx2 := src[y8+6]
+\t\tx3 := src[y8+2]
+\t\tx4 := src[y8+1]
+\t\tx5 := src[y8+7]
+\t\tx6 := src[y8+5]
+\t\tx7 := src[y8+3]
\t// Stage 1.
\tx8 := w7 * (x4 + x5)
@@ -123,14 +128,14 @@ func idct(dst []byte, stride int, src *block) {
\tx4 = (r2*(x4-x5) + 128) >> 8
\t// Stage 4.
-\t\tsrc[8*y+0] = (x7 + x1) >> 8
-\t\tsrc[8*y+1] = (x3 + x2) >> 8
-\t\tsrc[8*y+2] = (x0 + x4) >> 8
-\t\tsrc[8*y+3] = (x8 + x6) >> 8
-\t\tsrc[8*y+4] = (x8 - x6) >> 8
-\t\tsrc[8*y+5] = (x0 - x4) >> 8
-\t\tsrc[8*y+6] = (x3 - x2) >> 8
-\t\tsrc[8*y+7] = (x7 - x1) >> 8
+\t\tsrc[y8+0] = (x7 + x1) >> 8
+\t\tsrc[y8+1] = (x3 + x2) >> 8
+\t\tsrc[y8+2] = (x0 + x4) >> 8
+\t\tsrc[y8+3] = (x8 + x6) >> 8
+\t\tsrc[y8+4] = (x8 - x6) >> 8
+\t\tsrc[y8+5] = (x0 - x4) >> 8
+\t\tsrc[y8+6] = (x3 - x2) >> 8
+\t\tsrc[y8+7] = (x7 - x1) >> 8
}\n \n // Vertical 1-D IDCT.
@@ -189,8 +194,10 @@ func idct(dst []byte, stride int, src *block) {
// Level shift by +128, clip to [0, 255], and write to dst.
for y := 0; y < 8; y++ {\n+\t\ty8 := y * 8
+\t\tyStride := y * stride
\tfor x := 0; x < 8; x++ {\n-\t\t\tc := src[y*8+x]
+\t\t\tc := src[y8+x]
\t\tif c < -128 {
\t\t\tc = 0
\t\t} else if c > 127 {
@@ -198,7 +205,7 @@ func idct(dst []byte, stride int, src *block) {
\t\t} else {
\t\t\tc += 128
\t\t}
-\t\t\tdst[y*stride+x] = uint8(c)
+\t\t\tdst[yStride+x] = uint8(c)
\t}
}
}
src/pkg/image/jpeg/reader.go
--- a/src/pkg/image/jpeg/reader.go
+++ b/src/pkg/image/jpeg/reader.go
@@ -35,11 +35,7 @@ type component struct {
tq uint8 // Quantization table destination selector.
}
-type block [blockSize]int
-
const (
-\tblockSize = 64 // A DCT block is 8x8.
-\n dcTable = 0
acTable = 1
maxTc = 1
src/pkg/image/jpeg/dct_test.go
(新規ファイル)
--- /dev/null
+++ b/src/pkg/image/jpeg/dct_test.go
@@ -0,0 +1,297 @@
+// Copyright 2012 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package jpeg
+
+import (
+ "bytes"
+ "fmt"
+ "math"
+ "math/rand"
+ "testing"
+)
+
+func BenchmarkFDCT(b *testing.B) {
+ b.StopTimer()
+ blocks := make([]block, 0, b.N*len(testBlocks))
+ for i := 0; i < b.N; i++ {
+ blocks = append(blocks, testBlocks[:]...)
+ }
+ b.StartTimer()
+ for i := range blocks {
+ fdct(&blocks[i])
+ }
+}
+
+func BenchmarkIDCT(b *testing.B) {
+ b.StopTimer()
+ dummy := make([]byte, 64)
+ blocks := make([]block, 0, b.N*len(testBlocks))
+ for i := 0; i < b.N; i++ {
+ blocks = append(blocks, testBlocks[:]...)
+ }
+ b.StartTimer()
+ for i := range blocks {
+ idct(dummy, 8, &blocks[i])
+ }
+}
+
+func TestDCT(t *testing.T) {
+ blocks := make([]block, len(testBlocks))
+ copy(blocks, testBlocks[:])
+
+ // Append some randomly generated blocks of varying sparseness.
+ r := rand.New(rand.NewSource(123))
+ for i := 0; i < 100; i++ {
+ b := block{}
+ n := r.Int() % 64
+ for j := 0; j < n; j++ {
+ b[r.Int()%len(b)] = r.Int() % 256
+ }
+ blocks = append(blocks, b)
+ }
+
+ // Check that the FDCT and IDCT functions are inverses, after a scale and
+ // level shift. Scaling reduces the rounding errors in the conversion from
+ // floats to ints.
+ for i, b := range blocks {
+ got, want := b, b
+ for j := range got {
+ got[j] = (got[j] - 128) * 8
+ }
+ slowFDCT(&got)
+ slowIDCT(&got)
+ for j := range got {
+ got[j] = got[j]/8 + 128
+ }
+ if differ(&got, &want) {
+ t.Errorf("i=%d: IDCT(FDCT)\nsrc\n%s\ngot\n%s\nwant\n%s\n", i, &b, &got, &want)
+ }
+ }
+
+ // Check that the optimized and slow FDCT implementations agree.
+ // The fdct function already does a scale and level shift.
+ for i, b := range blocks {
+ got, want := b, b
+ fdct(&got)
+ for j := range want {
+ want[j] = (want[j] - 128) * 8
+ }
+ slowFDCT(&want)
+ if differ(&got, &want) {
+ t.Errorf("i=%d: FDCT\nsrc\n%s\ngot\n%s\nwant\n%s\n", i, &b, &got, &want)
+ }
+ }
+
+ // Check that the optimized and slow IDCT implementations agree.
+ dummy := make([]byte, 64)
+ for i, b := range blocks {
+ got, want := b, b
+ idct(dummy, 8, &got)
+ slowIDCT(&want)
+ if differ(&got, &want) {
+ t.Errorf("i=%d: IDCT\nsrc\n%s\ngot\n%s\nwant\n%s\n", i, &b, &got, &want)
+ }
+ }
+}
+
+// differ returns whether any pair-wise elements in b0 and b1 differ by 2 or
+// more. That tolerance is because there isn't a single definitive decoding of
+// a given JPEG image, even before the YCbCr to RGB conversion; implementations
+// can have different IDCT rounding errors.
+func differ(b0, b1 *block) bool {
+ for i := range b0 {
+ delta := b0[i] - b1[i]
+ if delta < -2 || +2 < delta {
+ return true
+ }
+ }
+ return false
+}
+
+// alpha returns 1 if i is 0 and returns √2 otherwise.
+func alpha(i int) float64 {
+ if i == 0 {
+ return 1
+ }
+ return math.Sqrt2
+}
+
+// slowFDCT performs the 8*8 2-dimensional forward discrete cosine transform:
+//
+// dst[u,v] = (1/8) * Σ_x Σ_y alpha(u) * alpha(v) * src[x,y] *
+// cos((π/2) * (2*x + 1) * u / 8) *
+// cos((π/2) * (2*y + 1) * v / 8)
+//
+// x and y are in pixel space, and u and v are in transform space.
+//
+// b acts as both dst and src.
+func slowFDCT(b *block) {
+ var dst [blockSize]float64
+ for v := 0; v < 8; v++ {
+ for u := 0; u < 8; u++ {
+ sum := 0.0
+ for y := 0; y < 8; y++ {
+ for x := 0; x < 8; x++ {
+ sum += alpha(u) * alpha(v) * float64(b[8*y+x]) *
+ math.Cos(math.Pi*float64((2*x+1)*u)/16) *
+ math.Cos(math.Pi*float64((2*y+1)*v)/16)
+ }
+ }
+ dst[8*v+u] = sum / 8
+ }
+ }
+ // Convert from float64 to int.
+ for i := range dst {
+ b[i] = int(dst[i] + 0.5)
+ }
+}
+
+// slowIDCT performs the 8*8 2-dimensional inverse discrete cosine transform:
+//
+// dst[x,y] = (1/8) * Σ_u Σ_v alpha(u) * alpha(v) * src[u,v] *
+// cos((π/2) * (2*x + 1) * u / 8) *
+// cos((π/2) * (2*y + 1) * v / 8)
+//
+// x and y are in pixel space, and u and v are in transform space.
+//
+// b acts as both dst and src.
+func slowIDCT(b *block) {
+ var dst [blockSize]float64
+ for y := 0; y < 8; y++ {
+ for x := 0; x < 8; x++ {
+ sum := 0.0
+ for v := 0; v < 8; v++ {
+ for u := 0; u < 8; u++ {
+ sum += alpha(u) * alpha(v) * float64(b[8*v+u]) *
+ math.Cos(math.Pi*float64((2*x+1)*u)/16) *
+ math.Cos(math.Pi*float64((2*y+1)*v)/16)
+ }
+ }
+ dst[8*y+x] = sum / 8
+ }
+ }
+ // Convert from float64 to int.
+ for i := range dst {
+ b[i] = int(dst[i] + 0.5)
+ }
+}
+
+func (b *block) String() string {
+ s := bytes.NewBuffer(nil)
+ fmt.Fprintf(s, "{\n")
+ for y := 0; y < 8; y++ {
+ fmt.Fprintf(s, "\t")
+ for x := 0; x < 8; x++ {
+ fmt.Fprintf(s, "0x%04x, ", uint16(b[8*y+x]))
+ }
+ fmt.Fprintln(s)
+ }
+ fmt.Fprintf(s, "}")
+ return s.String()
+}
+
+// testBlocks are the first 10 pre-IDCT blocks from ../testdata/video-001.jpeg.
+var testBlocks = [10]block{
+ {
+ 0x7f, 0xf6, 0x01, 0x07, 0xff, 0x00, 0x00, 0x00,
+ 0xf5, 0x01, 0xfa, 0x01, 0xfe, 0x00, 0x01, 0x00,
+ 0x05, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x01, 0xff, 0xf8, 0x00, 0x01, 0xff, 0x00, 0x00,
+ 0x00, 0x01, 0x00, 0x01, 0x00, 0xff, 0xff, 0x00,
+ 0xff, 0x0c, 0x00, 0x00, 0x00, 0x00, 0xff, 0x01,
+ 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
+ 0x01, 0x00, 0x00, 0x01, 0xff, 0x01, 0x00, 0xfe,
+ },
+ {
+ 0x29, 0x07, 0x00, 0xfc, 0x01, 0x01, 0x00, 0x00,
+ 0x07, 0x00, 0x03, 0x00, 0x01, 0x00, 0xff, 0xff,
+ 0xff, 0xfd, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x04, 0x00, 0xff, 0x01, 0x00, 0x00,
+ 0x01, 0x00, 0x01, 0xff, 0x00, 0x00, 0x00, 0x00,
+ 0x01, 0xfa, 0x01, 0x00, 0x01, 0x00, 0x01, 0xff,
+ 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0xff, 0x00, 0xff, 0x00, 0x02,
+ },
+ {
+ 0xc5, 0xfa, 0x01, 0x00, 0x00, 0x01, 0x00, 0xff,
+ 0x02, 0xff, 0x01, 0x00, 0x01, 0x00, 0xff, 0x00,
+ 0xff, 0xff, 0x00, 0xff, 0x01, 0x00, 0x00, 0x00,
+ 0xff, 0x00, 0x01, 0x00, 0x00, 0x00, 0xff, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff,
+ 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ },
+ {
+ 0x86, 0x05, 0x00, 0x02, 0x00, 0x00, 0x01, 0x00,
+ 0xf2, 0x06, 0x00, 0x00, 0x01, 0x02, 0x00, 0x00,
+ 0xf6, 0xfa, 0xf9, 0x00, 0xff, 0x01, 0x00, 0x00,
+ 0xf9, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0xff, 0x00, 0xff, 0xff, 0xff, 0x00, 0x00,
+ 0xff, 0x00, 0x00, 0x01, 0x00, 0xff, 0x01, 0x00,
+ 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0x01,
+ 0x00, 0x01, 0xff, 0x01, 0x00, 0xff, 0x00, 0x00,
+ },
+ {
+ 0x24, 0xfe, 0x00, 0xff, 0x00, 0xff, 0xff, 0x00,
+ 0x08, 0xfd, 0x00, 0x01, 0x01, 0x00, 0x01, 0x00,
+ 0x06, 0x03, 0x03, 0xff, 0x00, 0x00, 0x00, 0x00,
+ 0x04, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff,
+ 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01,
+ 0x01, 0x00, 0x01, 0xff, 0x00, 0x01, 0x00, 0x00,
+ 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0xff, 0x01,
+ },
+ {
+ 0xcd, 0xff, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01,
+ 0x03, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff,
+ 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00,
+ 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00,
+ 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0xff,
+ },
+ {
+ 0x81, 0xfe, 0x05, 0xff, 0x01, 0xff, 0x01, 0x00,
+ 0xef, 0xf9, 0x00, 0xf9, 0x00, 0xff, 0x00, 0xff,
+ 0x05, 0xf9, 0x00, 0xf8, 0x01, 0xff, 0x01, 0xff,
+ 0x00, 0xff, 0x07, 0x00, 0x01, 0x00, 0x00, 0x00,
+ 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00,
+ 0x01, 0x00, 0x00, 0x00, 0xff, 0xff, 0x00, 0x01,
+ 0xff, 0x01, 0x01, 0x00, 0xff, 0x00, 0x00, 0x00,
+ 0x01, 0x01, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff,
+ },
+ {
+ 0x28, 0x00, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x0b, 0x02, 0x01, 0x03, 0x00, 0xff, 0x00, 0x01,
+ 0xfe, 0x02, 0x01, 0x03, 0xff, 0x00, 0x00, 0x00,
+ 0x01, 0x00, 0xfd, 0x00, 0x01, 0x00, 0xff, 0x00,
+ 0x01, 0xff, 0x00, 0xff, 0x01, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0xff, 0x01, 0x01, 0x00, 0xff,
+ 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0xff, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x01,
+ },
+ {
+ 0xdf, 0xf9, 0xfe, 0x00, 0x03, 0x01, 0xff, 0xff,
+ 0x04, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
+ 0xff, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x01,
+ 0x00, 0x00, 0xfe, 0x01, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0xff, 0x01, 0x00, 0x00, 0x00, 0x01,
+ 0xff, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00,
+ 0x00, 0xff, 0x00, 0xff, 0x01, 0x00, 0x00, 0x01,
+ 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00,
+ },
+ {
+ 0x88, 0xfd, 0x00, 0x00, 0xff, 0x00, 0x01, 0xff,
+ 0xe1, 0x06, 0x06, 0x01, 0xff, 0x00, 0x01, 0x00,
+ 0x08, 0x00, 0xfa, 0x00, 0xff, 0xff, 0xff, 0xff,
+ 0x08, 0x01, 0x00, 0xff, 0x01, 0xff, 0x00, 0x00,
+ 0xf5, 0xff, 0x00, 0x01, 0xff, 0x01, 0x01, 0x00,
+ 0xff, 0xff, 0x01, 0xff, 0x01, 0x00, 0x01, 0x00,
+ 0x00, 0x01, 0x01, 0xff, 0x00, 0xff, 0x00, 0x01,
+ 0x02, 0x00, 0x00, 0xff, 0xff, 0x00, 0xff, 0x00,
+ },
+}
src/pkg/image/jpeg/writer_test.go
--- a/src/pkg/image/jpeg/writer_test.go
+++ b/src/pkg/image/jpeg/writer_test.go
@@ -171,6 +171,23 @@ func TestWriter(t *testing.T) {
}\n }\n \n+func BenchmarkDecodeRGBOpaque(b *testing.B) {
+\tb.StopTimer()
+\tdata, err := ioutil.ReadFile("../testdata/video-001.jpeg")
+\tif err != nil {
+\t\tb.Fatal(err)
+\t}
+\tcfg, err := DecodeConfig(bytes.NewReader(data))
+\tif err != nil {
+\t\tb.Fatal(err)
+\t}
+\tb.SetBytes(int64(cfg.Width * cfg.Height * 4))
+\tb.StartTimer()
+\tfor i := 0; i < b.N; i++ {
+\t\tDecode(bytes.NewReader(data))
+\t}
+}\n+\n func BenchmarkEncodeRGBOpaque(b *testing.B) {
\tb.StopTimer()
\timg := image.NewRGBA(image.Rect(0, 0, 640, 480))\
コアとなるコードの解説
idct.go
の最適化
idct
関数は、JPEGの逆離散コサイン変換(IDCT)を実行します。この関数は、8x8のブロックデータに対して水平方向と垂直方向の1次元IDCTを連続して適用することで、2次元IDCTを実現しています。
最適化の核心は、ループ内で繰り返し計算される配列インデックスの共通部分式を一時変数に格納することです。
-
y8 := y * 8
:idct
関数内のfor y := 0; y < 8; y++
ループにおいて、src
配列へのアクセス(例:src[y*8+1]
,src[y*8+0]
) でy*8
という計算が頻繁に行われていました。このy*8
をy8
という変数に一度計算して格納することで、以降のアクセスではsrc[y8+1]
のようにy8
を再利用し、乗算の回数を削減しています。これは、特にループの各イテレーションで同じベースアドレス計算が繰り返される場合に、CPUのサイクルを節約し、パフォーマンスを向上させます。 -
yStride := y * stride
: 同様に、最終的な出力dst
配列への書き込み(例:dst[y*stride+x]
) においても、y*stride
という計算が繰り返し行われていました。これをyStride
という変数に格納し再利用することで、同様の最適化効果を得ています。stride
は、画像の行のバイト数を表し、ピクセルデータをメモリに配置する際のオフセット計算に用いられます。
これらの変更は、コードの可読性をわずかに向上させつつ、コンパイラが生成するアセンブリコードをより効率的にすることを意図しています。Go言語の初期のコンパイラは、C/C++のコンパイラほど高度な最適化を自動で行わない場合があったため、このような手動での最適化が有効でした。
dct_test.go
の新規追加
このファイルは、image/jpeg
パッケージのDCTおよびIDCT実装の正確性と性能を保証するための重要なテストスイートです。
block
型の定義:idct.go
に移動されたtype block [blockSize]int
は、8x8のDCTブロックを表す型です。この型は、DCT/IDCTの入力および出力として使用されます。testBlocks
: JPEGのテストデータから抽出された実際のブロックデータです。これにより、現実的な入力に対するテストが可能になります。BenchmarkFDCT
とBenchmarkIDCT
:fdct
とidct
関数の実行時間を測定します。これにより、最適化の効果を数値的に確認できます。b.StopTimer()
とb.StartTimer()
を使用して、テストデータの準備時間を測定から除外しています。
TestDCT
:- 逆変換性の検証:
slowFDCT
とslowIDCT
を連続して適用し、元のデータがほぼ正確に復元されることを確認します。これは、DCTとIDCTが互いに逆変換であるという数学的性質を利用した基本的な検証です。 - 最適化された実装と参照実装の比較:
fdct
とslowFDCT
、idct
とslowIDCT
の結果を比較します。これにより、最適化された実装が、数学的に正しい(しかし非効率な)参照実装と同じ結果を生成することを確認し、最適化による正確性の低下がないことを保証します。 differ
関数は、浮動小数点演算の丸め誤差を考慮し、厳密な一致ではなく、ある程度の許容誤差(±2)を設けています。これは、JPEGのIDCTが浮動小数点演算を伴うため、異なる実装間でわずかな数値的な差異が生じることが一般的であるためです。
- 逆変換性の検証:
slowFDCT
とslowIDCT
:- これらは、DCTおよびIDCTの数学的な定義(コサイン関数の二重和)を直接実装したものです。計算コストは高いですが、アルゴリズムの正確な参照として機能します。テストにおいて、最適化された実装の出力がこれらの「遅い」実装の出力と一致するかどうかを検証することで、最適化が正しく行われたことを確認します。
これらのテストの追加により、image/jpeg
パッケージのDCT/IDCT処理の信頼性が大幅に向上し、将来の変更に対する安全網が提供されます。
関連リンク
- Go言語の
image/jpeg
パッケージのドキュメント: https://pkg.go.dev/image/jpeg - JPEG圧縮の概要 (Wikipedia): https://ja.wikipedia.org/wiki/JPEG
- 離散コサイン変換 (DCT) (Wikipedia): https://ja.wikipedia.org/wiki/%E9%9B%A2%E6%95%A3%E3%82%B3%E3%82%B5%E3%82%A4%E3%83%B3%E5%A4%89%E6%8F%9B
- 共通部分式除去 (Wikipedia): https://ja.wikipedia.org/wiki/%E5%85%B1%E9%80%9A%E9%83%A8%E5%88%86%E5%BC%8F%E9%99%A4%E5%8E%BB
参考にした情報源リンク
- Go言語の公式リポジトリ: https://github.com/golang/go
- Go言語のコードレビューシステム (Gerrit): https://go.dev/cl/6619056 (コミットメッセージに記載されているChange-ID)
- JPEG File Interchange Format (JFIF) Specification: https://www.w3.org/Graphics/JPEG/jfif3.pdf (JPEGの技術的な詳細に関する公式ドキュメント)
- 画像処理に関する一般的な書籍やオンラインリソース(DCT/IDCTの実装詳細や最適化手法について)
- コンパイラ最適化に関する書籍やオンラインリソース(共通部分式除去について)