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

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

このコミットは、Go言語の標準ライブラリである image パッケージにおいて、画像データへのアクセスを抽象化し、コードの可読性と保守性を向上させるための変更です。具体的には、image パッケージ内の様々な画像型(RGBA, NRGBA, Alpha, Gray など)に PixOffset メソッドが追加され、ピクセルデータが格納されている Pix スライス内の特定座標 (x, y) に対応するバイトオフセットを計算するロジックがカプセル化されました。これにより、image/draw および image/tiff パッケージ内の既存のピクセルアクセスコードが、この新しい PixOffset メソッドを使用するようにリファクタリングされています。

コミット

commit af08cfa494452b53d4b520f6ad862abf6f81f3ca
Author: Nigel Tao <nigeltao@golang.org>
Date:   Thu Jan 19 12:59:39 2012 +1100

    image: add PixOffset methods; use them in image/draw and image/tiff.
    
    image/draw benchmarks show <1% change for the fast paths.
    The slow paths got worse by 1-4%, but they're the slow paths.
    I don't care so much about them, and presumably compiler improvements
    could claw it back.
    
    IIUC 6g's inlining is enabled by default now.
    
    benchmark                        old ns/op    new ns/op    delta
    draw.BenchmarkFillOver             2988384      2999624   +0.38%
    draw.BenchmarkFillSrc               153141       153262   +0.08%
    draw.BenchmarkCopyOver             2155756      2170831   +0.70%
    draw.BenchmarkCopySrc                72591        72646   +0.08%
    draw.BenchmarkNRGBAOver            2487372      2491576   +0.17%
    draw.BenchmarkNRGBASrc             1361306      1409180   +3.52%
    draw.BenchmarkYCbCr                2540712      2562359   +0.85%
    draw.BenchmarkGlyphOver            1004879      1023308   +1.83%
    draw.BenchmarkRGBA                 8746670      8844455   +1.12%
    draw.BenchmarkGenericOver         31860960     32512960   +2.05%
    draw.BenchmarkGenericMaskOver     16369060     16435720   +0.41%
    draw.BenchmarkGenericSrc          13128540     13127810   -0.01%
    draw.BenchmarkGenericMaskSrc      30059300     28883210   -3.91%
    
    R=r, gri
    CC=golang-dev, rsc
    https://golang.org/cl/5536059

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

https://github.com/golang/go/commit/af08cfa494452b53d4b520f6ad862abf6f81f3ca

元コミット内容

image: add PixOffset methods; use them in image/draw and image/tiff.

このコミットの目的は、image パッケージに PixOffset メソッドを追加し、image/draw および image/tiff パッケージでそれらを使用することです。これにより、ピクセルデータへのアクセス方法が統一され、コードの明確性が向上します。ベンチマーク結果も示されており、高速パスでは1%未満の変更、低速パスでは1-4%の悪化が見られますが、これは許容範囲内とされています。また、6g コンパイラのインライン化がデフォルトで有効になっていることにも言及されています。

変更の背景

Go言語の image パッケージは、様々な画像フォーマットを扱うための基本的なデータ構造と操作を提供します。画像データは通常、Pix と呼ばれるバイトスライスに格納され、各ピクセルのデータは特定のオフセットに配置されます。このオフセットは、画像の幅(Stride)、ピクセルあたりのバイト数、および画像の矩形領域の開始座標 (Rect.Min.X, Rect.Min.Y) に基づいて計算されます。

コミット前のコードでは、このピクセルオフセットの計算ロジックが image/drawimage/tiff など、ピクセルデータにアクセスする様々な場所で重複して記述されていました。例えば、RGBA 画像の場合、(x, y) 座標のピクセルデータへのオフセットは (y-p.Rect.Min.Y)*p.Stride + (x-p.Rect.Min.X)*4 のように計算されていました。このような重複は、コードの保守性を低下させ、将来的な変更やバグ修正を困難にする可能性があります。

