PR #25116 opened by romainhedouin URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25116 Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25116.patch
arm64 has no SIMD yadif, so nearly all the time goes to the C loop The spatial checks were branches and the mode was tested for every pixel, which kept the loop scalar Use selects in CHECK() and split the loop on mode & 2 Output is identical: 48840 yadif combinations through ffmpeg (37 pixel formats, 11 sizes, all modes and parities, 1 and 4 threads) and the yadif FATE tests pass I tested on 13.8 million random rows and exhaustively on 1.4 trillion pixel windows with values from a set of three, the results were bit-identical throughout Filter time per frame, arm64 with clang, for the fate-suite sample mpeg2_field_encoding.ts (720x576): 3.0 -> 1.0 ms With the vectoriser disabled: 3.0 -> 2.8 ms The compiled code grows by about 9 kB with this change I found the inefficiency by running performance sweeps of ffmpeg using an LLM and it helped me come up with the solution <!-- If this PR requires new FATE test samples, attach them to the PR and list their target paths below (relative to the fate-suite root). Attached filenames must match the sample's filename: ```fate-samples # e.g. vorbis/new-sample.ogg ``` --> >From a6e17fd7f27bafb699e0da2831d3077cf1adfdaa Mon Sep 17 00:00:00 2001 From: romainhedouin <[email protected]> Date: Fri, 16 Oct 2026 13:44:17 +0200 Subject: [PATCH] avfilter/vf_yadif: let the compiler vectorise filter_line_c arm64 has no SIMD yadif, so nearly all the time goes to the C loop The spatial checks were branches and the mode was tested for every pixel, which kept the loop scalar Use selects in CHECK() and split the loop on mode & 2 Output is identical: 48840 yadif combinations through ffmpeg (37 pixel formats, 11 sizes, all modes and parities, 1 and 4 threads) and the yadif FATE tests pass I tested on 13.8 million random rows and exhaustively on 1.4 trillion pixel windows with values from a set of three, the results were bit-identical throughout Filter time per frame, arm64 with clang, for the fate-suite sample mpeg2_field_encoding.ts (720x576): 3.0 -> 1.0 ms With the vectoriser disabled: 3.0 -> 2.8 ms The compiled code grows by about 9 kB with this change I found the inefficiency by running performance sweeps of ffmpeg using an LLM and it helped me come up with the solution --- libavfilter/vf_yadif.c | 52 +++++++++++++++++++++++++++--------------- 1 file changed, 34 insertions(+), 18 deletions(-) diff --git a/libavfilter/vf_yadif.c b/libavfilter/vf_yadif.c index 7e6ac171f1..b9946ca0c2 100644 --- a/libavfilter/vf_yadif.c +++ b/libavfilter/vf_yadif.c @@ -33,18 +33,23 @@ typedef struct ThreadData { int tff; } ThreadData; -#define CHECK(j)\ +/* Select with a conditional move instead of a branch, so that the compiler can + * vectorize the loops below. cond is false when the previous direction was not + * an improvement, which skips the farther one just like a nested if would. */ +#define CHECK(j, cond)\ { int score = FFABS(cur[mrefs - 1 + (j)] - cur[prefs - 1 - (j)])\ + FFABS(cur[mrefs +(j)] - cur[prefs -(j)])\ + FFABS(cur[mrefs + 1 + (j)] - cur[prefs + 1 - (j)]);\ - if (score < spatial_score) {\ - spatial_score= score;\ - spatial_pred= (cur[mrefs +(j)] + cur[prefs -(j)])>>1;\ + better = (cond) && score < spatial_score;\ + spatial_score = better ? score : spatial_score;\ + spatial_pred = better ? (cur[mrefs + (j)] + cur[prefs - (j)]) >> 1 : spatial_pred;\ + } /* The is_not_edge argument here controls when the code will enter a branch - * which reads up to and including x-3 and x+3. */ + * which reads up to and including x-3 and x+3. The spatial_check argument + * controls the check that reads the lines two above and below. */ -#define FILTER(start, end, is_not_edge) \ +#define FILTER(start, end, is_not_edge, spatial_check) \ for (x = start; x < end; x++) { \ int c = cur[mrefs]; \ int d = (prev2[0] + next2[0])>>1; \ @@ -58,11 +63,12 @@ typedef struct ThreadData { if (is_not_edge) {\ int spatial_score = FFABS(cur[mrefs - 1] - cur[prefs - 1]) + FFABS(c-e) \ + FFABS(cur[mrefs + 1] - cur[prefs + 1]) - 1; \ - CHECK(-1) CHECK(-2) }} }} \ - CHECK( 1) CHECK( 2) }} }} \ + int better; \ + CHECK(-1, 1) CHECK(-2, better) \ + CHECK( 1, 1) CHECK( 2, better) \ }\ \ - if (!(mode&2)) { \ + if (spatial_check) { \ int b = (prev2[2 * mrefs] + next2[2 * mrefs])>>1; \ int f = (prev2[2 * prefs] + next2[2 * prefs])>>1; \ int max = FFMAX3(d - e, d - c, FFMIN(b - c, f - e)); \ @@ -101,8 +107,14 @@ static void filter_line_c(void *dst1, /* The function is called with the pointers already pointing to data[3] and * with 6 subtracted from the width. This allows the FILTER macro to be * called so that it processes all the pixels normally. A constant value of - * true for is_not_edge lets the compiler ignore the if statement. */ - FILTER(0, w, 1) + * true for is_not_edge lets the compiler ignore the if statement. The + * loop is split on spatial_check, because the compiler does not do it and + * cannot vectorize the loop without. */ + if (mode & 2) { + FILTER(0, w, 1, 0) + } else { + FILTER(0, w, 1, 1) + } } #define MAX_ALIGN 8 @@ -122,7 +134,7 @@ static void filter_edges(void *dst1, void *prev1, void *cur1, void *next1, /* Only edge pixels need to be processed here. A constant value of false * for is_not_edge should let the compiler ignore the whole branch. */ - FILTER(0, FFMIN(3, w), 0) + FILTER(0, FFMIN(3, w), 0, !(mode & 2)) dst = (uint8_t*)dst1 + offset; prev = (uint8_t*)prev1 + offset; @@ -131,9 +143,9 @@ static void filter_edges(void *dst1, void *prev1, void *cur1, void *next1, prev2 = (uint8_t*)(parity ? prev : cur); next2 = (uint8_t*)(parity ? cur : next); - FILTER(offset, w - 3, 1) + FILTER(offset, w - 3, 1, !(mode & 2)) offset = FFMAX(offset, w - 3); - FILTER(offset, w, 0) + FILTER(offset, w, 0, !(mode & 2)) } @@ -152,7 +164,11 @@ static void filter_line_c_16bit(void *dst1, mrefs /= 2; prefs /= 2; - FILTER(0, w, 1) + if (mode & 2) { + FILTER(0, w, 1, 0) + } else { + FILTER(0, w, 1, 1) + } } static void filter_edges_16bit(void *dst1, void *prev1, void *cur1, void *next1, @@ -172,7 +188,7 @@ static void filter_edges_16bit(void *dst1, void *prev1, void *cur1, void *next1, mrefs /= 2; prefs /= 2; - FILTER(0, FFMIN(3, w), 0) + FILTER(0, FFMIN(3, w), 0, !(mode & 2)) dst = (uint16_t*)dst1 + offset; prev = (uint16_t*)prev1 + offset; @@ -181,9 +197,9 @@ static void filter_edges_16bit(void *dst1, void *prev1, void *cur1, void *next1, prev2 = (uint16_t*)(parity ? prev : cur); next2 = (uint16_t*)(parity ? cur : next); - FILTER(offset, w - 3, 1) + FILTER(offset, w - 3, 1, !(mode & 2)) offset = FFMAX(offset, w - 3); - FILTER(offset, w, 0) + FILTER(offset, w, 0, !(mode & 2)) } static int filter_slice(AVFilterContext *ctx, void *arg, int jobnr, int nb_jobs) -- 2.54.0 _______________________________________________ ffmpeg-devel mailing list -- [email protected] To unsubscribe send an email to [email protected]
