[インデックス 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言語のコンパイラ最適化に関する一般的な知識 (インライン化など)
- 画像処理におけるピクセルデータ表現とストライドの概念