このコミットの背景には、以下の目的があったと考えられます。

  1. コードの抽象化とカプセル化: ピクセルオフセットの計算ロジックを PixOffset メソッドとしてカプセル化することで、image パッケージの内部実装の詳細を隠蔽し、外部からの利用者がより高レベルなインターフェースで画像データにアクセスできるようにします。
  2. 可読性と保守性の向上: 重複する計算ロジックを一つのメソッドにまとめることで、コードの可読性が向上し、ピクセルアクセスに関する意図がより明確になります。また、将来的にピクセルオフセットの計算方法が変更された場合でも、PixOffset メソッドの実装を一度変更するだけで済み、影響範囲を局所化できます。
  3. 潜在的な最適化の機会: メソッドとしてカプセル化することで、コンパイラがインライン化などの最適化を適用しやすくなる可能性があります。コミットメッセージで 6g コンパイラのインライン化に言及しているのは、この点を意識しているためと考えられます。

前提知識の解説

このコミットを理解するためには、Go言語の image パッケージの基本的な構造と、画像データがメモリ上でどのように表現されるかについての知識が必要です。

Go言語の image パッケージ

Go言語の image パッケージは、ビットマップ画像を表現するためのインターフェースと実装を提供します。主要な型は以下の通りです。

  • image.Image インターフェース: すべての画像型が実装する基本的なインターフェースで、Bounds() Rectangle (画像の矩形領域), ColorModel() color.Model (色モデル), At(x, y int) color.Color (指定座標のピクセル色を取得) などのメソッドを定義します。
  • image.RGBA 構造体: 最も一般的な画像型の一つで、各ピクセルが赤 (R), 緑 (G), 青 (B), アルファ (A) の4つの8ビット値で表現されます。
    type RGBA struct {
        Pix    []uint8 // ピクセルデータが格納されたバイトスライス
        Stride int     // 各行の開始から次の行の開始までのバイト数
        Rect   Rectangle // 画像の矩形領域
    }
    
  • Pix スライス: 画像の生ピクセルデータがバイトのシーケンスとして格納されます。例えば、image.RGBA の場合、Pix スライスは [R0, G0, B0, A0, R1, G1, B1, A1, ...] のようにピクセルデータが連続して並びます。
  • Stride: 画像の各行がメモリ上で占めるバイト数です。これは画像の幅とピクセルあたりのバイト数に基づいて計算されますが、アライメントのために実際の幅よりも大きくなることがあります。Stride を使用することで、ある行のピクセルから次の行の同じX座標のピクセルへ効率的に移動できます。
  • Rect: 画像の論理的な矩形領域を定義します。Min (左上隅の座標) と Max (右下隅の座標) を持ちます。画像のピクセルデータは、この Rect の範囲内で有効です。

ピクセルオフセットの計算

image.RGBA の場合、(x, y) 座標のピクセルデータが Pix スライス内のどこから始まるかを計算するには、以下の式が用いられます。

offset = (y - p.Rect.Min.Y) * p.Stride + (x - p.Rect.Min.X) * bytesPerPixel

ここで、bytesPerPixel はピクセルあたりのバイト数です。RGBA の場合は4バイト(R, G, B, Aそれぞれ1バイト)です。

Goコンパイラのインライン化 (6g)

Go言語のコンパイラ(当時の 6g など)は、プログラムの実行速度を向上させるために様々な最適化を行います。その一つが「インライン化 (inlining)」です。インライン化とは、関数呼び出しのオーバーヘッドを削減するために、呼び出される関数の本体を呼び出し元のコードに直接埋め込む最適化手法です。

コミットメッセージで「IIUC 6g's inlining is enabled by default now.」とあるのは、この最適化がデフォルトで有効になっていることを指しています。これは、PixOffset のような小さなヘルパーメソッドを導入しても、コンパイラがそれをインライン化することで、関数呼び出しのオーバーヘッドを実質的にゼロにできるため、パフォーマンスへの影響が最小限に抑えられるという期待を示唆しています。

