[インデックス 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/draw
や image/tiff
など、ピクセルデータにアクセスする様々な場所で重複して記述されていました。例えば、RGBA
画像の場合、(x, y)
座標のピクセルデータへのオフセットは (y-p.Rect.Min.Y)*p.Stride + (x-p.Rect.Min.X)*4
のように計算されていました。このような重複は、コードの保守性を低下させ、将来的な変更やバグ修正を困難にする可能性があります。
このコミットの背景には、以下の目的があったと考えられます。
- コードの抽象化とカプセル化: ピクセルオフセットの計算ロジックを
PixOffset
メソッドとしてカプセル化することで、image
パッケージの内部実装の詳細を隠蔽し、外部からの利用者がより高レベルなインターフェースで画像データにアクセスできるようにします。 - 可読性と保守性の向上: 重複する計算ロジックを一つのメソッドにまとめることで、コードの可読性が向上し、ピクセルアクセスに関する意図がより明確になります。また、将来的にピクセルオフセットの計算方法が変更された場合でも、
PixOffset
メソッドの実装を一度変更するだけで済み、影響範囲を局所化できます。 - 潜在的な最適化の機会: メソッドとしてカプセル化することで、コンパイラがインライン化などの最適化を適用しやすくなる可能性があります。コミットメッセージで
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) のデータが異なるスライスに格納されるため、YOffset
と COffset
という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/draw
や image/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
コンパイラのインライン化がデフォルトで有効になっているという言及は、メソッド呼び出しのオーバーヘッドが最小限に抑えられるという確信に基づいています。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更は、主に以下のファイルで行われています。
-
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
メソッドの呼び出しに置き換え。
-
src/pkg/image/ycbcr.go
:YCbCr
型にYOffset(x, y int) int
メソッドを追加。YCbCr
型にCOffset(x, y int) int
メソッドを追加。- 既存の
At
メソッド内で、直接オフセット計算を行っていた箇所を新しく追加されたYOffset
およびCOffset
メソッドの呼び出しに置き換え。
-
src/pkg/image/draw/draw.go
:drawFillOver
,drawFillSrc
,drawCopyOver
,drawCopySrc
,drawGlyphOver
,drawRGBA
などの関数内で、dst.PixOffset
やsrc.PixOffset
,mask.PixOffset
を使用するように変更。
-
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
型に YOffset
と COffset
メソッドが追加されました。
--- 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/draw
や image/tiff
のコードは、ピクセルデータの物理的な配置方法に依存せず、より高レベルな抽象化を利用できるようになりました。
関連リンク
- Go言語
image
パッケージのドキュメント: https://pkg.go.dev/image - Go言語
image/draw
パッケージのドキュメント: https://pkg.go.dev/image/draw - Go言語
image/tiff
パッケージのドキュメント: https://pkg.go.dev/image/tiff
参考にした情報源リンク
- Go言語の公式ドキュメント (上記リンク)
- Gitコミットの差分情報 (
git diff
) - Go言語のコンパイラ最適化に関する一般的な知識 (インライン化など)
- 画像処理におけるピクセルデータ表現とストライドの概念