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

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

このコミットは、Go言語の標準ライブラリ image/png パッケージにおけるPNGエンコーディングのパフォーマンス改善を目的としています。特に image.Gray および image.NRGBA 型の画像をエンコードする際の処理が最適化され、大幅な速度向上が図られています。

コミット

commit 237ee3926906ad08a048e764920e036ecdb08b11
Author: Nigel Tao <nigeltao@golang.org>
Date:   Thu Sep 13 15:47:12 2012 +1000

    image/png: optimize encoding image.Gray and image.NRGBA images.
    
    benchmark                    old ns/op    new ns/op    delta
    BenchmarkEncodeGray           23616080      5624558  -76.18%
    BenchmarkEncodeNRGBOpaque     34181260     17144380  -49.84%
    BenchmarkEncodeNRGBA          41235820     20345990  -50.66%
    BenchmarkEncodePaletted        5594652      5620362   +0.46%
    BenchmarkEncodeRGBOpaque      17242210     17168820   -0.43%
    BenchmarkEncodeRGBA           66515720     67243560   +1.09%
    
    R=r
    CC=golang-dev
    https://golang.org/cl/6490099

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

https://github.com/golang/go/commit/237ee3926906ad08a048e764920e036ecdb08b11

元コミット内容

image/png パッケージにおいて、image.Gray および image.NRGBA 型の画像エンコーディングを最適化しました。ベンチマーク結果は以下の通りです。

  • BenchmarkEncodeGray: 23616080 ns/op から 5624558 ns/op へ、-76.18% の改善。
  • BenchmarkEncodeNRGBOpaque: 34181260 ns/op から 17144380 ns/op へ、-49.84% の改善。
  • BenchmarkEncodeNRGBA: 41235820 ns/op から 20345990 ns/op へ、-50.66% の改善。
  • BenchmarkEncodePaletted: 5594652 ns/op から 5620362 ns/op へ、+0.46% の変化。
  • BenchmarkEncodeRGBOpaque: 17242210 ns/op から 17168820 ns/op へ、-0.43% の変化。
  • BenchmarkEncodeRGBA: 66515720 ns/op から 67243560 ns/op へ、+1.09% の変化。

この変更は、GoのコードレビューシステムであるGerritの変更リスト https://golang.org/cl/6490099 に基づいています。

変更の背景

Go言語の image パッケージは、様々な画像フォーマットを扱うための汎用的なインターフェースを提供しています。しかし、汎用的なインターフェース (image.Image インターフェースの At(x, y) メソッドなど) を介したピクセルアクセスは、その抽象化のオーバーヘッドにより、特定の具体的な画像型 (image.Gray, image.NRGBA など) の内部データ構造に直接アクセスするよりも遅くなる可能性があります。

PNGエンコーディングは、画像をピクセルデータに変換し、それを圧縮してファイルに書き出すプロセスです。このプロセスにおいて、ピクセルデータを効率的に読み出すことは全体のパフォーマンスに大きく影響します。特に、image.Gray (グレースケール) や image.NRGBA (非アルファ事前乗算RGBA) のように、ピクセルデータがメモリ上で連続したバイト配列として格納されている場合、At(x, y) メソッドを繰り返し呼び出すよりも、そのバイト配列を直接コピーする方がはるかに高速です。

このコミットの背景には、これらの特定の画像型に対するPNGエンコーディングのボトルネックを解消し、より高速な画像処理を実現するという目的があります。ベンチマーク結果が示すように、特に image.Grayimage.NRGBA のエンコーディングにおいて顕著なパフォーマンス改善が見込まれました。

前提知識の解説

Go言語の image パッケージ

