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]

Reply via email to