Goでの画像処理のパフォーマンスを改善した話 CPUキャッシュ、スライス


ウォーターマークをスクラッチでGoで実装した過程で、あまたの改善を行いましたが、その一つを紹介。

「画像のブロック処理のために、先んじて1次元スライスを並べ替える。」です。 メモリ効率は改善しましたが、速度については残念ながら期待する結果が得られませんでした。

BeforeAfter
/img/2026-01-09_16-49-11.png
/img/2026-01-09_16-45-46.png
スライスを左上から行優先スライスを左上からブロック/行優先

「ブロック」とは画像の分割したまとまりを指します。 例えば「4ピクセル四方のブロックに分割する」としたとき、80ピクセル四方の画像は、横に20個、縦に20個の計400個のブロックに分割できます。

ウォーターマークのアルゴリズムでは、画像の各ブロックに対してやや重たい計算します。 この時、データの順番を予め並び替えることで、CPUキャッシュを活用し、かつスライスのコピーをなくすことで、速度を改善を試みました。

実装詳細

もとのスライスから取得するためにコピーを複数回、かつ計算結果を戻すのに同じだけコピーを行います。 このコピーをしないように、取りやすい形にスライスを並べ替えておくのが今回の改善です。


src := []int{.....}
// block 0番目を取りたい!
// Before
// src の 10 ~ 13, 24 ~ 27, xxx, xxx の4箇所からデータを取らないと行けない!
copy(dist[0], src[0])
copy(dist[1], src[1])
copy(dist[2], src[2])
copy(dist[3], src[3])
// After
// 先にシャッフル
// 一度だけ参照すればOK!
return src[0:8:8]

詳細は以下。

// Before
wavelets := dwt.HaarDWT(img, srcWidth, nil)
src := wavelets[0] // cA
blockAt := 0
for startY := 0; startY < waveletHeight; startY += waveletBlockHeight {
    for startX := 0; startX < waveletWidth; startX += waveletBlockWidth {
        // ここでsrcからコピー
        b := get(src, startX, startY)

        // 計算: DCT -> SVD -> 計算 -> 逆変換で戻す
        v, idct := dct.Exec(b)
        s, isvd, _ := svd.Exec(v)
        bit := getMarkBit(blockAt)
        s[0], s[1] = embedFunc(s[0], s[1], bit)
        isvd()
        idct()
        // 再びsrcへコピー
        set(src, startX, startY, b)
        blockAt++
    }
}
dist := dwt.HaarIDWT(wavelets, srcWidth, srcHeight, nil)
_ = dist


// After
// 並べ替え用のIndexマップを作成
blockMap := dwt.NewBlockMap(waveletWidth, waveletHeight, waveletBlockWidth, waveletBlockHeight).GetMap()
// 並べ替え済みの一次元スライス
wavelets := dwt.HaarDWT(img, srcWidth, blockMap)
src := wavelets[0] // cA
totalBlocks := (waveletWidth / waveletBlockWidth) * (waveletHeight / waveletBlockHeight)
for at := range totalBlocks {
    // srcの参照
    block := src[at*waveletBlockWidth*waveletBlockHeight : (at+1)*waveletBlockWidth*waveletBlockHeight : (at+1)*waveletBlockWidth*waveletBlockHeight]
    v, idct := dct.Exec(block)
    s, isvd, _ := svd.Exec(v)
    bit := getMarkBit(at)
    s[0], s[1] = embedFunc(s[0], s[1], bit)
    isvd()
    idct()
    // それぞれ参照先へ逆変換を割り当てているためsrcに値が戻る
}
dist := dwt.HaarIDWT(wavelets, srcWidth, srcHeight, blockMap)
_ = dist

コード: https://github.com/yyyoichi/watermark_zero/commit/6cbec1bb68adbe2ecd96ae8b93d896311e2f380d

計測結果

▶ ベンチマーク詳細

ブロックサイズは8(wavelets変換で4になります)で、SHD, FHD, 4K サイズで比較しました。