Go言語の image パッケージは、画像処理のための基本的なインターフェースと実装を提供します。

  • image.Image インターフェース: 全ての画像型が実装する基本的なインターフェースです。Bounds(), ColorModel(), At(x, y) Color の3つのメソッドを持ちます。At(x, y) メソッドは指定された座標のピクセルの色を返します。
  • 具体的な画像型: image パッケージには、image.RGBA, image.NRGBA, image.Gray, image.Paletted など、様々な具体的な画像型が定義されています。これらの型は、それぞれ異なるピクセルフォーマットとメモリレイアウトを持ちます。
    • image.Gray: 8ビットグレースケール画像。Pix フィールドにピクセルデータがバイト配列として格納されます。
    • image.NRGBA: 非アルファ事前乗算RGBA画像。各ピクセルはR, G, B, Aの4バイトで表現され、Pix フィールドに連続して格納されます。アルファ値が0の場合、RGB値も0になります。
    • image.RGBA: アルファ事前乗算RGBA画像。image.NRGBA と同様に4バイトで表現されますが、アルファ値が乗算されたRGB値が格納されます。
  • Pix フィールドと Stride フィールド: image.Gray, image.RGBA, image.NRGBA などの具体的な画像型は、通常 Pix []uint8Stride int というフィールドを持ちます。
    • Pix: 画像のピクセルデータを格納するバイトスライスです。
    • Stride: 各行の開始点間のバイト数を示します。これにより、画像データがメモリ上でどのように配置されているかを効率的に計算できます。

型アサーション

Go言語の型アサーション (value.(Type)) は、インターフェース型の変数が、特定の具体的な型を保持しているかどうかをチェックし、その具体的な型の値を取得するために使用されます。例えば、m.(image.Gray) は、mimage.Image インターフェース型であり、その基底の具体的な型が image.Gray である場合に、image.Gray 型の値と true を返します。そうでない場合は、nilfalse を返します。この機能は、インターフェースの抽象化を保ちつつ、特定の具体的な型の最適化された処理パスを利用するために重要です。

PNGエンコーディング

PNG (Portable Network Graphics) は、可逆圧縮を特徴とするラスターグラフィックスファイルフォーマットです。PNGエンコーディングのプロセスには、通常、ピクセルデータのフィルタリング(差分エンコーディングなど)と、Deflate圧縮アルゴリズムによる圧縮が含まれます。このコミットで最適化された部分は、ピクセルデータをフィルタリングのために準備する段階、つまり image.Image からバイト配列への変換効率です。

技術的詳細

このコミットの主要な最適化戦略は、Goの型アサーションを利用して、入力された image.Image が特定の具体的な型 (image.Gray, image.RGBA, image.NRGBA) であるかどうかを判別し、もしそうであれば、その具体的な型の内部データ表現 (Pix スライス) を直接利用してピクセルデータをコピーすることです。これにより、汎用的な image.Image.At(x, y) メソッドを各ピクセルに対して呼び出すオーバーヘッドを回避し、大幅なパフォーマンス向上を実現しています。

具体的には、src/pkg/image/png/writer.gowriteImage 関数内で、各ピクセル行を処理するループにおいて、以下の変更が加えられました。

  1. 画像型の事前判別: ループに入る前に、入力画像 m*image.Gray, *image.RGBA, *image.Paletted, *image.NRGBA のいずれかの具体的な型であるかを型アサーションによって判別し、それぞれの変数に格納します。

    gray, _ := m.(*image.Gray)
    rgba, _ := m.(*image.RGBA)
    paletted, _ := m.(*image.Paletted)
    nrgba, _ := m.(*image.NRGBA)
    

    これにより、ループ内で何度も型アサーションを行う必要がなくなり、効率が向上します。

  2. cbG8 (グレースケール8ビット) の最適化:

    • 変更前は、m.At(x, y) を呼び出して color.Gray に変換し、その Y 成分を1バイトずつコピーしていました。
    • 変更後は、もし gray != nil (つまり入力画像が *image.Gray 型である) ならば、gray.Pix スライスから該当する行のピクセルデータを copy 関数を使って直接 cr[0][1:] にコピーします。gray.Pix はグレースケールデータが連続して格納されているため、この直接コピーは非常に高速です。
    • offset := (y - b.Min.Y) * gray.Stride で、現在の行の Pix スライス内での開始オフセットを計算します。
  3. cbTC8 (トゥルーカラー8ビット、不透明) の最適化:

    • 変更前は、m*image.RGBA 型の場合にのみ rgba.Pix を直接利用していました。
    • 変更後は、*image.RGBA または *image.NRGBA のいずれかである場合に、それぞれの Pix スライスと Stride を取得し、そこからピクセルデータを直接コピーするように拡張されました。これにより、image.NRGBA 型の不透明画像もこの高速パスの恩恵を受けられるようになりました。
  4. cbTCA8 (トゥルーカラーアルファ8ビット) の最適化:

    • 変更前は、m.At(x, y) を呼び出して color.NRGBA に変換し、R, G, B, Aの各成分を1バイトずつコピーしていました。
    • 変更後は、もし nrgba != nil (つまり入力画像が *image.NRGBA 型である) ならば、nrgba.Pix スライスから該当する行のピクセルデータを copy 関数を使って直接 cr[0][1:] にコピーします。nrgba.Pix はNRGBAデータが連続して格納されているため、この直接コピーは非常に高速です。

