Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 14045] ファイルの概要

このコミットは、Go言語の標準ライブラリ image/jpeg パッケージにおけるJPEG画像デコード処理の最適化とコード構造の改善を目的としています。具体的には、逆離散コサイン変換(IDCT)関数から、結果のレベルシフト(+128)とクリッピング([0, 255]への丸め)処理を分離し、呼び出し元に移動しています。これにより、IDCT関数の純粋性が高まり、将来的なアセンブリ最適化が容易になることが期待されます。

コミット

commit 0b9fe6d24e50aaa0e3910566d938bd6ad0bf86d7
Author: Nigel Tao <nigeltao@golang.org>
Date:   Sun Oct 7 11:32:02 2012 +1100

    image/jpeg: move the level-shift and clip out of the idct function,
    to be consistent with the fdct function, and to ease any future
    idct rewrites in assembly.
    
    The BenchmarkIDCT delta is obviously just an accounting change and not
    a real saving, but it does give an indication of what proportion of
    time was spent in the actual IDCT and what proportion was in shift and
    clip. The idct time taken is now comparable to fdct.
    
    The BenchmarkFDCT delta is an estimate of benchmark noise.
    
    benchmark                   old ns/op    new ns/op    delta
    BenchmarkFDCT                    3842         3837   -0.13%
    BenchmarkIDCT                    5611         3478  -38.01%
    BenchmarkDecodeRGBOpaque      2932785      2929751   -0.10%
    
    R=r
    CC=golang-dev
    https://golang.org/cl/6625057

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/0b9fe6d24e50aaa0e3910566d938bd6ad0bf86d7

元コミット内容

image/jpeg: move the level-shift and clip out of the idct function, to be consistent with the fdct function, and to ease any future idct rewrites in assembly.

(日本語訳) image/jpeg: IDCT関数からレベルシフトとクリップ処理を移動。FDCT関数との一貫性を保ち、将来的なIDCTのアセンブリでの書き換えを容易にするため。

変更の背景

JPEG画像のデコードプロセスにおいて、逆離散コサイン変換(IDCT)は中心的な役割を担います。従来の idct 関数は、IDCTの計算だけでなく、その結果を8ビットのピクセル値として表現するために必要な「レベルシフト」(値を+128する)と「クリッピング」(値を0から255の範囲に丸める)の処理も内部で行っていました。

このコミットの背景には、以下の目的があります。

  1. コードの一貫性: 順方向離散コサイン変換(FDCT)関数は、純粋な変換のみを行い、後処理を含んでいませんでした。IDCTも同様に純粋な変換のみを行うようにすることで、コードベース全体での設計の一貫性を高めます。
  2. アセンブリ最適化の容易化: IDCTのような計算負荷の高い処理は、パフォーマンス向上のためにアセンブリ言語で最適化されることがあります。IDCT関数が純粋な数学的変換のみを行うようにすることで、アセンブリでの実装がよりシンプルになり、レベルシフトやクリッピングといった周辺的な処理に煩わされることなく、コアな計算ロジックに集中できるようになります。これにより、将来的なパフォーマンス改善の余地が広がります。
  3. 処理時間の内訳の明確化: ベンチマーク結果が示すように、IDCT関数内でレベルシフトとクリッピングに費やされていた時間が、純粋なIDCT計算時間から分離されることで、各処理ステップの実際のコストがより明確になります。

ベンチマーク結果は、BenchmarkIDCT の実行時間が大幅に短縮されたことを示していますが、これは処理がなくなったわけではなく、単に idct 関数から reader.go へと処理が移動した「会計上の変更」であると説明されています。しかし、この変更により、純粋なIDCTの実行時間がFDCTの実行時間と同程度になったことが示されており、各変換の相対的なコストがより明確になりました。

前提知識の解説