go test ./bench -benchmem -bench BenchmarkBlockMap --benchtime 10s -cpu 1,2,3,4 -count 2
goos: linux
goarch: amd64
pkg: github.com/yyyoichi/watermark_zero/bench
cpu: Intel(R) Core(TM) i5-8250U CPU @ 1.60GHz
BenchmarkBlockMap/noBlockMap_1280x720                         96         121957257 ns/op        43928577 B/op     432065 allocs/op
BenchmarkBlockMap/noBlockMap_1280x720                         87         119609855 ns/op        43911349 B/op     432040 allocs/op
BenchmarkBlockMap/noBlockMap_1280x720-2                      102         116399734 ns/op        43918409 B/op     432052 allocs/op
BenchmarkBlockMap/noBlockMap_1280x720-2                      103         115051393 ns/op        43918628 B/op     432054 allocs/op
BenchmarkBlockMap/noBlockMap_1280x720-3                      104         114422260 ns/op        43924385 B/op     432060 allocs/op
BenchmarkBlockMap/noBlockMap_1280x720-3                       96         114612915 ns/op        43925061 B/op     432062 allocs/op
BenchmarkBlockMap/noBlockMap_1280x720-4                      104         114336974 ns/op        43928029 B/op     432064 allocs/op
BenchmarkBlockMap/noBlockMap_1280x720-4                       88         114416441 ns/op        43927747 B/op     432063 allocs/op
BenchmarkBlockMap/withBlockMap_1280x720                       86         117949790 ns/op        41157083 B/op     417646 allocs/op
BenchmarkBlockMap/withBlockMap_1280x720                       88         122248001 ns/op        41145883 B/op     417632 allocs/op
BenchmarkBlockMap/withBlockMap_1280x720-2                     88         118033696 ns/op        41150078 B/op     417639 allocs/op
BenchmarkBlockMap/withBlockMap_1280x720-2                     86         117828413 ns/op        41150080 B/op     417639 allocs/op
BenchmarkBlockMap/withBlockMap_1280x720-3                     88         117709383 ns/op        41154761 B/op     417644 allocs/op
BenchmarkBlockMap/withBlockMap_1280x720-3                     88         117762902 ns/op        41155050 B/op     417645 allocs/op
BenchmarkBlockMap/withBlockMap_1280x720-4                     87         117927198 ns/op        41156935 B/op     417645 allocs/op
BenchmarkBlockMap/withBlockMap_1280x720-4                     90         117957209 ns/op        41157569 B/op     417646 allocs/op
BenchmarkBlockMap/noBlockMap_1920x1080                        44         254826147 ns/op        98821574 B/op     972065 allocs/op
BenchmarkBlockMap/noBlockMap_1920x1080                        44         264941760 ns/op        98802756 B/op     972038 allocs/op
BenchmarkBlockMap/noBlockMap_1920x1080-2                      45         258642176 ns/op        98811652 B/op     972053 allocs/op
BenchmarkBlockMap/noBlockMap_1920x1080-2                      43         255359455 ns/op        98809893 B/op     972052 allocs/op
BenchmarkBlockMap/noBlockMap_1920x1080-3                      43         255276244 ns/op        98818638 B/op     972061 allocs/op
BenchmarkBlockMap/noBlockMap_1920x1080-3                      45         255466291 ns/op        98817710 B/op     972060 allocs/op
BenchmarkBlockMap/noBlockMap_1920x1080-4                      44         256113600 ns/op        98823189 B/op     972067 allocs/op
BenchmarkBlockMap/noBlockMap_1920x1080-4                      45         256175622 ns/op        98820985 B/op     972066 allocs/op
BenchmarkBlockMap/withBlockMap_1920x1080                      43         265378407 ns/op        92588334 B/op     939647 allocs/op
BenchmarkBlockMap/withBlockMap_1920x1080                      40         271193171 ns/op        92576906 B/op     939634 allocs/op
BenchmarkBlockMap/withBlockMap_1920x1080-2                    42         262745753 ns/op        92579560 B/op     939638 allocs/op
BenchmarkBlockMap/withBlockMap_1920x1080-2                    42         262224028 ns/op        92580600 B/op     939640 allocs/op
BenchmarkBlockMap/withBlockMap_1920x1080-3                    42         263259068 ns/op        92586136 B/op     939647 allocs/op
BenchmarkBlockMap/withBlockMap_1920x1080-3                    42         263080892 ns/op        92586029 B/op     939646 allocs/op
BenchmarkBlockMap/withBlockMap_1920x1080-4                    42         263668150 ns/op        92587988 B/op     939647 allocs/op
BenchmarkBlockMap/withBlockMap_1920x1080-4                    43         263996282 ns/op        92587382 B/op     939646 allocs/op
BenchmarkBlockMap/noBlockMap_3840x2160                        10        1013895857 ns/op        395058336 B/op   3888064 allocs/op
BenchmarkBlockMap/noBlockMap_3840x2160                        10        1021764291 ns/op        395041662 B/op   3888044 allocs/op
BenchmarkBlockMap/noBlockMap_3840x2160-2                      10        1007760872 ns/op        395049423 B/op   3888053 allocs/op
BenchmarkBlockMap/noBlockMap_3840x2160-2                      12        1009751876 ns/op        395050734 B/op   3888055 allocs/op
BenchmarkBlockMap/noBlockMap_3840x2160-3                      10        1009557056 ns/op        395055084 B/op   3888065 allocs/op
BenchmarkBlockMap/noBlockMap_3840x2160-3                      12        1011831316 ns/op        395053463 B/op   3888062 allocs/op
BenchmarkBlockMap/noBlockMap_3840x2160-4                      10        1012322098 ns/op        395057068 B/op   3888062 allocs/op
BenchmarkBlockMap/noBlockMap_3840x2160-4                      10        1012902691 ns/op        395056931 B/op   3888065 allocs/op
BenchmarkBlockMap/withBlockMap_3840x2160                      10        1050244693 ns/op        370169001 B/op   3758449 allocs/op
BenchmarkBlockMap/withBlockMap_3840x2160                      10        1069734389 ns/op        370156368 B/op   3758431 allocs/op
BenchmarkBlockMap/withBlockMap_3840x2160-2                    10        1059889272 ns/op        370163527 B/op   3758442 allocs/op
BenchmarkBlockMap/withBlockMap_3840x2160-2                    10        1050247929 ns/op        370163057 B/op   3758441 allocs/op
BenchmarkBlockMap/withBlockMap_3840x2160-3                    10        1047074380 ns/op        370168936 B/op   3758451 allocs/op
BenchmarkBlockMap/withBlockMap_3840x2160-3                    10        1047546085 ns/op        370167361 B/op   3758450 allocs/op
BenchmarkBlockMap/withBlockMap_3840x2160-4                    10        1047713375 ns/op        370170285 B/op   3758450 allocs/op
BenchmarkBlockMap/withBlockMap_3840x2160-4                    10        1053755803 ns/op        370169924 B/op   3758450 allocs/op

マルチコアでも対して実行速度に差がない

メモリの割当効率は改善しましたが、速度はほとんど変化なし・やや遅いという結果でした。

改善

改善策として、スライスの順番を変更するときに、ループで一つずつ回しているため、効率が悪い可能性があります。 時間があれば改善、再計測可能かもです。

// 現状の一次元スライスの変換方法
blockMap := []int{
    1,2,5,6,9,10,
    3,4,7,8,11,12,
    13,14,17,18,21,22,
    15,16,19,20,23,24, 
}
for i, v := range blockMap {
    dist[i] = src[v]
}

// 以下のようにできれば、あるいは?
for i, chunk := range chunks {
    copy(dist[chunk], src[chunk])
}

原因反省

CPUキャッシュを活かす方向で「ランダムアクセスを減らそう」とする意図もこの事前並べ替えの目的にありましたが、 大して活かせていないのかもしれないです。

原因考察はAIに事実認定されるのも嫌なので書かないでおきます。

以上!

背景

「わたしが描いた」を証明できるウォーターマークツール作っています。

https://zerome.me

インターネット上のだれでも「あたなの作品なんだね」と分かります。あとから「これは無断転載です」「これはAIじゃありません」といえるように作りました。


も参照してください