これらの変更により、image.Grayimage.NRGBA のような、内部データが連続したバイト配列として効率的に格納されている画像型に対して、PNGエンコーディング時のピクセルデータ読み出しが大幅に高速化されました。他の画像型や、具体的な型にキャストできない汎用的な image.Image 実装の場合には、従来の At(x, y) を使用するフォールバックパスが引き続き利用されます。

src/pkg/image/png/writer_test.go では、これらの最適化の効果を測定するために、BenchmarkEncodeGray, BenchmarkEncodeNRGBOpaque, BenchmarkEncodeNRGBA といった新しいベンチマークが追加されました。これにより、変更が意図した通りにパフォーマンスを向上させていることが確認できます。

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

src/pkg/image/png/writer.go

--- a/src/pkg/image/png/writer.go
+++ b/src/pkg/image/png/writer.go
@@ -290,26 +290,42 @@ func writeImage(w io.Writer, m image.Image, cb int) error {
 	}\n 	pr := make([]uint8, 1+bpp*b.Dx())\n \n+\tgray, _ := m.(*image.Gray)\n+\trgba, _ := m.(*image.RGBA)\n+\tpaletted, _ := m.(*image.Paletted)\n+\tnrgba, _ := m.(*image.NRGBA)\n+\n 	for y := b.Min.Y; y < b.Max.Y; y++ {\n \t\t// Convert from colors to bytes.\n \t\ti := 1\n \t\tswitch cb {\n \t\tcase cbG8:\n-\t\t\tfor x := b.Min.X; x < b.Max.X; x++ {\n-\t\t\t\tc := color.GrayModel.Convert(m.At(x, y)).(color.Gray)\n-\t\t\t\tcr[0][i] = c.Y
-\t\t\t\ti++
+\t\t\tif gray != nil {\n+\t\t\t\toffset := (y - b.Min.Y) * gray.Stride\n+\t\t\t\tcopy(cr[0][1:], gray.Pix[offset:offset+b.Dx()])\n+\t\t\t} else {\n+\t\t\t\tfor x := b.Min.X; x < b.Max.X; x++ {\n+\t\t\t\t\tc := color.GrayModel.Convert(m.At(x, y)).(color.Gray)\n+\t\t\t\t\tcr[0][i] = c.Y
+\t\t\t\t\ti++
+\t\t\t\t}\n \t\t\t}\n \t\tcase cbTC8:\n \t\t\t// We have previously verified that the alpha value is fully opaque.\n \t\t\tcr0 := cr[0]\n-\t\t\tif rgba, _ := m.(*image.RGBA); rgba != nil {\n-\t\t\t\tj0 := (y - b.Min.Y) * rgba.Stride\n+\t\t\tstride, pix := 0, []byte(nil)\n+\t\t\tif rgba != nil {\n+\t\t\t\tstride, pix = rgba.Stride, rgba.Pix\n+\t\t\t} else if nrgba != nil {\n+\t\t\t\tstride, pix = nrgba.Stride, nrgba.Pix\n+\t\t\t}\n+\t\t\tif stride != 0 {\n+\t\t\t\tj0 := (y - b.Min.Y) * stride\n \t\t\t\tj1 := j0 + b.Dx()*4\n \t\t\t\tfor j := j0; j < j1; j += 4 {\n-\t\t\t\t\tcr0[i+0] = rgba.Pix[j+0]\n-\t\t\t\t\tcr0[i+1] = rgba.Pix[j+1]\n-\t\t\t\t\tcr0[i+2] = rgba.Pix[j+2]\n+\t\t\t\t\tcr0[i+0] = pix[j+0]\n+\t\t\t\t\tcr0[i+1] = pix[j+1]\n+\t\t\t\t\tcr0[i+2] = pix[j+2]\n \t\t\t\t\ti += 3\n \t\t\t\t}\n \t\t\t} else {\n@@ -322,9 +338,9 @@ func writeImage(w io.Writer, m image.Image, cb int) error {\n \t\t\t\t}\n \t\t\t}\n \t\tcase cbP8:\n-\t\t\tif p, _ := m.(*image.Paletted); p != nil {\n-\t\t\t\toffset := (y - b.Min.Y) * p.Stride\n-\t\t\t\tcopy(cr[0][1:], p.Pix[offset:offset+b.Dx()])\n+\t\t\tif paletted != nil {\n+\t\t\t\toffset := (y - b.Min.Y) * paletted.Stride\n+\t\t\t\tcopy(cr[0][1:], paletted.Pix[offset:offset+b.Dx()])\n \t\t\t} else {\n \t\t\t\tpi := m.(image.PalettedImage)\n \t\t\t\tfor x := b.Min.X; x < b.Max.X; x++ {\n@@ -333,14 +349,19 @@ func writeImage(w io.Writer, m image.Image, cb int) error {\n \t\t\t\t}\n \t\t\t}\n \t\tcase cbTCA8:\n-\t\t\t// Convert from image.Image (which is alpha-premultiplied) to PNG\'s non-alpha-premultiplied.\n-\t\t\tfor x := b.Min.X; x < b.Max.X; x++ {\n-\t\t\t\tc := color.NRGBAModel.Convert(m.At(x, y)).(color.NRGBA)\n-\t\t\t\tcr[0][i+0] = c.R\n-\t\t\t\tcr[0][i+1] = c.G\n-\t\t\t\tcr[0][i+2] = c.B\n-\t\t\t\tcr[0][i+3] = c.A\n-\t\t\t\ti += 4\n+\t\t\tif nrgba != nil {\n+\t\t\t\toffset := (y - b.Min.Y) * nrgba.Stride\n+\t\t\t\tcopy(cr[0][1:], nrgba.Pix[offset:offset+b.Dx()*4])\n+\t\t\t} else {\n+\t\t\t\t// Convert from image.Image (which is alpha-premultiplied) to PNG\'s non-alpha-premultiplied.\n+\t\t\t\tfor x := b.Min.X; x < b.Max.X; x++ {\n+\t\t\t\t\tc := color.NRGBAModel.Convert(m.At(x, y)).(color.NRGBA)\n+\t\t\t\t\tcr[0][i+0] = c.R\n+\t\t\t\t\tcr[0][i+1] = c.G\n+\t\t\t\t\tcr[0][i+2] = c.B\n+\t\t\t\t\tcr[0][i+3] = c.A\n+\t\t\t\t\ti += 4\n+\t\t\t\t}\n \t\t\t}\n \t\tcase cbG16:\

src/pkg/image/png/writer_test.go

--- a/src/pkg/image/png/writer_test.go
+++ b/src/pkg/image/png/writer_test.go
@@ -101,6 +101,49 @@ func TestSubImage(t *testing.T) {
 	}\n }\n \n+func BenchmarkEncodeGray(b *testing.B) {\n+\tb.StopTimer()\n+\timg := image.NewGray(image.Rect(0, 0, 640, 480))\n+\tb.SetBytes(640 * 480 * 1)\n+\tb.StartTimer()\n+\tfor i := 0; i < b.N; i++ {\n+\t\tEncode(ioutil.Discard, img)\n+\t}\n+}\n+\n+func BenchmarkEncodeNRGBOpaque(b *testing.B) {\n+\tb.StopTimer()\n+\timg := image.NewNRGBA(image.Rect(0, 0, 640, 480))\n+\t// Set all pixels to 0xFF alpha to force opaque mode.\n+\tbo := img.Bounds()\n+\tfor y := bo.Min.Y; y < bo.Max.Y; y++ {\n+\t\tfor x := bo.Min.X; x < bo.Max.X; x++ {\n+\t\t\timg.Set(x, y, color.NRGBA{0, 0, 0, 255})\n+\t\t}\n+\t}\n+\tif !img.Opaque() {\n+\t\tb.Fatal(\"expected image to be opaque\")\n+\t}\n+\tb.SetBytes(640 * 480 * 4)\n+\tb.StartTimer()\n+\tfor i := 0; i < b.N; i++ {\n+\t\tEncode(ioutil.Discard, img)\n+\t}\n+}\n+\n+func BenchmarkEncodeNRGBA(b *testing.B) {\n+\tb.StopTimer()\n+\timg := image.NewNRGBA(image.Rect(0, 0, 640, 480))\n+\tif img.Opaque() {\n+\t\tb.Fatal(\"expected image not to be opaque\")\n+\t}\n+\tb.SetBytes(640 * 480 * 4)\n+\tb.StartTimer()\n+\tfor i := 0; i < b.N; i++ {\n+\t\tEncode(ioutil.Discard, img)\n+\t}\n+}\n+\n func BenchmarkEncodePaletted(b *testing.B) {\n \tb.StopTimer()\n \timg := image.NewPaletted(image.Rect(0, 0, 640, 480), color.Palette{\n@@ -138,7 +181,7 @@ func BenchmarkEncodeRGBA(b *testing.B) {\n \tb.StopTimer()\n \timg := image.NewRGBA(image.Rect(0, 0, 640, 480))\n \tif img.Opaque() {\n-\t\tb.Fatal(\"expected image to not be opaque\")\n+\t\tb.Fatal(\"expected image not to be opaque\")\n \t}\n \tb.SetBytes(640 * 480 * 4)\n \tb.StartTimer()\

コアとなるコードの解説

src/pkg/image/png/writer.go の変更点

writeImage 関数は、image.Image インターフェースを受け取り、その画像をPNG形式でエンコードする主要なロジックを含んでいます。この関数は、画像のカラータイプ (cb 変数) に応じて異なるエンコーディングパスを選択します。

  1. 型アサーションの導入:

    gray, _ := m.(*image.Gray)
    rgba, _ := m.(*image.RGBA)
    paletted, _ := m.(*image.Paletted)
    nrgba, _ := m.(*image.NRGBA)
    

    mimage.Image インターフェース型ですが、その基底の具体的な型が *image.Gray, *image.RGBA, *image.Paletted, *image.NRGBA のいずれかであるかを事前にチェックし、それぞれのポインタ変数に格納しています。これにより、後続の switch 文内で何度も型アサーションを行う手間を省き、コードの可読性と効率を向上させています。

  2. cbG8 (グレースケール) の最適化:

    		case cbG8:
    			if gray != nil {
    				offset := (y - b.Min.Y) * gray.Stride
    				copy(cr[0][1:], gray.Pix[offset:offset+b.Dx()])
    			} else {
    				for x := b.Min.X; x < b.Max.X; x++ {
    					c := color.GrayModel.Convert(m.At(x, y)).(color.Gray)
    					cr[0][i] = c.Y
    					i++
    				}
    			}
    
    • if gray != nil の条件が追加されました。これは、入力画像 m が実際に *image.Gray 型のインスタンスである場合に真となります。
    • この条件が真の場合、gray.Pix スライスから直接ピクセルデータをコピーします。gray.Stride を使って現在の行の開始オフセットを計算し、copy 関数でその行の全ピクセルデータを cr[0][1:] (PNGエンコーダの内部バッファ) に効率的に転送します。image.Gray はピクセルデータが連続したバイト配列として格納されているため、この直接コピーは m.At(x, y) を繰り返し呼び出すよりもはるかに高速です。
    • else ブロックは、m*image.Gray 型ではない場合のフォールバックパスです。この場合、従来の m.At(x, y) を使用したピクセルごとの変換とコピーが行われます。
  3. cbTC8 (トゥルーカラー、不透明) の最適化:

    		case cbTC8:
    			// We have previously verified that the alpha value is fully opaque.
    			cr0 := cr[0]
    			stride, pix := 0, []byte(nil)
    			if rgba != nil {
    				stride, pix = rgba.Stride, rgba.Pix
    			} else if nrgba != nil {
    				stride, pix = nrgba.Stride, nrgba.Pix
    			}
    			if stride != 0 {
    				j0 := (y - b.Min.Y) * stride
    				j1 := j0 + b.Dx()*4
    				for j := j0; j < j1; j += 4 {
    					cr0[i+0] = pix[j+0]
    					cr0[i+1] = pix[j+1]
    					cr0[i+2] = pix[j+2]
    					i += 3
    				}
    			} else {
    				// ... (既存のフォールバックロジック)
    			}
    
    • stride, pix := 0, []byte(nil) で、ピクセルデータとストライドを保持する変数を初期化します。
    • if rgba != nil または else if nrgba != nil の条件で、入力画像が *image.RGBA または *image.NRGBA のいずれかであるかをチェックします。
    • いずれかの条件が真の場合、それぞれの StridePix フィールドを stridepix 変数に代入します。これにより、image.RGBAimage.NRGBA の両方の不透明画像が、この高速な直接ピクセルアクセスパスを利用できるようになります。
    • if stride != 0 の条件が真の場合、pix スライスから直接R, G, B成分をコピーします。
  4. cbP8 (パレット) の変更:

    -		if p, _ := m.(*image.Paletted); p != nil {
    -			offset := (y - b.Min.Y) * p.Stride
    -			copy(cr[0][1:], p.Pix[offset:offset+b.Dx()])
    +		if paletted != nil {
    +			offset := (y - b.Min.Y) * paletted.Stride
    +			copy(cr[0][1:], paletted.Pix[offset:offset+b.Dx()])
    		} else {
    			// ...
    		}
    
    • この変更は機能的な最適化ではなく、事前に取得した paletted 変数を使用するようにコードをクリーンアップしたものです。ロジック自体は変更されていません。
  5. cbTCA8 (トゥルーカラーアルファ) の最適化:

    		case cbTCA8:
    			if nrgba != nil {
    				offset := (y - b.Min.Y) * nrgba.Stride
    				copy(cr[0][1:], nrgba.Pix[offset:offset+b.Dx()*4])
    			} else {
    				// Convert from image.Image (which is alpha-premultiplied) to PNG's non-alpha-premultiplied.
    				for x := b.Min.X; x < b.Max.X; x++ {
    					c := color.NRGBAModel.Convert(m.At(x, y)).(color.NRGBA)
    					cr[0][i+0] = c.R
    					cr[0][i+1] = c.G
    					cr[0][i+2] = c.B
    					cr[0][i+3] = c.A
    					i += 4
    				}
    			}
    
    • if nrgba != nil の条件が追加されました。これは、入力画像 m*image.NRGBA 型のインスタンスである場合に真となります。
    • この条件が真の場合、nrgba.Pix スライスから直接ピクセルデータをコピーします。image.NRGBA は各ピクセルがR, G, B, Aの4バイトで連続して格納されているため、copy 関数でその行の全ピクセルデータを効率的に転送できます。
    • else ブロックは、m*image.NRGBA 型ではない場合のフォールバックパスです。この場合、従来の m.At(x, y) を使用したピクセルごとの変換とコピーが行われます。

src/pkg/image/png/writer_test.go の変更点

  • 新しいベンチマーク関数の追加:
    • BenchmarkEncodeGray: image.NewGray で作成した画像をエンコードするベンチマーク。
    • BenchmarkEncodeNRGBOpaque: image.NewNRGBA で作成し、全てのピクセルのアルファ値を 0xFF (完全に不透明) に設定した画像をエンコードするベンチマーク。これにより、不透明な NRGBA 画像のエンコーディング性能を測定します。
    • BenchmarkEncodeNRGBA: image.NewNRGBA で作成した、アルファ値が不透明ではない画像をエンコードするベンチマーク。これにより、アルファチャンネルを持つ NRGBA 画像のエンコーディング性能を測定します。
  • 既存ベンチマークの修正:
    • BenchmarkEncodeRGBA の中で、img.Opaque()true でないことを確認するアサーションが追加されました。これは、このベンチマークがアルファチャンネルを持つ RGBA 画像のエンコーディングを正確に測定していることを保証するためです。

これらのベンチマークの追加により、image.Grayimage.NRGBA のエンコーディング最適化が実際にパフォーマンスに貢献していることを定量的に確認できるようになりました。

関連リンク

参考にした情報源リンク