Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Rustの画像処理でSIMDを使ってみる

 Rustの画像処理でSIMDを使ってみる

fmy (FUJINAKA Fumiya)

June 08, 2023
Tweet

Other Decks in Technology

Transcript

  1. 自己紹介 fmy twitter: @fmy4159 仕事: 家族型ロボットを作っている at GROOVE X 趣味:

    GStreamer とかJetson を触っている Qiita:Rust でGStreamer Plugin を書く Qiita:Jetson のカメラドライバを書く
  2. 小さな移動体は電力が厳しい 用途例 本体従量 容量 卓上移動ロボット 500g-2kg 15Wh 家庭屋内移動ロボット 2kg-4kg 85Wh

    屋外移動ロボット 10kg-30kg 200Wh 以上 電気自動車 1680kg 62000Wh 今のロボットは30-90 分程度の稼働、同等以上の時間をかけて充電することが多い 90 分動くとして計算に使える電力としては5-20W ぐらい
  3. SoC の計算能力 よく使われているのはRaspberryPi, Jetson, Snapdragon, Zynq クロックが低くシングルコアでの性能は低め GPU はハード依存が大きい ->

    汎用部分はCPU の性能を活かしたい Arch: aarch64 Core: 2-8 Core Clock: 1GHz-2Ghz Mem: 4-16GB GPU: ものによる その他アクセラレータを持っている
  4. ナイーブな実装 u8 単位でシフトして上書きする pub fn t16_to_raw16_u8(buf: &mut [u8]) { for

    i in 0..buf.len() / 2 { let i: usize = i * 2; buf[i] = buf[i] >> 4 | buf[i+1] << 4; buf[i+1] >>= 4; } }
  5. ベンチマーク 1.2MPixel(Quad-VGA12, 1280×960) ぐらいのデータを使う CPU: AMD Ryzen 5 3400G (Base

    3.7GHz, Boost 4.2GHz) Rust: 1.70.0(stable) criterion でベンチマーク format use u8 time: [1.1869 ms 1.1900 ms 1.1935 ms] カメラが5-60fps(200ms-16ms) で他の処理もあることを想定すると 1ms は十分早いとは言えない
  6. u8 アセンブリ .LBB6_2: lea rcx, [rax - 1] cmp rcx,

    rsi jae .LBB6_7 cmp rax, rsi jae .LBB6_6 # for ループ抜け movzx r9d, byte ptr [rdi + rax - 1] # 読み出し movzx ecx, byte ptr [rdi + rax] shr r9b, 4 mov edx, ecx shl dl, 4 or dl, r9b # 下位8bit mov byte ptr [rdi + rax - 1], dl # 書き込み shr cl, 4 # 上位8bit mov byte ptr [rdi + rax], cl # 書き込み add rax, 2 dec r8 jne .LBB6_2
  7. 遅い理由の考察 1. shift 操作3 回 i. 16bit の単位で見たら全部右シフトで同じ操作にできる 2. mov

    操作4 回 i. load とstore の2 回が理想 3. pixel 数x2 のループ回数 2. 64bit 単位で1 回にできるのでは? i. 64bit 単位ならループ回数が1/4 に減る
  8. u64 実装 pub fn t16_to_raw16_u64(buf: &mut [u8]) { for i

    in 0..buf.len() / 8 { let i: usize = i * 8; let mut a = LittleEndian::read_u64(&buf[i..i + 8]); *x = (*x & 0xfff0fff0fff0fff0) >> 4; LittleEndian::write_u64(&mut buf[i..i + 8], a); } } byteorder を使ってu64 として操作する
  9. ベンチマーク with u64 1/4 になった! loop 回数削減( pixel/2 -> pixel/8

    ) の影響が大きい? format use u8 time: [1.1869 ms 1.1900 ms 1.1935 ms] format use u64 time: [210.27 µs 211.84 µs 213.57 µs]
  10. u64 のアセンブリ シフト1 回、move も読み出しと書き込みの2 回 単純な操作は削減効果は少ないが、ループ展開は効くようだ .LBB2_2: cmp rax,

    rsi ja .LBB2_5 mov r8, qword ptr [rdi + rax - 8] shr r8, 4 and r8, rdx mov qword ptr [rdi + rax - 8], r8 add rax, 8 dec rcx jne .LBB2_2
  11. u128 Rust にはu128 がプリミティブとして用意されている。 これはどうなる? pub fn t16_to_raw16_u128(buf: &mut [u8])

    { for i in 0..buf.len() / 16 { let i: usize = i * 16; let mut a = LittleEndian::read_u128(&buf[i..i + 16]); a = (a & 0xfff0fff0fff0fff0fff0fff0fff0fff0) >> 4; LittleEndian::write_u128(&mut buf[i..i + 16], a); } }
  12. ベンチマーク with u128 u16 も比較対象に追加: 命令が単純になった分高速化している u128 はu64 より早い format

    use u8 time: [1.1869 ms 1.1900 ms 1.1935 ms] format use u16 time: [706.00 µs 710.77 µs 716.34 µs] format use u64 time: [210.27 µs 211.84 µs 213.57 µs] format use u128 time: [159.99 µs 164.70 µs 169.74 µs]
  13. U128 ASM 処理自体は64bit と同じだがループ回数が少なくなっている 高速化要因はループ展開でオーバーヘッド削減したから? .LBB3_2: cmp rax, rsi ja

    .LBB3_5 mov r8, qword ptr [rdi + rax - 16] mov r9, qword ptr [rdi + rax - 8] shr r8, 4 shr r9, 4 and r9, rdx and r8, rdx mov qword ptr [rdi + rax - 16], r8 mov qword ptr [rdi + rax - 8], r9 add rax, 16 dec rcx jne .LBB3_2
  14. SSE2 /// # Safety #[target_feature(enable = "sse2")] #[cfg(any(target_arch = "x86",

    target_arch = "x86_64"))] pub unsafe fn t16_to_raw16_unroll_128(buf: &mut [u8]) { #[cfg(target_arch = "x86")] use std::arch::x86::*; #[cfg(target_arch = "x86_64")] use std::arch::x86_64::*; #[allow(overflowing_literals)] let shift4 = _mm_setr_epi16(4, 0, 0, 0, 0, 0, 0, 0); for i in 0..buf.len() / 16 { let i: usize = i * 16; let invec = _mm_loadu_si128(buf.as_ptr().add(i) as *const _); let shifted = _mm_srl_epi16(invec, shift4); // 論理右シフト _mm_storeu_si128(buf.as_mut_ptr().add(i) as *mut _, shifted); } }
  15. AVX2 /// # Safety #[target_feature(enable = "avx2")] #[cfg(any(target_arch = "x86",

    target_arch = "x86_64"))] pub unsafe fn t16_to_raw16_unroll_256(buf: &mut [u8]) { #[cfg(target_arch = "x86")] use std::arch::x86::*; #[cfg(target_arch = "x86_64")] use std::arch::x86_64::*; #[allow(overflowing_literals)] let shift4 = _mm_setr_epi16(4, 0, 0, 0, 0, 0, 0, 0); for i in 0..buf.len() / 32 { let i: usize = i * 32; let invec = _mm256_loadu_si256(buf.as_ptr().add(i) as *const _); let shifted = _mm256_srl_epi16(invec, shift4); // 論理右シフト _mm256_storeu_si256(buf.as_mut_ptr().add(i) as *mut _, shifted); } }
  16. ベンチマーク with SIMD SSE2 はさらに半分ぐらいになった AVX2 は右シフト1 回程度の操作の場合ではあまり効果がない? format use

    u8 time: [1.1869 ms 1.1900 ms 1.1935 ms] format use u16 time: [706.00 µs 710.77 µs 716.34 µs] format use u64 time: [210.27 µs 211.84 µs 213.57 µs] format use u128 time: [159.99 µs 164.70 µs 169.74 µs] format use 128 SSE2 time: [95.795 µs 97.490 µs 99.380 µs] format use 256 AVX2 time: [92.268 µs 92.971 µs 93.704 µs]
  17. SSE2 ASM ループ展開されている。これも高速化の一因か .LBB4_8: movdqu xmm0, xmmword ptr [rdx -

    48] movdqu xmm1, xmmword ptr [rdx - 32] movdqu xmm2, xmmword ptr [rdx - 16] movdqu xmm3, xmmword ptr [rdx] psrlw xmm0, 4 movdqu xmmword ptr [rdx - 48], xmm0 psrlw xmm1, 4 movdqu xmmword ptr [rdx - 32], xmm1 psrlw xmm2, 4 movdqu xmmword ptr [rdx - 16], xmm2 add rcx, 4 psrlw xmm3, 4 movdqu xmmword ptr [rdx], xmm3 add rdx, 64 cmp rsi, rcx jne .LBB4_8
  18. Jetson でも実験をする 手元にある Jetson Orin Nano 8GB 6-core Arm® Cortex®-A78AE

    v8.2 64-bit CPU 1.5MB L2 + 4MB L3 1.5Ghz シングルコアのクロック数は3400G の1/2 以下
  19. aarch64 Neon ARM のSIMD 拡張命令としてNeon がある /// # Safety #[cfg(target_arch

    = "aarch64")] pub unsafe fn t16_to_raw16_unroll_128(buf: &mut [u8]) { use std::arch::aarch64::*; const SHIFT4_VEC: [i16; 8] = [-4, -4, -4, -4, -4, -4, -4, -4]; let shift4 = vld1q_s16(SHIFT4_VEC.as_ptr() as *const _); #[allow(clippy::never_loop)] for i in 0..buf.len() / 16 { let i: usize = i * 16; let invec = vld1q_u16(buf.as_ptr().add(i) as *const _); let res = vshlq_u16(invec, shift4); vst1q_u16(buf.as_mut_ptr().add(i) as *mut _, res); } }
  20. ベンチマークその4 CPU: Cortex®-A78AE v8.2 64-bit 1.5GHz Rust: 1.70.0(stable) format use

    u8 time: [2.6863 ms 2.6927 ms 2.7000 ms] format use u16 time: [1.3942 ms 1.3950 ms 1.3964 ms] format use u64 time: [399.52 µs 402.12 µs 404.56 µs] format use u128 time: [280.36 µs 280.38 µs 280.41 µs] format use 128 neon time: [144.28 µs 144.79 µs 145.35 µs] クロックが遅いためかu8 では2.7ms もかかる u128 で1/10 になっている SIMD を使うとx86 の処理時間に肉薄する
  21. aarch64 u128 x86_64 と同様にu64 に加えてループ展開されている .LBB3_2: add x8, x10, #16

    cmp x8, x1 b.hi .LBB3_5 add x11, x0, x10 subs x9, x9, #1 ldp x10, x12, [x11] lsr x10, x10, #4 lsr x12, x12, #4 and x13, x10, #0xfff0fff0fff0fff and x12, x12, #0xfff0fff0fff0fff mov x10, x8 stp x13, x12, [x11] b.ne .LBB3_2
  22. Additional もっと強いCPU の場合はどうなる? CPU: Intel(R) Core(TM) i7-12700 4.7GHz SIMD 使えば早いというわけでもないらしい

    format use u8 time: [875.07 µs 876.88 µs 878.68 µs] format use u16 time: [435.49 µs 436.66 µs 437.85 µs] format use u64 time: [109.95 µs 110.36 µs 110.86 µs] format use u128 time: [77.175 µs 77.540 µs 77.984 µs] format use 128 SSE2 time: [83.290 µs 83.452 µs 83.639 µs] format use 256 AVX2 time: [86.874 µs 87.127 µs 87.402 µs]
  23. まとめ cargo-asm とても便利 同一命令操作は最適化で性能を上げる余地はある 64bitCPU なら64bit 単位の処理で書くことで命令数を減らせる u128 を64bit 環境使った場合64bit

    命令のループ展開になる SIMD は多くの場合には高速化が見込めそう データや状況によるので効果の計測を行うのが大事