[インデックス 12983] ファイルの概要
このコミットは、Go言語の標準ライブラリ image/png
パッケージにおけるPNGデコード処理のパフォーマンス改善を目的としています。特に、Gray、NRGBA、Paletted、RGBAといった一般的なカラーモデルのデコード速度が向上しています。ベンチマーク結果が示されており、最大で43%以上のデコード速度向上が達成されています。
コミット
- Author: Nigel Tao nigeltao@golang.org
- Date: Fri Apr 27 16:03:58 2012 +1000
- Commit Hash: dd294fbd5a6eea9574df8c3f842342a8cd10f2c6
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/dd294fbd5a6eea9574df8c3f842342a8cd10f2c6
元コミット内容
image/png: speed up PNG decoding for common color models: Gray, NRGBA,
Paletted, RGBA.
benchmark old ns/op new ns/op delta
BenchmarkDecodeGray 3681144 2536049 -31.11%
BenchmarkDecodeNRGBAGradient 12108660 10020650 -17.24%
BenchmarkDecodeNRGBAOpaque 10699230 8677165 -18.90%
BenchmarkDecodePaletted 2562806 1458798 -43.08%
BenchmarkDecodeRGB 8468175 7180730 -15.20%
benchmark old MB/s new MB/s speedup
BenchmarkDecodeGray 17.80 25.84 1.45x
BenchmarkDecodeNRGBAGradient 21.65 26.16 1.21x
BenchmarkDecodeNRGBAOpaque 24.50 30.21 1.23x
BenchmarkDecodePaletted 25.57 44.92 1.76x
BenchmarkDecodeRGB 30.96 36.51 1.18x
$ file $GOROOT/src/pkg/image/png/testdata/bench*
benchGray.png: PNG image, 256 x 256, 8-bit grayscale, non-interlaced
benchNRGBA-gradient.png: PNG image, 256 x 256, 8-bit/color RGBA, non-interlaced
benchNRGBA-opaque.png: PNG image, 256 x 256, 8-bit/color RGBA, non-interlaced
benchPaletted.png: PNG image, 256 x 256, 8-bit colormap, non-interlaced
benchRGB.png: PNG image, 256 x 256, 8-bit/color RGB, non-interlaced
R=r
CC=golang-dev
https://golang.org/cl/6127051
変更の背景
PNG画像のデコード処理は、画像処理アプリケーションにおいて頻繁に実行される操作であり、そのパフォーマンスはアプリケーション全体の応答性に大きく影響します。特に、Go言語の image/png
パッケージは、画像処理ライブラリの中核をなすため、デコード速度の最適化は非常に重要です。
このコミットの背景には、既存のPNGデコード処理において、ピクセルデータを画像構造体に書き込む際の効率性の問題がありました。特に、Set
メソッドを介したピクセルごとの書き込みは、ループ内で頻繁に呼び出されるため、オーバーヘッドが大きくなる傾向がありました。このオーバーヘッドを削減し、より高速なデコードを実現することが、この変更の主な動機です。ベンチマーク結果が示すように、特定のカラーモデルにおいて顕著な速度改善が見込まれました。
前提知識の解説
PNG (Portable Network Graphics)
PNGは、可逆圧縮を特徴とするラスターグラフィックファイルフォーマットです。ウェブ上での画像表示や、透過性が必要なグラフィックによく使用されます。PNGは様々なカラーモデルをサポートしており、このコミットで言及されているのは以下のものです。
- Gray (グレースケール): 各ピクセルが単一の輝度値で表現されます。
- NRGBA (Non-premultiplied Red, Green, Blue, Alpha): 各ピクセルが赤、緑、青、アルファ(透明度)の4つの成分で表現されます。アルファ値は色成分に乗算されていません。
- Paletted (パレット): 画像内の各ピクセルが、定義されたカラーパレット内のインデックスを参照します。これにより、ファイルサイズを削減できます。
- RGBA (Red, Green, Blue, Alpha): NRGBAと同様に4つの成分を持ちますが、Goの
image.RGBA
型は通常、アルファ値が色成分に事前に乗算されていることを意味します(ただし、このコミットの文脈では、cdat
から直接値を読み込むため、その違いは実装の詳細に依存します)。
Go言語の image
パッケージ
Go言語の標準ライブラリ image
パッケージは、様々な画像フォーマットの読み書きと操作を提供します。
image.Image
インターフェース: すべての画像型が実装するインターフェースで、At(x, y int) color.Color
メソッドなどを持ちます。image.Gray
: グレースケール画像を表現する型。Pix
フィールドにピクセルデータがバイトスライスとして格納されます。image.NRGBA
: NRGBAカラーモデルの画像を表現する型。Pix
フィールドにピクセルデータがバイトスライスとして格納されます。各ピクセルは4バイト(R, G, B, A)で構成されます。image.Paletted
: パレット画像を表現する型。Pix
フィールドにパレットインデックスがバイトスライスとして格納されます。image.RGBA
: RGBAカラーモデルの画像を表現する型。Pix
フィールドにピクセルデータがバイトスライスとして格納されます。各ピクセルは4バイト(R, G, B, A)で構成されます。Stride
: 画像の各行の開始点間のバイト数を表します。これは、画像の幅とピクセルあたりのバイト数だけでなく、アライメントのためにパディングが含まれる場合があるため重要です。Pix
: 画像のピクセルデータを格納するバイトスライスです。Pix[i]
はi
番目のバイトを表します。
ピクセルデータの操作方法
Goの image
パッケージでは、ピクセルデータを操作する方法がいくつかあります。
Set(x, y int, c color.Color)
メソッド:image.Image
インターフェースを実装する型が持つメソッドで、指定された座標(x, y)
にcolor.Color
型の値を設定します。このメソッドは、内部でピクセルデータの計算やメモリへの書き込みを行います。抽象度が高く、使いやすい反面、ピクセルごとの呼び出しにはオーバーヘッドが伴う可能性があります。Pix
フィールドへの直接アクセス:image.Gray
,image.NRGBA
,image.Paletted
,image.RGBA
などの具体的な画像型は、Pix
というバイトスライスフィールドを公開しています。このPix
スライスに直接バイトを書き込むことで、より低レベルで高速なピクセル操作が可能です。copy
関数を使用することで、スライス全体または一部を効率的にコピーできます。
このコミットは、Set
メソッドによるピクセルごとの書き込みから、Pix
フィールドへの直接 copy
操作への移行を通じて、パフォーマンスを向上させています。
技術的詳細
このコミットの主要な最適化は、PNGデコード処理において、デコードされたピクセルデータをGoの image
パッケージの画像構造体(image.Gray
, image.NRGBA
, image.Paletted
, image.RGBA
)に書き込む際の効率を改善することにあります。
以前の実装では、デコードされた各ピクセルに対して SetGray
, SetNRGBA
, SetRGBA
, SetColorIndex
といったメソッドを呼び出し、ピクセルごとに色情報を設定していました。これらの Set
メソッドは、内部で座標計算や型変換、境界チェックなどの処理を行うため、ループ内で大量に呼び出されると、そのオーバーヘッドが無視できないものとなります。
新しい実装では、以下の変更が加えられています。
pixOffset
変数の導入: 各行のピクセルデータを書き込む際のPix
スライス内の開始オフセットを追跡するためにpixOffset
変数が導入されました。これにより、行ごとにStride
を加算するだけで、次の行の開始位置を効率的に計算できます。copy
関数による一括コピー:cbG8
(Gray):gray.SetGray(x, y, color.Gray{cdat[x]})
のようなピクセルごとの設定から、copy(gray.Pix[pixOffset:], cdat)
のように、デコードされた行全体のピクセルデータ (cdat
) をgray.Pix
スライスに一括でコピーするように変更されました。これにより、関数呼び出しのオーバーヘッドが大幅に削減されます。cbP8
(Paletted): 同様に、paletted.SetColorIndex(x, y, cdat[x])
からcopy(paletted.Pix[pixOffset:], cdat)
へと変更されました。パレットインデックスも一括でコピーされます。ただし、パレットインデックスが範囲外でないかのチェックは、copy
の前にループで行われるようになりました。cbTCA8
(NRGBA with Alpha):nrgba.SetNRGBA(x, y, color.NRGBA{cdat[4*x+0], ...})
からcopy(nrgba.Pix[pixOffset:], cdat)
へと変更されました。NRGBAの4バイトデータも一括でコピーされます。
Pix
スライスへの直接書き込み:cbTC8
(RGBA):rgba.SetRGBA(x, y, color.RGBA{cdat[3*x+0], ...})
の代わりに、rgba.Pix
スライスに直接バイトを書き込むように変更されました。具体的には、pix, i, j := rgba.Pix, pixOffset, 0
のようにスライスとインデックスを初期化し、ループ内でpix[i+0] = cdat[j+0]
のように各色成分を直接代入しています。これにより、SetRGBA
メソッドの呼び出しオーバーヘッドが排除されます。RGBAは3バイトの色データと1バイトのアルファデータ(0xff)を組み合わせるため、copy
関数を直接使うのではなく、ループ内でバイトを組み立てる必要がありますが、それでもメソッド呼び出しよりは高速です。
これらの変更により、デコードされたピクセルデータを画像構造体に転送する際のCPUサイクルが大幅に削減され、結果としてPNGデコード全体の速度が向上しました。特に、copy
関数はGoランタイムによって高度に最適化されており、大量のデータを効率的に転送するのに非常に適しています。
また、reader_test.go
には、これらの最適化の効果を測定するための新しいベンチマーク関数が追加されています。これにより、変更が実際にパフォーマンスに貢献していることを数値的に確認できるようになっています。
コアとなるコードの変更箇所
src/pkg/image/png/reader.go
--- a/src/pkg/image/png/reader.go
+++ b/src/pkg/image/png/reader.go
@@ -301,6 +301,7 @@ func (d *decoder) decode() (image.Image, error) {
defer r.Close()
bitsPerPixel := 0
maxPalette := uint8(0)
+ pixOffset := 0 // 新規追加: ピクセルデータ書き込みのオフセット
var (
gray *image.Gray
rgba *image.RGBA
@@ -423,18 +424,24 @@ func (d *decoder) decode() (image.Image, error) {
}
}
case cbG8:
- for x := 0; x < d.width; x++ {
- gray.SetGray(x, y, color.Gray{cdat[x]})
- }
+ // Gray画像のピクセルデータを一括コピー
+ copy(gray.Pix[pixOffset:], cdat)
+ pixOffset += gray.Stride
case cbGA8:
for x := 0; x < d.width; x++ {
ycol := cdat[2*x+0]
nrgba.SetNRGBA(x, y, color.NRGBA{ycol, ycol, ycol, cdat[2*x+1]})
}
case cbTC8:
+ // RGBA画像のピクセルデータを直接操作して書き込み
+ pix, i, j := rgba.Pix, pixOffset, 0
for x := 0; x < d.width; x++ {
- rgba.SetRGBA(x, y, color.RGBA{cdat[3*x+0], cdat[3*x+1], cdat[3*x+2], 0xff})
+ pix[i+0] = cdat[j+0]
+ pix[i+1] = cdat[j+1]
+ pix[i+2] = cdat[j+2]
+ pix[i+3] = 0xff // アルファ値を不透明に設定
+ i += 4 // RGBAは4バイト
+ j += 3 // 元データはRGBで3バイト
}
+ pixOffset += rgba.Stride
case cbP1:
for x := 0; x < d.width; x += 8 {
b := cdat[x/8]
@@ -472,16 +479,18 @@ func (d *decoder) decode() (image.Image, error) {
}
}
case cbP8:
- for x := 0; x < d.width; x++ {
- if cdat[x] > maxPalette {
- return nil, FormatError("palette index out of range")
+ // Paletted画像のピクセルデータを一括コピー
+ if maxPalette != 255 { // パレット範囲チェックが必要な場合
+ for x := 0; x < d.width; x++ {
+ if cdat[x] > maxPalette {
+ return nil, FormatError("palette index out of range")
+ }
}
- paletted.SetColorIndex(x, y, cdat[x])
}
+ copy(paletted.Pix[pixOffset:], cdat)
+ pixOffset += paletted.Stride
case cbTCA8:
- for x := 0; x < d.width; x++ {
- nrgba.SetNRGBA(x, y, color.NRGBA{cdat[4*x+0], cdat[4*x+1], cdat[4*x+2], cdat[4*x+3]})
- }
+ // NRGBA画像のピクセルデータを一括コピー
+ copy(nrgba.Pix[pixOffset:], cdat)
+ pixOffset += nrgba.Stride
case cbG16:
for x := 0; x < d.width; x++ {
ycol := uint16(cdat[2*x+0])<<8 | uint16(cdat[2*x+1])
src/pkg/image/png/reader_test.go
--- a/src/pkg/image/png/reader_test.go
+++ b/src/pkg/image/png/reader_test.go
@@ -10,6 +10,7 @@ import (
"image"
"image/color"
"io"
+ "io/ioutil" // 新規追加: ファイル読み込み用
"os"
"strings"
"testing"
@@ -267,3 +268,41 @@ func TestReaderError(t *testing.T) {
}
}
}\n+\n+// ベンチマークヘルパー関数
+func benchmarkDecode(b *testing.B, filename string, bytesPerPixel int) {
+ b.StopTimer()
+ data, err := ioutil.ReadFile(filename) // テストデータファイルを読み込み
+ if err != nil {
+ b.Fatal(err)
+ }
+ s := string(data)
+ cfg, err := DecodeConfig(strings.NewReader(s)) // 画像設定をデコード
+ if err != nil {
+ b.Fatal(err)
+ }
+ b.SetBytes(int64(cfg.Width * cfg.Height * bytesPerPixel)) // 処理バイト数を設定
+ b.StartTimer()
+ for i := 0; i < b.N; i++ {
+ Decode(strings.NewReader(s)) // デコード処理を実行
+ }
+}\n+\n+// 各カラーモデルのデコードベンチマーク関数
+func BenchmarkDecodeGray(b *testing.B) {
+ benchmarkDecode(b, "testdata/benchGray.png", 1)
+}\n+\n+func BenchmarkDecodeNRGBAGradient(b *testing.B) {
+ benchmarkDecode(b, "testdata/benchNRGBA-gradient.png", 4)
+}\n+\n+func BenchmarkDecodeNRGBAOpaque(b *testing.B) {
+ benchmarkDecode(b, "testdata/benchNRGBA-opaque.png", 4)
+}\n+\n+func BenchmarkDecodePaletted(b *testing.B) {
+ benchmarkDecode(b, "testdata/benchPaletted.png", 1)
+}\n+\n+func BenchmarkDecodeRGB(b *testing.B) {
+ benchmarkDecode(b, "testdata/benchRGB.png", 4)
+}\n```
## コアとなるコードの解説
### `src/pkg/image/png/reader.go` の変更点
* **`pixOffset` の導入**: `decode` 関数内で `pixOffset := 0` が追加されました。これは、各行のピクセルデータを `image.Pix` スライスに書き込む際の開始オフセットを管理するための変数です。行が処理されるたびに `pixOffset += image.Stride` と更新され、次の行の開始位置を効率的に指し示します。
* **`cbG8` (Gray) の最適化**:
* 変更前: `for x := 0; x < d.width; x++ { gray.SetGray(x, y, color.Gray{cdat[x]}) }`
* 変更後: `copy(gray.Pix[pixOffset:], cdat)`
* 解説: 以前は `SetGray` メソッドをピクセルごとに呼び出していましたが、これは各ピクセルに対して関数呼び出しのオーバーヘッドが発生していました。変更後は、デコードされた1行分のグレースケールデータ (`cdat`) を、`gray.Pix` スライスの適切なオフセット位置に `copy` 関数を使って一括でコピーするようにしました。`copy` 関数はGoランタイムによって高度に最適化されており、バイトスライス間のデータ転送を非常に高速に行うことができます。
* **`cbTC8` (RGBA) の最適化**:
* 変更前: `for x := 0; x < d.width; x++ { rgba.SetRGBA(x, y, color.RGBA{cdat[3*x+0], cdat[3*x+1], cdat[3*x+2], 0xff}) }`
* 変更後: `pix, i, j := rgba.Pix, pixOffset, 0` を初期化し、ループ内で `pix[i+0] = cdat[j+0]` のように直接 `rgba.Pix` スライスにバイトを書き込む。
* 解説: `SetRGBA` メソッドの呼び出しを避け、`rgba.Pix` スライスに直接バイトを書き込むことで、関数呼び出しのオーバーヘッドを排除しています。RGBAは4バイト(R, G, B, A)ですが、入力データ `cdat` はRGBの3バイトであるため、アルファ値 `0xff` を明示的に設定しながら、各色成分を個別にコピーしています。これにより、ピクセルごとの処理は残るものの、メソッド呼び出しのコストが削減されます。
* **`cbP8` (Paletted) の最適化**:
* 変更前: `for x := 0; x < d.width; x++ { ... paletted.SetColorIndex(x, y, cdat[x]) }`
* 変更後: `if maxPalette != 255 { ... }` でパレット範囲チェックを行い、その後 `copy(paletted.Pix[pixOffset:], cdat)`
* 解説: `cbG8` と同様に、`SetColorIndex` メソッドの代わりに `copy` 関数を使用して、パレットインデックスデータを一括でコピーするように変更されました。パレットインデックスの範囲チェックは `copy` の前にまとめて行われるようになりました。
* **`cbTCA8` (NRGBA with Alpha) の最適化**:
* 変更前: `for x := 0; x < d.width; x++ { nrgba.SetNRGBA(x, y, color.NRGBA{cdat[4*x+0], ...}) }`
* 変更後: `copy(nrgba.Pix[pixOffset:], cdat)`
* 解説: `cbG8` や `cbP8` と同様に、`SetNRGBA` メソッドの代わりに `copy` 関数を使用して、NRGBAデータを一括でコピーするように変更されました。NRGBAは4バイトのデータが連続しているため、`copy` が非常に効果的です。
これらの変更は、Go言語におけるパフォーマンス最適化の一般的なパターンである「アロケーションを減らす」「関数呼び出しのオーバーヘッドを減らす」「低レベルのデータ操作を効率的に行う」という原則に基づいています。
### `src/pkg/image/png/reader_test.go` の変更点
* **`ioutil` パッケージのインポート**: `io/ioutil` がインポートされ、ファイル読み込みに使用されます。
* **`benchmarkDecode` ヘルパー関数の追加**:
* この関数は、ベンチマークテストの共通ロジックをカプセル化します。
* `ioutil.ReadFile` を使用してPNGテストデータファイルを読み込みます。
* `DecodeConfig` を使用して画像の幅と高さを取得し、`b.SetBytes` で処理されるバイト数を設定します。これにより、ベンチマーク結果がMB/sで表示されるようになります。
* `Decode` 関数を `b.N` 回呼び出し、デコード処理の時間を測定します。
* **各カラーモデルのベンチマーク関数の追加**:
* `BenchmarkDecodeGray`, `BenchmarkDecodeNRGBAGradient`, `BenchmarkDecodeNRGBAOpaque`, `BenchmarkDecodePaletted`, `BenchmarkDecodeRGB` といった具体的なベンチマーク関数が追加されました。これらはそれぞれ異なるカラーモデルのテストPNGファイルを使用し、`benchmarkDecode` ヘルパー関数を呼び出します。
* これらのベンチマークの追加により、PNGデコードのパフォーマンス改善が、特定のカラーモデルにおいて実際にどの程度効果があったかを定量的に測定できるようになりました。
これらのテストコードの追加は、パフォーマンス改善が単なる推測ではなく、具体的な数値として確認できることを保証するために不可欠です。
## 関連リンク
* Go Code Review 6127051: [https://golang.org/cl/6127051](https://golang.org/cl/6127051)
## 参考にした情報源リンク
* Go言語の `image` パッケージに関する公式ドキュメント
* PNGファイルフォーマットの仕様
* Go言語におけるパフォーマンス最適化に関する一般的なプラクティス