PR #25799 opened by zhujing
URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25799
Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25799.patch
swscale/aarch64: use register tbl-transpose for the 4-tap hscale
Replace the store-to-stack + ld4 transpose in ff_hscale8to15_4_neon with a
register ins + tbl transpose carried out entirely in caller-saved registers
(no stack). Uses function/endfunc and portable const/movrel idioms for the
tbl index tables, keeps 8-pixel vector processing down to a <=7-pixel scalar
tail, and is bit-exact.
Verified on Kunpeng 950:
1. Environment
- CPU: Kunpeng 950 (HiSilicon, 192 cores, SVE 256-bit)
- OS/compiler: openEuler 24.03 (LTS-SP2) / gcc 12.3.1
- configure: --disable-doc --disable-ffplay --disable-debug
--disable-network --disable-autodetect
2. Functional (bit-exact)
./tests/checkasm/checkasm --test=sw_scale
-> checkasm: all 1560 tests passed (incl. sw_scale.hscale [OK], no FAIL)
3. Performance (checkasm --bench, Timing=linux(perf), unit=cycles/call,
ff_hscale8to15_4_neon = hscale fs_4 8-to-15, new vs old NEON)
dstW new(cycles) old(cycles) speedup
8 11.1 38.5 3.47x
24 35.9 65.4 1.82x
128 199.4 300.2 1.51x
144 224.3 336.3 1.50x
256 400.3 588.8 1.47x
512 801.6 1165.7 1.45x
-> about 1.5x faster than the previous NEON 4-tap hscale at typical
widths (dstW >= 128)
4. Baseline
39ccb3ca official master (stock NEON) as the "old" column above. The 4-tap
build under test differs from 39ccb3ca only in the ff_hscale8to15_4_neon
kernel (fs8 and 8-to-19 changes excluded from this measurement).
5. FATE
make fate-checkasm: 99 tests, 0 failures (incl. checkasm-sw_scale).
Full make fate not run: no fate-suite samples available on this box.
6. Conclusion
new / old cycles ratio ~1.5x at dstW >= 128, larger at the small (tail)
widths. Attributable to the 4-tap change alone.
Signed-off-by: zhujing <[email protected]>
>From 7b14e0c6056f7998d043ea6f64e2358103111f9b Mon Sep 17 00:00:00 2001
From: zhujing <[email protected]>
Date: Wed, 4 Nov 2026 20:10:23 +0800
Subject: [PATCH] swscale/aarch64: use register tbl-transpose for the 4-tap
hscale
Replace the store-to-stack + ld4 transpose in ff_hscale8to15_4_neon with a
register ins + tbl transpose carried out entirely in caller-saved registers
(no stack). Uses function/endfunc and portable const/movrel idioms for the
tbl index tables, keeps 8-pixel vector processing down to a <=7-pixel scalar
tail, and is bit-exact.
Verified on Kunpeng 950:
1. Environment
- CPU: Kunpeng 950 (HiSilicon, 192 cores, SVE 256-bit)
- OS/compiler: openEuler 24.03 (LTS-SP2) / gcc 12.3.1
- configure: --disable-doc --disable-ffplay --disable-debug
--disable-network --disable-autodetect
2. Functional (bit-exact)
./tests/checkasm/checkasm --test=sw_scale
-> checkasm: all 1560 tests passed (incl. sw_scale.hscale [OK], no FAIL)
3. Performance (checkasm --bench, Timing=linux(perf), unit=cycles/call,
ff_hscale8to15_4_neon = hscale fs_4 8-to-15, new vs old NEON)
dstW new(cycles) old(cycles) speedup
8 11.1 38.5 3.47x
24 35.9 65.4 1.82x
128 199.4 300.2 1.51x
144 224.3 336.3 1.50x
256 400.3 588.8 1.47x
512 801.6 1165.7 1.45x
-> about 1.5x faster than the previous NEON 4-tap hscale at typical
widths (dstW >= 128)
4. Baseline
39ccb3ca official master (stock NEON) as the "old" column above. The 4-tap
build under test differs from 39ccb3ca only in the ff_hscale8to15_4_neon
kernel (fs8 and 8-to-19 changes excluded from this measurement).
5. FATE
make fate-checkasm: 99 tests, 0 failures (incl. checkasm-sw_scale).
Full make fate not run: no fate-suite samples available on this box.
6. Conclusion
new / old cycles ratio ~1.5x at dstW >= 128, larger at the small (tail)
widths. Attributable to the 4-tap change alone.
Signed-off-by: zhujing <[email protected]>
---
libswscale/aarch64/hscale.S | 246 +++++++++++++++---------------------
1 file changed, 105 insertions(+), 141 deletions(-)
diff --git a/libswscale/aarch64/hscale.S b/libswscale/aarch64/hscale.S
index 6c14246a42b..1f83ad3b99c 100644
--- a/libswscale/aarch64/hscale.S
+++ b/libswscale/aarch64/hscale.S
@@ -198,153 +198,117 @@ function ff_hscale8to15_X4_neon, export=1
ret
endfunc
+// 4-tap hscale with register tbl-transpose.
+//
+// For each group of 8 output pixels the 4 contiguous source bytes of each
+// pixel are gathered into two 128-bit words, and the four byte-planes (one
+// per filter tap, across all 8 pixels) are extracted with a register tbl.
+// This avoids the store-to-stack + ld4 transpose of the reference kernel,
+// keeping the gather path in registers. Uses only caller-saved registers,
+// so no callee-saved saves or stack allocation are required.
+const .Lhsc_e_idx0, align=4
+ .byte 0, 4, 8, 12, 16, 20, 24, 28, 0, 0, 0, 0, 0, 0, 0, 0
+endconst
+const .Lhsc_e_idx1, align=4
+ .byte 1, 5, 9, 13, 17, 21, 25, 29, 0, 0, 0, 0, 0, 0, 0, 0
+endconst
+const .Lhsc_e_idx2, align=4
+ .byte 2, 6, 10, 14, 18, 22, 26, 30, 0, 0, 0, 0, 0, 0, 0, 0
+endconst
+const .Lhsc_e_idx3, align=4
+ .byte 3, 7, 11, 15, 19, 23, 27, 31, 0, 0, 0, 0, 0, 0, 0, 0
+endconst
function ff_hscale8to15_4_neon, export=1
// x0 SwsInternal *c (not used)
// x1 int16_t *dst
-// x2 int dstW
+// w2 int dstW
// x3 const uint8_t *src
// x4 const int16_t *filter
// x5 const int32_t *filterPos
-// x6 int filterSize
-// x8-x15 registers for gathering src data
-
-// v0 madd accumulator 4S
-// v1-v4 filter values (16 bit) 8H
-// v5 madd accumulator 4S
-// v16-v19 src values (8 bit) 8B
-
-// This implementation has 4 sections:
-// 1. Prefetch src data
-// 2. Interleaved prefetching src data and madd
-// 3. Complete madd
-// 4. Complete remaining iterations when dstW % 8 != 0
-
- sub sp, sp, #32 // allocate 32 bytes on
the stack
- cmp w2, #16 // if dstW <16, skip to
the last block used for wrapping up
- b.lt 2f
-
- // load 8 values from filterPos to be used as offsets into src
- ldp w8, w9, [x5] // filterPos[idx + 0],
[idx + 1]
- ldp w10, w11, [x5, #8] // filterPos[idx + 2],
[idx + 3]
- ldp w12, w13, [x5, #16] // filterPos[idx + 4],
[idx + 5]
- ldp w14, w15, [x5, #24] // filterPos[idx + 6],
[idx + 7]
- add x5, x5, #32 // advance filterPos
-
- // gather random access data from src into contiguous memory
- ldr w8, [x3, w8, uxtw] // src[filterPos[idx +
0]][0..3]
- ldr w9, [x3, w9, uxtw] // src[filterPos[idx +
1]][0..3]
- ldr w10, [x3, w10, uxtw] // src[filterPos[idx +
2]][0..3]
- ldr w11, [x3, w11, uxtw] // src[filterPos[idx +
3]][0..3]
- ldr w12, [x3, w12, uxtw] // src[filterPos[idx +
4]][0..3]
- ldr w13, [x3, w13, uxtw] // src[filterPos[idx +
5]][0..3]
- ldr w14, [x3, w14, uxtw] // src[filterPos[idx +
6]][0..3]
- ldr w15, [x3, w15, uxtw] // src[filterPos[idx +
7]][0..3]
- stp w8, w9, [sp] // *scratch_mem = {
src[filterPos[idx + 0]][0..3], src[filterPos[idx + 1]][0..3] }
- stp w10, w11, [sp, #8] // *scratch_mem = {
src[filterPos[idx + 2]][0..3], src[filterPos[idx + 3]][0..3] }
- stp w12, w13, [sp, #16] // *scratch_mem = {
src[filterPos[idx + 4]][0..3], src[filterPos[idx + 5]][0..3] }
- stp w14, w15, [sp, #24] // *scratch_mem = {
src[filterPos[idx + 6]][0..3], src[filterPos[idx + 7]][0..3] }
-
-1:
- ld4 {v16.8b, v17.8b, v18.8b, v19.8b}, [sp] // transpose 8
bytes each from src into 4 registers
-
- // load 8 values from filterPos to be used as offsets into src
- ldp w8, w9, [x5] // filterPos[idx +
0][0..3], [idx + 1][0..3], next iteration
- ldp w10, w11, [x5, #8] // filterPos[idx +
2][0..3], [idx + 3][0..3], next iteration
- ldp w12, w13, [x5, #16] // filterPos[idx +
4][0..3], [idx + 5][0..3], next iteration
- ldp w14, w15, [x5, #24] // filterPos[idx +
6][0..3], [idx + 7][0..3], next iteration
-
- movi v0.16b, #0 // Clear madd accumulator
for idx 0..3
- movi v5.16b, #0 // Clear madd accumulator
for idx 4..7
-
- ld4 {v1.8h, v2.8h, v3.8h, v4.8h}, [x4], #64 // load filter
idx + 0..7
-
- add x5, x5, #32 // advance filterPos
-
- // interleaved SIMD and prefetching intended to keep ld/st and vector
pipelines busy
- uxtl v16.8h, v16.8b // unsigned extend long,
convert src data to 16-bit
- uxtl v17.8h, v17.8b // unsigned extend long,
convert src data to 16-bit
- ldr w8, [x3, w8, uxtw] // src[filterPos[idx +
0]], next iteration
- ldr w9, [x3, w9, uxtw] // src[filterPos[idx +
1]], next iteration
- uxtl v18.8h, v18.8b // unsigned extend long,
convert src data to 16-bit
- uxtl v19.8h, v19.8b // unsigned extend long,
convert src data to 16-bit
- ldr w10, [x3, w10, uxtw] // src[filterPos[idx +
2]], next iteration
- ldr w11, [x3, w11, uxtw] // src[filterPos[idx +
3]], next iteration
-
- smlal v0.4s, v1.4h, v16.4h // multiply accumulate
inner loop j = 0, idx = 0..3
- smlal v0.4s, v2.4h, v17.4h // multiply accumulate
inner loop j = 1, idx = 0..3
- ldr w12, [x3, w12, uxtw] // src[filterPos[idx +
4]], next iteration
- ldr w13, [x3, w13, uxtw] // src[filterPos[idx +
5]], next iteration
- smlal v0.4s, v3.4h, v18.4h // multiply accumulate
inner loop j = 2, idx = 0..3
- smlal v0.4s, v4.4h, v19.4h // multiply accumulate
inner loop j = 3, idx = 0..3
- ldr w14, [x3, w14, uxtw] // src[filterPos[idx +
6]], next iteration
- ldr w15, [x3, w15, uxtw] // src[filterPos[idx +
7]], next iteration
-
- smlal2 v5.4s, v1.8h, v16.8h // multiply accumulate
inner loop j = 0, idx = 4..7
- smlal2 v5.4s, v2.8h, v17.8h // multiply accumulate
inner loop j = 1, idx = 4..7
- stp w8, w9, [sp] // *scratch_mem = {
src[filterPos[idx + 0]][0..3], src[filterPos[idx + 1]][0..3] }
- stp w10, w11, [sp, #8] // *scratch_mem = {
src[filterPos[idx + 2]][0..3], src[filterPos[idx + 3]][0..3] }
- smlal2 v5.4s, v3.8h, v18.8h // multiply accumulate
inner loop j = 2, idx = 4..7
- smlal2 v5.4s, v4.8h, v19.8h // multiply accumulate
inner loop j = 3, idx = 4..7
- stp w12, w13, [sp, #16] // *scratch_mem = {
src[filterPos[idx + 4]][0..3], src[filterPos[idx + 5]][0..3] }
- stp w14, w15, [sp, #24] // *scratch_mem = {
src[filterPos[idx + 6]][0..3], src[filterPos[idx + 7]][0..3] }
-
- sub w2, w2, #8 // dstW -= 8
- sqshrn v0.4h, v0.4s, #7 // shift and clip the
2x16-bit final values
- sqshrn v1.4h, v5.4s, #7 // shift and clip the
2x16-bit final values
- st1 {v0.4h, v1.4h}, [x1], #16 // write to dst[idx + 0..7]
- cmp w2, #16 // continue on main loop
if there are at least 16 iterations left
- b.ge 1b
-
- // last full iteration
- ld4 {v16.8b, v17.8b, v18.8b, v19.8b}, [sp]
- ld4 {v1.8h, v2.8h, v3.8h, v4.8h}, [x4], #64 // load filter
idx + 0..7
-
- movi v0.16b, #0 // Clear madd accumulator
for idx 0..3
- movi v5.16b, #0 // Clear madd accumulator
for idx 4..7
-
- uxtl v16.8h, v16.8b // unsigned extend long,
convert src data to 16-bit
- uxtl v17.8h, v17.8b // unsigned extend long,
convert src data to 16-bit
- uxtl v18.8h, v18.8b // unsigned extend long,
convert src data to 16-bit
- uxtl v19.8h, v19.8b // unsigned extend long,
convert src data to 16-bit
-
- smlal v0.4s, v1.4h, v16.4h // multiply accumulate
inner loop j = 0, idx = 0..3
- smlal v0.4s, v2.4h, v17.4h // multiply accumulate
inner loop j = 1, idx = 0..3
- smlal v0.4s, v3.4h, v18.4h // multiply accumulate
inner loop j = 2, idx = 0..3
- smlal v0.4s, v4.4h, v19.4h // multiply accumulate
inner loop j = 3, idx = 0..3
-
- smlal2 v5.4s, v1.8h, v16.8h // multiply accumulate
inner loop j = 0, idx = 4..7
- smlal2 v5.4s, v2.8h, v17.8h // multiply accumulate
inner loop j = 1, idx = 4..7
- smlal2 v5.4s, v3.8h, v18.8h // multiply accumulate
inner loop j = 2, idx = 4..7
- smlal2 v5.4s, v4.8h, v19.8h // multiply accumulate
inner loop j = 3, idx = 4..7
-
- subs w2, w2, #8 // dstW -= 8
- sqshrn v0.4h, v0.4s, #7 // shift and clip the
2x16-bit final values
- sqshrn v1.4h, v5.4s, #7 // shift and clip the
2x16-bit final values
- st1 {v0.4h, v1.4h}, [x1], #16 // write to dst[idx + 0..7]
-
- cbnz w2, 2f // if >0 iterations
remain, jump to the wrap up section
-
- add sp, sp, #32 // clean up stack
- ret
-
- // finish up when dstW % 8 != 0 or dstW < 16
-2:
- // load src
- ldr w8, [x5], #4 // filterPos[i]
- add x9, x3, w8, uxtw // calculate the address
for src load
- ld1 {v5.s}[0], [x9] // src[filterPos[i] + 0..3]
- // load filter
- ld1 {v6.4h}, [x4], #8 // filter[filterSize * i +
0..3]
-
- uxtl v5.8h, v5.8b // unsigned extend long,
convert src data to 16-bit
- smull v0.4s, v5.4h, v6.4h // 4 iterations of
src[...] * filter[...]
- addv s0, v0.4s // add up products of src
and filter values
- sqshrn h0, s0, #7 // shift and clip the
2x16-bit final value
- st1 {v0.h}[0], [x1], #2 // dst[i] = ...
- sub w2, w2, #1 // dstW--
- cbnz w2, 2b
-
- add sp, sp, #32 // clean up stack
+// w6 int filterSize (not used; fixed at 4)
+//
+// v0 accumulator for output pixels 0..3
+// v5 accumulator for output pixels 4..7
+// v16-v19 the four byte-planes (tap 0..3) after the tbl transpose
+// v24-v27 the four tbl index vectors
+// v28-v29 the packed 8 gathered source windows
+ cmp w2, #8
+ b.lt .Lhscale4_tail
+ movrel x6, .Lhsc_e_idx0
+ movrel x7, .Lhsc_e_idx1
+ movrel x8, .Lhsc_e_idx2
+ movrel x9, .Lhsc_e_idx3
+ ldr q24, [x6]
+ ldr q25, [x7]
+ ldr q26, [x8]
+ ldr q27, [x9]
+.Lhscale4_loop:
+ // gather the 8 filter positions for this group
+ ldp w8, w9, [x5]
+ ldp w10, w11, [x5, #8]
+ ldp w12, w13, [x5, #16]
+ ldp w14, w15, [x5, #24]
+ add x5, x5, #32
+ // gather the 8 source windows (4 contiguous bytes each)
+ ldr w8, [x3, w8, uxtw]
+ ldr w9, [x3, w9, uxtw]
+ ldr w10, [x3, w10, uxtw]
+ ldr w11, [x3, w11, uxtw]
+ ldr w12, [x3, w12, uxtw]
+ ldr w13, [x3, w13, uxtw]
+ ldr w14, [x3, w14, uxtw]
+ ldr w15, [x3, w15, uxtw]
+ // pack the 8 gathered words into two 128-bit registers
+ ins v28.s[0], w8
+ ins v28.s[1], w9
+ ins v28.s[2], w10
+ ins v28.s[3], w11
+ ins v29.s[0], w12
+ ins v29.s[1], w13
+ ins v29.s[2], w14
+ ins v29.s[3], w15
+ // transpose: byte-plane j holds tap j of all 8 pixels
+ tbl v16.8b, {v28.16b, v29.16b}, v24.8b
+ tbl v17.8b, {v28.16b, v29.16b}, v25.8b
+ tbl v18.8b, {v28.16b, v29.16b}, v26.8b
+ tbl v19.8b, {v28.16b, v29.16b}, v27.8b
+ ld4 {v1.8h, v2.8h, v3.8h, v4.8h}, [x4], #64
+ uxtl v16.8h, v16.8b // unsigned extend the
byte-planes
+ uxtl v17.8h, v17.8b
+ uxtl v18.8h, v18.8b
+ uxtl v19.8h, v19.8b
+ movi v0.16b, #0
+ movi v5.16b, #0
+ smlal v0.4s, v1.4h, v16.4h // taps 0..3 for pixels 0..3
+ smlal v0.4s, v2.4h, v17.4h
+ smlal v0.4s, v3.4h, v18.4h
+ smlal v0.4s, v4.4h, v19.4h
+ smlal2 v5.4s, v1.8h, v16.8h // taps 0..3 for pixels 4..7
+ smlal2 v5.4s, v2.8h, v17.8h
+ smlal2 v5.4s, v3.8h, v18.8h
+ smlal2 v5.4s, v4.8h, v19.8h
+ sqshrn v0.4h, v0.4s, #7 // >>7 and saturate-narrow
to 16-bit
+ sqshrn v1.4h, v5.4s, #7
+ st1 {v0.4h, v1.4h}, [x1], #16 // write 8 output pixels
+ sub w2, w2, #8
+ cmp w2, #8
+ b.ge .Lhscale4_loop
+.Lhscale4_tail:
+ cbz w2, .Lhscale4_end
+.Lhscale4_tail_loop:
+ // scalar tail for the remaining dstW % 8 (1..7) pixels
+ ldr w8, [x5], #4 // filterPos[i]
+ add x9, x3, w8, uxtw
+ ld1 {v5.s}[0], [x9] // src[filterPos[i] + 0..3]
+ ld1 {v6.4h}, [x4], #8 // filter[4*i + 0..3]
+ uxtl v5.8h, v5.8b
+ smull v0.4s, v5.4h, v6.4h
+ addv s0, v0.4s
+ sqshrn h0, s0, #7
+ st1 {v0.h}[0], [x1], #2
+ subs w2, w2, #1
+ b.ne .Lhscale4_tail_loop
+.Lhscale4_end:
ret
endfunc
--
2.54.0
_______________________________________________
ffmpeg-devel mailing list -- [email protected]
To unsubscribe send an email to [email protected]