技術的詳細

このコミットの技術的な核心は、Go言語の image パッケージにおけるピクセルデータアクセスの一貫性と効率性の向上にあります。

PixOffset メソッドの導入

コミットの主要な変更は、image パッケージ内の複数の画像型(RGBA, RGBA64, NRGBA, NRGBA64, Alpha, Alpha16, Gray, Gray16, Paletted, YCbCr)に PixOffset メソッドが追加されたことです。これらのメソッドは、特定の (x, y) 座標に対応する Pix スライス内の開始バイトオフセットを計算するロジックをカプセル化します。

例えば、image.RGBA 型に追加された PixOffset メソッドは以下のようになります。

// PixOffset returns the index of the first element of Pix that corresponds to
// the pixel at (x, y).
func (p *RGBA) PixOffset(x, y int) int {
	return (y-p.Rect.Min.Y)*p.Stride + (x-p.Rect.Min.X)*4
}

このメソッドは、RGBA 画像の Pix スライス内で (x, y) 座標のピクセルデータが始まるインデックスを返します。同様のメソッドが、各画像型のピクセルあたりのバイト数(RGBA は4、RGBA64 は8、Alpha は1など)に合わせて実装されています。

YCbCr 型には、輝度 (Y) と色差 (Cb, Cr) のデータが異なるスライスに格納されるため、YOffsetCOffset という2つの関連メソッドが追加されています。これらは、YCbCrのサブサンプリング比率(4:2:2, 4:2:0など)に応じてオフセット計算ロジックを内部で処理します。

既存コードのリファクタリング

PixOffset メソッドが導入された後、image/draw および image/tiff パッケージ内の既存のピクセルアクセスコードが、直接オフセットを計算する代わりに、これらの新しい PixOffset メソッドを呼び出すように変更されました。

変更前:

i0 := (r.Min.Y-dst.Rect.Min.Y)*dst.Stride + (r.Min.X-dst.Rect.Min.X)*4

変更後:

i0 := dst.PixOffset(r.Min.X, r.Min.Y)

この変更により、ピクセルオフセットの計算ロジックが image パッケージの各画像型に集約され、image/drawimage/tiff のコードはより簡潔で高レベルな記述になりました。

パフォーマンスへの影響

コミットメッセージには、ベンチマーク結果が詳細に記載されています。

  • 高速パス (fast paths): draw.BenchmarkFillOver, draw.BenchmarkFillSrc, draw.BenchmarkCopyOver, draw.BenchmarkCopySrc など、多くのベンチマークで1%未満のわずかな性能変化(ほとんどが微増)が見られます。これは、PixOffset メソッドの導入が、コンパイラのインライン化によってオーバーヘッドがほとんど発生しないためと考えられます。
  • 低速パス (slow paths): draw.BenchmarkNRGBASrc, draw.BenchmarkGlyphOver, draw.BenchmarkRGBA, draw.BenchmarkGenericOver など、一部のベンチマークでは1-4%程度の性能悪化が見られます。コミットメッセージでは、これらは「低速パス」であり、それほど重要視されていないこと、そして将来的なコンパイラの改善によって性能が回復する可能性があることが述べられています。

この結果から、この変更は主にコードの構造と保守性の改善を目的としており、パフォーマンスへの影響は全体として許容範囲内であると判断されたことがわかります。特に、6g コンパイラのインライン化がデフォルトで有効になっているという言及は、メソッド呼び出しのオーバーヘッドが最小限に抑えられるという確信に基づいています。

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