このコミットを理解するためには、以下の概念を把握しておく必要があります。

  1. JPEG画像圧縮の概要:

    • JPEGは、主に写真などの自然画像を効率的に圧縮するための標準的な方法です。
    • 圧縮プロセスは、画像を8x8ピクセルのブロックに分割し、各ブロックに対して以下の変換を行います。
      • 色空間変換: RGBからYCbCr(輝度Yと2つの色差Cb, Cr)へ変換します。人間の目は輝度変化に敏感で色差変化に鈍感なため、色差情報を間引くことで圧縮率を高めます。
      • 離散コサイン変換 (DCT): 各8x8ブロックのピクセル値を周波数成分に変換します。低周波数成分(画像の滑らかな変化)にエネルギーが集中し、高周波数成分(画像の細かいディテール)はエネルギーが小さくなります。
      • 量子化: DCT係数を丸めることで、情報の損失を伴いながらもデータ量を削減します。人間の視覚が認識しにくい高周波数成分ほど大きく丸められます。これが非可逆圧縮の主要な原因です。
      • エントロピー符号化: 量子化された係数をハフマン符号化などの方法でさらに圧縮します。
    • デコードプロセスは、これらの逆の操作を行います。
  2. 離散コサイン変換 (Discrete Cosine Transform, DCT):

    • 画像を周波数領域に変換する数学的な手法です。
    • 順方向DCT (FDCT): 空間領域のピクセルデータを周波数領域の係数に変換します。JPEGエンコーダで使用されます。
    • 逆方向DCT (IDCT): 周波数領域の係数を空間領域のピクセルデータに戻します。JPEGデコーダで使用されます。
    • IDCTの出力は通常、符号付きの整数値(例: -255から255)であり、そのままでは8ビットの符号なしピクセル値(0から255)として扱えません。
  3. レベルシフト (Level Shift):

    • IDCTの出力は、通常、中心が0の範囲(例: -128から127、または-255から255)の符号付き整数値です。
    • これを8ビットの符号なし整数(0から255)に変換するために、通常は128を加算します。例えば、-128は0に、0は128に、127は255に変換されます。
  4. クリッピング (Clipping):

    • レベルシフト後の値が、目的の範囲(8ビットピクセルの場合は0から255)を超えてしまうことがあります。
    • クリッピングは、この範囲外の値を強制的に範囲内に収める処理です。例えば、255を超える値は255に、0未満の値は0に丸められます。
  5. アセンブリ言語による最適化:

    • アセンブリ言語は、CPUが直接実行する機械語に非常に近い低レベルのプログラミング言語です。
    • 特定の計算負荷の高い処理(例: IDCT)をアセンブリで記述することで、コンパイラが生成するコードよりもさらに効率的なコードを作成し、パフォーマンスを最大化できる場合があります。
    • しかし、アセンブリコードはプラットフォーム依存性が高く、記述が複雑でデバッグも困難です。そのため、通常はパフォーマンスがクリティカルな部分に限定して使用されます。
  6. Go言語のベンチマーク:

    • Go言語には、testing パッケージに組み込みのベンチマーク機能があります。
    • go test -bench=. コマンドで実行され、関数の実行時間(ns/op、ナノ秒/操作)やメモリ割り当てなどを測定できます。
    • delta は、変更前後のパフォーマンス変化率を示します。

技術的詳細

このコミットの技術的詳細は、image/jpeg パッケージ内の3つのファイルにわたる変更に集約されます。

  1. src/pkg/image/jpeg/idct.go の変更:

    • 最も重要な変更は、idct 関数のシグネチャと実装です。
    • 変更前: func idct(dst []byte, stride int, src *block)
      • dststride は、IDCT結果を書き込むバイトスライスとそのストライド(行間のバイト数)を指定していました。
      • 関数内部でIDCT計算後、dst にレベルシフトとクリッピングを適用した結果を書き込んでいました。
    • 変更後: func idct(src *block)
      • dststride パラメータが削除されました。
      • 関数内部から、IDCT計算後のレベルシフトとクリッピング、そして dst への書き込みを行うループが完全に削除されました。
      • idct 関数の役割は、src ブロック内のDCT係数に対して純粋なIDCT変換を実行し、結果を同じ src ブロック(内部的には符号付き整数値)に格納することに限定されました。これにより、idct は数学的な変換器としての役割に特化します。
  2. src/pkg/image/jpeg/reader.go の変更:

    • idct.go から削除されたレベルシフトとクリッピングのロジックは、この reader.go ファイル内の processSOS メソッドに移動されました。
    • processSOS メソッドは、JPEGストリームからスキャン(Scan Of Scan, SOS)データを処理し、各MCU(Minimum Coded Unit)ブロックをデコードする役割を担っています。
    • 変更前: idct 関数を呼び出す際に、dststride を直接渡していました。
      // Perform the inverse DCT and store the MCU component to the image.
      if d.nComp == nGrayComponent {
          idct(d.img1.Pix[8*(my*d.img1.Stride+mx):], d.img1.Stride, &b)
      } else {
          // ... (YCbCr components)
          idct(d.img3.Y[8*(my0*d.img3.YStride+mx0):], d.img3.YStride, &b)
          // ...
      }
      
    • 変更後:
      1. まず、新しいシグネチャに合わせて idct(&b) を呼び出します。この時点では、b (block型) にはIDCT変換後の符号付き整数値が格納されています。
      2. 次に、dststride 変数を初期化し、デコード対象の画像コンポーネント(輝度Y、色差Cb、Cr)に応じて適切なピクセルバッファとストライドを設定します。
      3. その後、idct.go から移動してきたレベルシフトとクリッピングのループが追加され、b の内容を読み取り、+128のレベルシフトと[0, 255]へのクリッピングを適用し、その結果を dstuint8 型として書き込みます。
      // Perform the inverse DCT and store the MCU component to the image.
      idct(&b) // Call idct with new signature
      dst, stride := []byte(nil), 0
      if d.nComp == nGrayComponent {
          dst, stride = d.img1.Pix[8*(my*d.img1.Stride+mx):], d.img1.Stride
      } else {
          // ... (YCbCr components)
          switch i {
          case 0:
              dst, stride = d.img3.Y[8*(my0*d.img3.YStride+mx0):], d.img3.YStride
          case 1:
              dst, stride = d.img3.Cb[8*(my*d.img3.CStride+mx):], d.img3.CStride
          case 2:
              dst, stride = d.img3.Cr[8*(my*d.img3.CStride+mx):], d.img3.CStride
          }
      }
      // Level shift by +128, clip to [0, 255], and write to dst.
      for y := 0; y < 8; y++ {
          y8 := y * 8
          yStride := y * stride
          for x := 0; x < 8; x++ {
              c := b[y8+x] // Read from the block (which now holds IDCT result)
              if c < -128 {
                  c = 0
              } else if c > 127 {
                  c = 255
              } else {
                  c += 128
              }
              dst[yStride+x] = uint8(c) // Write to the destination byte slice
          }
      }
      
    • この変更により、reader.go はIDCTの「結果」を受け取り、それを最終的なピクセルデータに変換する役割を担うようになりました。
  3. src/pkg/image/jpeg/dct_test.go の変更:

    • idct 関数のシグネチャ変更に伴い、ベンチマークとテストコードが更新されました。
    • BenchmarkIDCT 関数は、新しい idct シグネチャに合わせて dststride の引数を削除しました。
    • TestDCT 関数も同様に idct の呼び出しを修正しました。
    • さらに、BenchmarkFDCTBenchmarkIDCT の両方で共通のベンチマークロジックを再利用するために、新しいヘルパー関数 benchmarkDCT が導入されました。これにより、テストコードの重複が削減され、保守性が向上しました。

