キャッシュラインの意識は大事という話

43a86362f48371972eaedb51f4323e76?s=47 shirouzu
February 22, 2018

 キャッシュラインの意識は大事という話

キャッシュラインを意識したコードにすることで、スマートフォンカメラ画像の90度回転を80%高速化した話です。
(さらに、i5で計測し直してみると7倍高速化)

また、AVX2 gather I/O での実験コードも追加してみました。

付記(2018/2/26)
(初版のscatter I/Oベースから、gather I/Oベースに更新しました。このあたりの経緯は https://twitter.com/shirouzu/status/967054027048419328 に書いてありますので、興味のある方は参照下さい)

計測用テストコードは下記にあります。
https://github.com/shirouzu/samples/tree/master/fast_rotate

43a86362f48371972eaedb51f4323e76?s=128

shirouzu

February 22, 2018
Tweet

Transcript

  1. キャッシュラインの意識は大事という話 (カメラ画像の90度回転の高速化) (株)朝日ネット 技術戦略研究所 白水啓章 初版 2018/02/22 大幅更新 2018/02/26 Web会議ASPをスマートフォン対応するため、Android/iOS対応していた時の話(2010年頃)

    カメラからの入力画像は、スマートフォンの向きを変えても変化しないため、必要に応じて自前での画像回転の必要。 私の配下の開発メンバが実装したものの、回転処理が入ると一気に性能低下。 プロファイルを取ると、独自映像コーデック部分よりも重く、「そんなバカな?」状態になったが、 キャッシュラインを意識したコードに変更することで、80%高速化したという話。 (そして、最新CoffeeLake世代Core i5で再計測すると、7倍高速化した、という実験も追記) (ちなみにNTTアイティ(現NTT-TX) MeetingPlaza開発部長時代の経験談)
  2. 0 1 2 3 4 5 6 7 62 63

    ... ... ... ... ... ... ... ... ... ... 512 ... ... 575 576 577 ... ... 639 640 641 642 ... ... 703 704 705 706 707 ... ... 767 768 769 770 771 772 ... ... 831 832 833 834 835 836 837 ... ... 895 896 897 898 899 890 891 892 ... ... 959 960 961 962 963 964 965 966 967 … 1024 960 896 832 768 704 640 576 512 ... 0 961 897 833 769 705 641 577 ... ... 1 962 898 834 770 706 642 ... ... 2 964 899 835 771 707 ... ... 3 965 900 836 772 ... ... 4 966 901 837 ... ... 5 967 902 ... ... 6 968 ... ... 7 ... ... ... ... ... ... ... ... ... ... 1024 959 895 831 767 703 639 575 ... 63 カメラ画像を90度回転する (スマートフォンの向きを変えたときに必要な処理) 下記、64x 16 ピクセルでの例 左画像の場合、メモリ上は 0,1,2... と各画素(4バイト)が並んでいる。 (実際のBMP形式は画像の左下列から0,1,2...として上方向に格納する。 判りづらいので、便宜的に左上を0,1,2,...として、下方向に格納するイメージに変更) 回転処理=左画像960番目の画素をメモリ上の先頭に、 その次に、左画像896番目,832, 768 …と並べたい。 右回転により、16 x 64ピクセルに 90度、右回転 やりたいことは「これだけ」だが…
  3. 960 896 832 768 704 640 576 512 ... 0

    961 897 833 769 705 641 577 ... ... 1 962 898 834 770 706 642 ... ... 2 964 899 835 771 707 ... ... 3 965 900 836 772 ... ... 4 966 901 837 ... ... 5 967 902 ... ... 6 968 ... ... 7 ... ... ... ... ... ... ... ... ... ... 1024 959 895 831 767 703 639 575 ... 63 0 1 2 3 4 5 6 7 ... 63 ... ... ... ... ... ... ... ... ... ... 512 ... ... 575 576 577 ... ... 639 640 641 642 ... ... 703 704 705 706 707 ... ... 767 768 769 770 771 772 ... ... 831 832 833 834 835 836 837 ... ... 895 896 897 898 899 890 891 892 ... ... 959 960 961 962 963 964 965 966 967 … 1024 単純なループで代入していくと、とても遅い 1.左画像のロードは、同一キャッシュラインを舐めていくため高速。 2.右画像のストアは、全て新しいキャッシュライン格納となっていて、こちら側の速度がボトルネックとなる。 最も素直な実装だと… 下記、128 x 16 ピクセルでの例 (1ピクセル4byte) 回転の結果、左画像960番目の画素をメモリ上の先頭に、その次に 左画像896番目, 832, 768 …と並べたい。(再掲) アフィン変換: x = (cosΘ * x) - (sinΘ * y) = (0 * x) - (1 * y) = -y (原点回転の場合。上記は127-y) y = (sinΘ * x) + (cosΘ * x) = (1 * x) + (0 * y) = x 右回転により、16 x 128 ピクセルに 90度、右回転 左画像の場合、メモリ上は 0,1,2... と各画素(4バイト)が並んでいる。 (再掲) なお、ARM32のキャッシュラインサイズは8WORD=32byte
  4. 960 896 832 768 704 640 576 512 ... 0

    961 897 833 769 705 641 577 ... ... 1 962 898 834 770 706 642 ... ... 2 964 899 835 771 707 ... ... 3 965 900 836 772 ... ... 4 966 901 837 ... ... 5 967 902 ... ... 6 968 ... ... 7 ... ... ... ... ... ... ... ... ... ... 1024 959 895 831 767 703 639 575 ... 63 0 1 2 3 4 5 6 7 ... 63 ... ... ... ... ... ... ... ... ... ... 512 ... ... 575 576 577 ... ... 639 640 641 642 ... ... 703 704 705 706 707 ... ... 767 768 769 770 771 772 ... ... 831 832 833 834 835 836 837 ... ... 895 896 897 898 899 890 891 892 ... ... 959 960 961 962 963 964 965 966 967 … 1024 90度、右回転 同一キャッシュラインに格納していくため、こちらは速い キャッシュラインを意識したLoadとStoreを行うには…その1 上記は全て、別々のキャッシュラインにロードされる(=遅い) 左画像から(右画像の1キャッシュラインデータ分を)飛び飛びにロードし、右画像に格納する場合、 1.左画像のロードは、全て新しいキャッシュラインへのロードとなっていて、こちらの速度がボトルネックとなる。 2.右画像のストアは、同一キャッシュラインに格納していくため高速。 右と左のコストを逆転させただけでは…?
  5. 960 896 832 768 704 640 576 512 ... 0

    961 897 833 769 705 641 577 ... ... 1 962 898 834 770 706 642 ... ... 2 964 899 835 771 707 ... ... 3 965 900 836 772 ... ... 4 966 901 837 ... ... 5 967 902 ... ... 6 968 ... ... 7 ... ... ... ... ... ... ... ... ... ... 1024 959 895 831 767 703 639 575 ... 63 0 1 2 3 4 5 6 7 ... 63 ... ... ... ... ... ... ... ... ... ... 512 ... ... 575 576 577 ... ... 639 640 641 642 ... ... 703 704 705 706 707 ... ... 767 768 769 770 771 772 ... ... 831 832 833 834 835 836 837 ... ... 895 896 897 898 899 890 891 892 ... ... 959 960 961 962 963 964 965 966 967 … 1024 90度、右回転 前回と同様、同一キャッシュラインに格納していくため、こちらは速い キャッシュラインを意識したLoadとStoreを行うには…その2 青のロードは全て、赤のキャッシュラインと同一(=速い) 次のアクセスでは、右画像データをX方向ではなく、Y方向に成長させると…? 1.左画像のロード(青)は、直前にアクセスした赤のキャッシュラインと同一のため、高速になる。 2.右画像のストア(青)は、同一キャッシュラインに格納していくため高速。 この形のロードを続けていけば…?
  6. 960 896 832 768 704 640 576 512 ... 0

    961 897 833 769 705 641 577 ... ... 1 962 898 834 770 706 642 ... ... 2 964 899 835 771 707 ... ... 3 965 900 836 772 ... ... 4 966 901 837 ... ... 5 967 902 ... ... 6 968 ... ... 7 ... ... ... ... ... ... ... ... ... ... 1024 959 895 831 767 703 639 575 ... 63 0 1 2 3 4 5 6 7 ... 63 ... ... ... ... ... ... ... ... ... ... 512 ... ... 575 576 577 ... ... 639 640 641 642 ... ... 703 704 705 706 707 ... ... 767 768 769 770 771 772 ... ... 831 832 833 834 835 836 837 ... ... 895 896 897 898 899 890 891 892 ... ... 959 960 961 962 963 964 965 966 967 … 1024 90度、右回転 同一キャッシュラインへの格納の繰り返し キャッシュラインを意識したLoadとStoreを行うには…その3 直前ループでアクセス済みキャッシュラインからのロードの繰り返し (緑色に注目。初回の赤色は別々キャッシュラインだったアクセスが、 青色以降は、赤でロード済みのキャッシュラインへのアクセスに変化) キャッシュライン単位コピーの8回目のループが終了すると…
  7. 960 896 832 768 704 640 576 512 ... 0

    961 897 833 769 705 641 577 ... ... 1 962 898 834 770 706 642 ... ... 2 964 899 835 771 707 ... ... 3 965 900 836 772 ... ... 4 966 901 837 ... ... 5 967 902 ... ... 6 968 ... ... 7 ... ... ... ... ... ... ... ... ... ... 1024 959 895 831 767 703 639 575 ... 63 0 1 2 3 4 5 6 7 ... 63 ... ... ... ... ... ... ... ... ... ... 512 ... ... 575 576 577 ... ... 639 640 641 642 ... ... 703 704 705 706 707 ... ... 767 768 769 770 771 772 ... ... 831 832 833 834 835 836 837 ... ... 895 896 897 898 899 890 891 892 ... ... 959 960 961 962 963 964 965 966 967 … 1024 90度、右回転 キャッシュラインを意識したLoadとStoreを行うには…その4 1.左画像では、最初の赤色アクセス以外は全てロード済みキャッシュラインへのアクセスに。 2.右画像では、色単位で、全て同一キャッシュラインへの格納に (つまり、別キャッシュラインアクセス多発(≒キャッシュライン無効化多発)が抑制) これにより、ARM環境で回転処理時間が80%高速化 (実際は、右画像の1ラインを左画像に分散格納して、8ループでライン完成する形だった。上記の形にすれば更に高速化していた…)
  8. さらに… 8年前のARMでの記憶だけだと心許ないので(笑)、 最新CoffeeLake世代の i5-8600K で、このアルゴリズムを再計測。 さらに、AVX2 GETHER命令(散らばったデータを一気に集める命令)を使ったアルゴリズムも 追加して、テストしてみた結果…

  9. (*1) 必要なのはメモリの分散再配置のみのため、AVX2 gather命令だけを活用。(もっと良い手があればお知らせ下さい) (*2) なお、当該関数部分で自動SSE/AVXが使われていないことは、VS2017のアセンブラ出力で確認済み。 ソースコードは、こちら https://github.com/shirouzu/samples/tree/master/fast_rotate 約7倍の高速化 → 8K画像の回転時間を、i5-8600K(CoffeeLake)

    で計測した結果 やはりこれが最速 → なぜわずかに遅くなる → 種類 結果 (ms) 説明 ノーマル版 164ms X方向の全スキャンを、Y列分ループ ノーマル+AVX2版 178ms AVX2(_mm256_i32gather_epi32)利用 (*1) キャッシュライン版 23ms キャッシュライン単位でY方向に移動 (*2) キャッシュライン+AVX2版 20ms 上記+AVX2(_mm256_i32gather_epi32)利用 単純なAVX2利用よりも、キャッシュライン効果の方がずっと高い結果に (両者を組み合わせるのが最強) (かつ、SIMDでキャッシュラインを意識しないのは普通あり得ないよね、という話は別として(笑))
  10. まとめ (離散的なアクセスが発生する場合) キャッシュラインを意識したコードにすると、一気に高速化できることがあります。 おまけ 1.Gather I/O(離散データを連続領域にストア)と Scatter I/O(連続領域を離散領域に分割ストア)が選べる場合、 Gather I/Oの形を選んだ方が圧倒的に良いです。

    (n-Wayアソシエイティブ縛りによる、キャッシュエントリ無効化(=競合性ミス)が多発するとき、 dirtyキャッシュの無効化ペナルティが大きいため。詳しい話は付記にあるURL参照) 2.マルチスレッドでは、スレッド間でキャッシュラインを跨がないことも大事(False Sharing)) 付記: このスライドの初版では、Gather I/O(離散データをロードして連続領域にストア)ではなく、 Scatter I/O(連続領域データを離 散領域に分割ストア)の形でした。 その場合、n-Wayセットアソシエイティブ問題による、競合性ミス多発が顕在化して、キャッシュラインサイズの64byteではなく、 32byteが最速でした。このあたりの経緯は、こちらにあります。 https://twitter.com/shirouzu/status/967054027048419328 (こっちの方が面白いかったりして)