このコミットにおけるコアとなるコードの変更は、主に以下のファイルで行われています。

  1. src/pkg/image/image.go:

    • RGBA 型に PixOffset(x, y int) int メソッドを追加。
    • RGBA64 型に PixOffset(x, y int) int メソッドを追加。
    • NRGBA 型に PixOffset(x, y int) int メソッドを追加。
    • NRGBA64 型に PixOffset(x, y int) int メソッドを追加。
    • Alpha 型に PixOffset(x, y int) int メソッドを追加。
    • Alpha16 型に PixOffset(x, y int) int メソッドを追加。
    • Gray 型に PixOffset(x, y int) int メソッドを追加。
    • Gray16 型に PixOffset(x, y int) int メソッドを追加。
    • Paletted 型に PixOffset(x, y int) int メソッドを追加。
    • 既存の At, Set, SetRGBA, SubImage などのメソッド内で、直接オフセット計算を行っていた箇所を新しく追加された PixOffset メソッドの呼び出しに置き換え。
  2. src/pkg/image/ycbcr.go:

    • YCbCr 型に YOffset(x, y int) int メソッドを追加。
    • YCbCr 型に COffset(x, y int) int メソッドを追加。
    • 既存の At メソッド内で、直接オフセット計算を行っていた箇所を新しく追加された YOffset および COffset メソッドの呼び出しに置き換え。
  3. src/pkg/image/draw/draw.go:

    • drawFillOver, drawFillSrc, drawCopyOver, drawCopySrc, drawGlyphOver, drawRGBA などの関数内で、dst.PixOffsetsrc.PixOffset, mask.PixOffset を使用するように変更。
  4. src/pkg/image/tiff/reader.go:

    • decode 関数内で、img.PixOffset を使用するように変更。

コアとなるコードの解説

src/pkg/image/image.go の変更

image.go では、各画像型に PixOffset メソッドが追加されました。これは、特定の (x, y) 座標に対応するピクセルデータが Pix スライス内で始まるインデックスを計算するヘルパーメソッドです。

例: RGBA 型の PixOffset メソッド追加

--- a/src/pkg/image/image.go
+++ b/src/pkg/image/image.go
@@ -61,15 +61,21 @@ func (p *RGBA) At(x, y int) color.Color {
  	if !(Point{x, y}.In(p.Rect)) {
  		return color.RGBA{}
  	}
-	i := (y-p.Rect.Min.Y)*p.Stride + (x-p.Rect.Min.X)*4
+	i := p.PixOffset(x, y)
  	return color.RGBA{p.Pix[i+0], p.Pix[i+1], p.Pix[i+2], p.Pix[i+3]}
 }
 