これらの変更は、idct 関数の責務を明確にし、純粋な数学的変換に限定することで、コードのモジュール性を高め、将来的な最適化(特にアセンブリでの実装)を容易にするための基盤を築いています。

コアとなるコードの変更箇所

このコミットにおけるコアとなるコードの変更は、主に src/pkg/image/jpeg/idct.gosrc/pkg/image/jpeg/reader.go に集中しています。

src/pkg/image/jpeg/idct.go:

--- a/src/pkg/image/jpeg/idct.go
+++ b/src/pkg/image/jpeg/idct.go
@@ -59,9 +59,7 @@ const (
 	r2 = 181 // 256/sqrt(2)
 )
 
-// idct performs a 2-D Inverse Discrete Cosine Transformation, followed by a
-// +128 level shift and a clip to [0, 255], writing the results to dst.\n-// stride is the number of elements between successive rows of dst.
+// idct performs a 2-D Inverse Discrete Cosine Transformation.
 //
 // The input coefficients should already have been multiplied by the
 // appropriate quantization table. We use fixed-point computation, with the
@@ -71,7 +69,7 @@ const (
 // For more on the actual algorithm, see Z. Wang, \"Fast algorithms for the
 // discrete cosine transform and for the discrete Fourier transform\", IEEE Trans. on
 // ASSP, Vol. ASSP- 32, pp. 803-816, Aug. 1984.
-func idct(dst []byte, stride int, src *block) {
+func idct(src *block) {
 	// Horizontal 1-D IDCT.
 	for y := 0; y < 8; y++ {
 	\ty8 := y * 8
@@ -191,21 +189,4 @@ func idct(dst []byte, stride int, src *block) {
 	\tsrc[8*6+x] = (y3 - y2) >> 14
 	\tsrc[8*7+x] = (y7 - y1) >> 14
 	}\n-\n-\t// Level shift by +128, clip to [0, 255], and write to dst.\n-\tfor y := 0; y < 8; y++ {\n-\t\ty8 := y * 8\n-\t\tyStride := y * stride\n-\t\tfor x := 0; x < 8; x++ {\n-\t\t\tc := src[y8+x]\n-\t\t\tif c < -128 {\n-\t\t\t\tc = 0\n-\t\t\t} else if c > 127 {\n-\t\t\t\tc = 255\n-\t\t\t} else {\n-\t\t\t\tc += 128\n-\t\t\t}\n-\t\t\tdst[yStride+x] = uint8(c)\n-\t\t}\n-\t}\n }

src/pkg/image/jpeg/reader.go:

--- a/src/pkg/image/jpeg/reader.go
+++ b/src/pkg/image/jpeg/reader.go
@@ -309,8 +309,10 @@ func (d *decoder) processSOS(n int) error {
 					}
 
 					// Perform the inverse DCT and store the MCU component to the image.
-					if d.nComp == nGrayComponent {
-						idct(d.img1.Pix[8*(my*d.img1.Stride+mx):], d.img1.Stride, &b)
+					idct(&b) // Call idct with new signature
+					dst, stride := []byte(nil), 0
+					if d.nComp == nGrayComponent {
+						dst, stride = d.img1.Pix[8*(my*d.img1.Stride+mx):], d.img1.Stride
 					} else {
 						switch i {
 						case 0:
@@ -321,11 +323,27 @@ func (d *decoder) processSOS(n int) error {
 							mx0 += j % 2
 							my0 += j / 2
 						}
-						idct(d.img3.Y[8*(my0*d.img3.YStride+mx0):], d.img3.YStride, &b)
+						dst, stride = d.img3.Y[8*(my0*d.img3.YStride+mx0):], d.img3.YStride
 						case 1:
-						idct(d.img3.Cb[8*(my*d.img3.CStride+mx):], d.img3.CStride, &b)
+						dst, stride = d.img3.Cb[8*(my*d.img3.CStride+mx):], d.img3.CStride
 						case 2:
-						idct(d.img3.Cr[8*(my*d.img3.CStride+mx):], d.img3.CStride, &b)
+						dst, stride = d.img3.Cr[8*(my*d.img3.CStride+mx):], d.img3.CStride
+						}
+					}
+					// Level shift by +128, clip to [0, 255], and write to dst.
+					for y := 0; y < 8; y++ {
+						y8 := y * 8
+						yStride := y * stride
+						for x := 0; x < 8; x++ {
+							c := b[y8+x]
+							if c < -128 {
+								c = 0
+							} else if c > 127 {
+								c = 255
+							} else {
+								c += 128
+							}
+							dst[yStride+x] = uint8(c)
 						}
 					}
 				} // for j

コアとなるコードの解説

idct.go の変更点

  • 関数シグネチャの変更:
    • func idct(dst []byte, stride int, src *block) から func idct(src *block) へと変更されました。
    • これにより、idct 関数は、変換結果を書き込む先のバイトスライス (dst) とそのストライド (stride) を直接受け取らなくなりました。
    • idct は、入力として受け取った src ブロック(DCT係数を保持)に対してIDCTを実行し、その結果を同じ src ブロック内に(内部的な表現で)格納するようになりました。この src ブロックは、int16 のような符号付き整数値を保持する構造であると推測されます。
  • レベルシフトとクリッピングロジックの削除:
    • 変更前の idct 関数の末尾にあった、IDCT結果を +128 レベルシフトし、[0, 255] の範囲にクリッピングして dstuint8 として書き込むループが完全に削除されました。
    • これにより、idct 関数は純粋に2次元逆離散コサイン変換のみを実行する、より「数学的」な関数となりました。

reader.go の変更点

  • idct 呼び出しの変更:
    • processSOS 関数内で idct を呼び出す際、新しいシグネチャに合わせて idct(&b) のように、block 型のポインタのみを渡すようになりました。
  • レベルシフトとクリッピングロジックの追加:
    • idct(&b) の呼び出し後、reader.go は、idct が変換した結果が格納されている b ブロックから値を取り出し、手動でレベルシフトとクリッピングを実行するようになりました。
    • 具体的には、以下の処理が追加されました。
      1. dststride 変数を宣言し、現在の画像コンポーネント(グレースケール、Y、Cb、Cr)に応じて、最終的なピクセルデータを書き込むべきバイトスライスとストライドを設定します。
      2. idct.go から移動してきた、8x8ブロックの各要素に対するループが追加されました。
      3. ループ内で、b[y8+x] からIDCT結果(符号付き整数値)を読み取ります。
      4. 読み取った値 c に対して、以下の条件分岐でクリッピングとレベルシフトを行います。
        • c < -128 の場合、c0 に設定(下限クリッピング)。
        • c > 127 の場合、c255 に設定(上限クリッピング)。
        • それ以外の場合(-128 <= c <= 127)、c128 を加算(レベルシフト)。
      5. 最終的に変換された c の値を uint8 型にキャストし、dst[yStride+x] に書き込みます。

全体的な影響

この変更により、idct 関数は、その名前が示す通り「逆離散コサイン変換」のみを行う責務を持つようになりました。ピクセル値への最終的な変換(レベルシフトとクリッピング)は、デコードプロセス全体を管理する reader.go の責務となりました。これは、関数の単一責任の原則(Single Responsibility Principle)に沿った改善であり、コードのモジュール性と理解しやすさを向上させます。また、idct のような計算の核となる部分をより純粋に保つことで、将来的にアセンブリ言語などで最適化を行う際の障壁が低減されます。

関連リンク

参考にした情報源リンク