+// PixOffset returns the index of the first element of Pix that corresponds to
+// the pixel at (x, y).
+func (p *RGBA) PixOffset(x, y int) int {
+	return (y-p.Rect.Min.Y)*p.Stride + (x-p.Rect.Min.X)*4
+}
+
 func (p *RGBA) Set(x, y int, c color.Color) {
  	if !(Point{x, y}.In(p.Rect)) {
  		return
  	}
-	i := (y-p.Rect.Min.Y)*p.Stride + (x-p.Rect.Min.X)*4
+	i := p.PixOffset(x, y)
  	c1 := color.RGBAModel.Convert(c).(color.RGBA)
  	p.Pix[i+0] = c1.R
  	p.Pix[i+1] = c1.G

この変更により、At, Set, SubImage などのメソッド内で重複していたオフセット計算ロジックが PixOffset メソッドに集約され、コードがよりDRY (Don't Repeat Yourself) になりました。他の画像型 (RGBA64, NRGBA など) も同様に PixOffset メソッドが追加され、それぞれのピクセルあたりのバイト数に合わせて計算式が調整されています。

src/pkg/image/ycbcr.go の変更

YCbCr 画像は、輝度 (Y) と色差 (Cb, Cr) のデータが別々のスライスに格納され、さらにサブサンプリングが行われるため、オフセット計算がより複雑です。このコミットでは、YCbCr 型に YOffsetCOffset メソッドが追加されました。

--- a/src/pkg/image/ycbcr.go
+++ b/src/pkg/image/ycbcr.go
@@ -49,28 +49,32 @@ func (p *YCbCr) At(x, y int) color.Color {
  	if !(Point{x, y}.In(p.Rect)) {
  		return color.YCbCr{}
  	}
+	yi := p.YOffset(x, y)
+	ci := p.COffset(x, y)
+	return color.YCbCr{
+		p.Y[yi],
+		p.Cb[ci],
+		p.Cr[ci],
+	}
+}
+
+// YOffset returns the index of the first element of Y that corresponds to
+// the pixel at (x, y).
+func (p *YCbCr) YOffset(x, y int) int {
+	return y*p.YStride + x
+}
+
+// COffset returns the index of the first element of Cb or Cr that corresponds
+// to the pixel at (x, y).
+func (p *YCbCr) COffset(x, y int) int {
  	switch p.SubsampleRatio {
  	case YCbCrSubsampleRatio422:
-\t\ti := x / 2
-\t\treturn color.YCbCr{
-\t\t\tp.Y[y*p.YStride+x],\n-\t\t\tp.Cb[y*p.CStride+i],\n-\t\t\tp.Cr[y*p.CStride+i],\n-\t\t}\n+\t\treturn y*p.CStride + (x / 2)
  	case YCbCrSubsampleRatio420:
-\t\ti, j := x/2, y/2
-\t\treturn color.YCbCr{
-\t\t\tp.Y[y*p.YStride+x],\n-\t\t\tp.Cb[j*p.CStride+i],\n-\t\t\tp.Cr[j*p.CStride+i],\n-\t\t}\n+\t\treturn (y/2)*p.CStride + (x / 2)
  	}\n  	// Default to 4:4:4 subsampling.
-\treturn color.YCbCr{
-\t\tp.Y[y*p.YStride+x],\n-\t\tp.Cb[y*p.CStride+x],\n-\t\tp.Cr[y*p.CStride+x],\n-\t}\n+	return y*p.CStride + x
 }
 
  // SubImage returns an image representing the portion of the image p visible

YOffset は輝度データ (Y スライス) のオフセットを、COffset は色差データ (Cb, Cr スライス) のオフセットを計算します。COffset は、YCbCrSubsampleRatio に応じて異なる計算ロジックを適用します。これにより、YCbCr 画像のピクセルアクセスも抽象化され、コードが整理されました。

src/pkg/image/draw/draw.go および src/pkg/image/tiff/reader.go の変更

これらのファイルでは、image.go で定義された新しい PixOffset メソッドを使用するように、既存のピクセルアクセスコードが更新されました。

例: src/pkg/image/draw/draw.go の変更

--- a/src/pkg/image/draw/draw.go
+++ b/src/pkg/image/draw/draw.go
@@ -171,7 +171,7 @@ func drawFillOver(dst *image.RGBA, r image.Rectangle, src *image.Uniform) {
  	sr, sg, sb, sa := src.RGBA()
  	// The 0x101 is here for the same reason as in drawRGBA.
  	a := (m - sa) * 0x101
-	i0 := (r.Min.Y-dst.Rect.Min.Y)*dst.Stride + (r.Min.X-dst.Rect.Min.X)*4
+	i0 := dst.PixOffset(r.Min.X, r.Min.Y)
  	i1 := i0 + r.Dx()*4
  	for y := r.Min.Y; y != r.Max.Y; y++ {
  		for i := i0; i < i1; i += 4 {

このように、直接オフセット計算を行っていた箇所が dst.PixOffset(x, y) のような簡潔な呼び出しに置き換えられています。これにより、image/drawimage/tiff のコードは、ピクセルデータの物理的な配置方法に依存せず、より高レベルな抽象化を利用できるようになりました。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント (上記リンク)
  • Gitコミットの差分情報 (git diff)
  • Go言語のコンパイラ最適化に関する一般的な知識 (インライン化など)
  • 画像処理におけるピクセルデータ表現とストライドの概念