[PR] avutil/base64: add aarch64 NEON implementation for base64 encode (PR #23682)

Shreesh Adiga via ffmpeg-devel <[email protected]>
Newsgroups gmane.comp.video.ffmpeg.devel
Message-ID <178301078407.59.10188498754807883034@29965ddac10e>
PR #23682 opened by Shreesh Adiga (tantei3)
URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/23682
Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/23682.patch

Add NEON implementation using LD3 to load 48 bytes with de-interleaving
using which 6 bits are extracted using USHR and SLI to get 4 registers, which
is used as a index into lookup table using TBL with 4 registers to translate
into the 64-byte output, which is written using ST4 to de-interleave.
Add a checkasm test to validate the input lengths from 0 to 239 bytes to cover
all the code paths introduced in the NEON implementation.

Observed about 4x speedup by checkasm on A720 core on 1kB input:
```
C:
 - base64.base64 [OK]
NEON:
 - base64.base64 [OK]
checkasm: all 1 tests passed
Benchmark results:
  name                      nsecs (vs ref)
  av_base64_encode_c:      1286.9
  av_base64_encode_neon:    315.5 ( 4.08x)
```


>From 7aa79a908ecfcb09790983b8470be33685c69b50 Mon Sep 17 00:00:00 2001
From: Shreesh Adiga <[email protected]>
Date: Thu, 2 Jul 2026 21:54:16 +0530
Subject: [PATCH] avutil/base64: add aarch64 NEON implementation for base64
 encode

Add NEON implementation using LD3 to load 48 bytes with de-interleaving
using which 6 bits are extracted using USHR and SLI to get 4 registers, which
is used as a index into lookup table using TBL with 4 registers to translate
into the 64-byte output, which is written using ST4 to de-interleave.
Add a checkasm test to validate the input lengths from 0 to 239 bytes to cover
all the code paths introduced in the NEON implementation.

Observed about 4x speedup by checkasm on A720 core on 1kB input:
C:
 - base64.base64 [OK]
NEON:
 - base64.base64 [OK]
checkasm: all 1 tests passed
Benchmark results:
  name                      nsecs (vs ref)
  av_base64_encode_c:      1286.9
  av_base64_encode_neon:    315.5 ( 4.08x)
---
 libavutil/aarch64/Makefile |   3 +-
 libavutil/aarch64/base64.S | 160 +++++++++++++++++++++++++++++++++++++
 libavutil/aarch64/base64.h |  31 +++++++
 libavutil/base64.c         |   9 +++
 tests/checkasm/Makefile    |   1 +
 tests/checkasm/base64.c    | 130 ++++++++++++++++++++++++++++++
 tests/checkasm/checkasm.c  |   1 +
 tests/checkasm/checkasm.h  |   1 +
 8 files changed, 335 insertions(+), 1 deletion(-)
 create mode 100644 libavutil/aarch64/base64.S
 create mode 100644 libavutil/aarch64/base64.h
 create mode 100644 tests/checkasm/base64.c

diff --git a/libavutil/aarch64/Makefile b/libavutil/aarch64/Makefile
index 8a7e7ca057..fffeca96ee 100644
--- a/libavutil/aarch64/Makefile
+++ b/libavutil/aarch64/Makefile
@@ -4,7 +4,8 @@ OBJS += aarch64/cpu.o                                                 \
 
 ARMV8-OBJS += aarch64/crc.o
 
-NEON-OBJS += aarch64/float_dsp_neon.o                                 \
+NEON-OBJS += aarch64/base64.o                                         \
+             aarch64/float_dsp_neon.o                                 \
              aarch64/tx_float_neon.o                                  \
 
 NEON-OBJS-$(CONFIG_PIXELUTILS) += aarch64/pixelutils_neon.o
diff --git a/libavutil/aarch64/base64.S b/libavutil/aarch64/base64.S
new file mode 100644
index 0000000000..776930b3b3
--- /dev/null
+++ b/libavutil/aarch64/base64.S
@@ -0,0 +1,160 @@
+/*
+ * Copyright (c) 2026 Shreesh Adiga <[email protected]>
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with FFmpeg; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "asm.S"
+
+function ff_base64_encode_neon, export=1
+          // x0 - char *out
+          // x1 - const char *in
+          // x2 - int in_size
+          // x3 - const char *tbl
+        cmp             x2, #48
+        add             x4, x2, x1 // end = src + in_size
+        mov             x5, x0 // dst = out
+        b.lt            2f // skip NEON loop
+        ld1             { v0.16b - v3.16b }, [x3]
+        movi            v4.16b, #63
+        sub             x6, x4, #48
+
+1: // NEON 48bytes loop
+        ld3             { v5.16b - v7.16b }, [x1], #48
+        ushr            v18.16b, v5.16b, #2
+        ushr            v16.16b, v6.16b, #4
+        tbl             v18.16b, { v0.16b, v1.16b, v2.16b, v3.16b }, v18.16b
+        ushr            v17.16b, v7.16b, #6
+        sli             v16.16b, v5.16b, #4
+        and             v5.16b, v7.16b, v4.16b
+        sli             v17.16b, v6.16b, #2
+        tbl             v21.16b, { v0.16b, v1.16b, v2.16b, v3.16b }, v5.16b
+        and             v16.16b, v16.16b, v4.16b
+        cmp             x1, x6
+        tbl             v19.16b, { v0.16b, v1.16b, v2.16b, v3.16b }, v16.16b
+        and             v17.16b, v17.16b, v4.16b
+        tbl             v20.16b, { v0.16b, v1.16b, v2.16b, v3.16b }, v17.16b
+        st4             { v18.16b - v21.16b }, [x5], #64
+        b.ls            1b // NEON 48bytes loop
+        sub             x6, x4, #24
+        cmp             x1, x6
+        b.hi            2f
+
+        ld3             { v5.8b - v7.8b }, [x1], #24
+        ushr            v18.8b, v5.8b, #2
+        ushr            v16.8b, v6.8b, #4
+        tbl             v18.8b, { v0.16b, v1.16b, v2.16b, v3.16b }, v18.8b
+        ushr            v17.8b, v7.8b, #6
+        sli             v16.8b, v5.8b, #4
+        and             v5.8b, v7.8b, v4.8b
+        sli             v17.8b, v6.8b, #2
+        tbl             v21.8b, { v0.16b, v1.16b, v2.16b, v3.16b }, v5.8b
+        and             v16.8b, v16.8b, v4.8b
+        tbl             v19.8b, { v0.16b, v1.16b, v2.16b, v3.16b }, v16.8b
+        and             v17.8b, v17.8b, v4.8b
+        tbl             v20.8b, { v0.16b, v1.16b, v2.16b, v3.16b }, v17.8b
+        st4             { v18.8b - v21.8b }, [x5], #32
+2: // skip NEON loop
+        sub             x6, x4, #4
+        cmp             x1, x6
+        b.hi            4f
+
+3: // 4bytes available loop
+        ldr             w7, [x1], #3
+        rev             w7, w7
+        lsr             x8, x7, #26
+        ubfx            x9, x7, #20, #6
+        ldrb            w12, [x3, x8]
+        ubfx            x10, x7, #14, #6
+        ldrb            w13, [x3, x9]
+        ubfx            x11, x7, #8, #6
+        ldrb            w14, [x3, x10]
+        orr             w12, w12, w13, lsl #8
+        cmp             x1, x6
+        ldrb            w15, [x3, x11]
+        orr             w12, w12, w14, lsl #16
+        orr             w12, w12, w15, lsl #24
+        str             w12, [x5], #4
+        b.ls            3b // 4bytes available loop
+
+4:
+        sub             x6, x4, x1
+        cmp             x6, #3
+        b.eq            7f
+        cmp             x6, #2
+        b.eq            6f
+        cmp             x6, #1
+        b.eq            5f
+        strb            wzr, [x5]
+        ret
+
+5:
+          // 1 byte input remaining, 2 bytes padding needed
+        ldrb            w7, [x1]
+        lsr             x8, x7, #2
+        ubfiz           x9, x7, #4, #2
+        ldrb            w8, [x3, x8]
+        movz            w10, #0x3d3d, lsl #16
+        ldrb            w9, [x3, x9]
+        orr             w10, w10, w8
+        orr             w10, w10, w9, lsl #8
+        str             w10, [x5]
+        strb            wzr, [x5, #4]
+        ret
+
+6:
+          // 2 bytes input remaining, 1 byte padding needed
+        ldrb            w7, [x1]
+        ldrb            w8, [x1, #1]
+        lsr             w9, w7, #2
+        lsr             w10, w8, #4
+        ubfiz           w11, w8, #2, #4
+        ldrb            w12, [x3, x9]
+        bfi             w10, w7, #4, #2
+        ldrb            w14, [x3, x11]
+        movz            w15, #0x3d00, lsl #16
+        ldrb            w13, [x3, x10]
+        orr             w15, w15, w12
+        orr             w15, w15, w14, lsl #16
+        orr             w15, w15, w13, lsl #8
+        str             w15, [x5]
+        strb            wzr, [x5, #4]
+        ret
+
+7:
+          // 3 bytes input remaining, no padding needed
+        ldrb            w7, [x1]
+        ldrb            w8, [x1, #1]
+        lsr             w10, w7, #2
+        ldrb            w9, [x1, #2]
+        lsr             w11, w8, #4
+        ldrb            w10, [x3, x10]
+        lsr             w12, w9, #6
+        bfi             w11, w7, #4, #2
+        bfi             w12, w8, #2, #4
+        ldrb            w11, [x3, x11]
+        and             w13, w9, #0x3f
+        ldrb            w12, [x3, x12]
+        orr             w10, w10, w11, lsl #8
+        ldrb            w13, [x3, x13]
+        orr             w10, w10, w12, lsl #16
+        orr             w10, w10, w13, lsl #24
+        str             w10, [x5]
+        strb            wzr, [x5, #4]
+        ret
+
+endfunc
diff --git a/libavutil/aarch64/base64.h b/libavutil/aarch64/base64.h
new file mode 100644
index 0000000000..7201b76ce1
--- /dev/null
+++ b/libavutil/aarch64/base64.h
@@ -0,0 +1,31 @@
+/*
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+#ifndef AVUTIL_AARCH64_BASE64_H
+#define AVUTIL_AARCH64_BASE64_H
+
+#include "libavutil/cpu.h"
+#include "libavutil/aarch64/cpu.h"
+
+static inline int ff_is_neon_supported(void) {
+    int cpu_flags = av_get_cpu_flags();
+
+    return have_neon(cpu_flags);
+}
+
+char *ff_base64_encode_neon(char *out, const uint8_t *in, int in_size, const char *tbl);
+#endif /* AVUTIL_AARCH64_BASE64_H */
diff --git a/libavutil/base64.c b/libavutil/base64.c
index 69e11e6f5e..b16993a723 100644
--- a/libavutil/base64.c
+++ b/libavutil/base64.c
@@ -31,6 +31,10 @@
 #include "error.h"
 #include "intreadwrite.h"
 
+#if ARCH_AARCH64
+#include "libavutil/aarch64/base64.h"
+#endif
+
 /* ---------------- private code */
 static const uint8_t map2[256] =
 {
@@ -156,6 +160,11 @@ char *av_base64_encode(char *out, int out_size, const uint8_t *in, int in_size)
     if (in_size >= UINT_MAX / 4 ||
         out_size < AV_BASE64_SIZE(in_size))
         return NULL;
+#if ARCH_AARCH64
+    if (ff_is_neon_supported()) {
+        return ff_base64_encode_neon(out, in, in_size, b64);
+    }
+#endif
     ret = dst = out;
     while (bytes_remaining > 3) {
         i_bits = AV_RB32(in);
diff --git a/tests/checkasm/Makefile b/tests/checkasm/Makefile
index c154d19ed4..d79a6b8b55 100644
--- a/tests/checkasm/Makefile
+++ b/tests/checkasm/Makefile
@@ -100,6 +100,7 @@ CHECKASMOBJS-$(CONFIG_SWSCALE)  += $(SWSCALEOBJS)
 # libavutil tests
 AVUTILOBJS                              += aes.o
 AVUTILOBJS                              += av_tx.o
+AVUTILOBJS                              += base64.o
 AVUTILOBJS                              += crc.o
 AVUTILOBJS                              += fixed_dsp.o
 AVUTILOBJS                              += float_dsp.o
diff --git a/tests/checkasm/base64.c b/tests/checkasm/base64.c
new file mode 100644
index 0000000000..28846d0fa6
--- /dev/null
+++ b/tests/checkasm/base64.c
@@ -0,0 +1,130 @@
+/*
+ * Copyright (c) 2026 Shreesh Adiga <[email protected]>
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with FFmpeg; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "checkasm.h"
+#include "libavutil/base64.h"
+#include "libavutil/mem_internal.h"
+#include "libavutil/error.h"
+#include "libavutil/intreadwrite.h"
+#if ARCH_AARCH64
+#include "libavutil/aarch64/base64.h"
+#endif
+
+
+#define BUF_SIZE (1024)
+
+static char *base64_encode_ref(char *out, int out_size, const uint8_t *in, int in_size)
+{
+    static const char b64[] =
+        "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+    char *ret, *dst;
+    unsigned i_bits = 0;
+    int i_shift = 0;
+    int bytes_remaining = in_size;
+
+    if (in_size >= UINT_MAX / 4 ||
+        out_size < AV_BASE64_SIZE(in_size))
+        return NULL;
+
+    ret = dst = out;
+    while (bytes_remaining > 3) {
+        i_bits = AV_RB32(in);
+        in += 3; bytes_remaining -= 3;
+        *dst++ = b64[ i_bits>>26        ];
+        *dst++ = b64[(i_bits>>20) & 0x3F];
+        *dst++ = b64[(i_bits>>14) & 0x3F];
+        *dst++ = b64[(i_bits>>8 ) & 0x3F];
+    }
+    i_bits = 0;
+    while (bytes_remaining) {
+        i_bits = (i_bits << 8) + *in++;
+        bytes_remaining--;
+        i_shift += 8;
+    }
+    while (i_shift > 0) {
+        *dst++ = b64[(i_bits << 6 >> i_shift) & 0x3f];
+        i_shift -= 6;
+    }
+    while ((dst - ret) & 3)
+        *dst++ = '=';
+    *dst = '\0';
+
+    return ret;
+}
+
+#if ARCH_AARCH64
+static char *base64_encode_neon(char *out, int out_size, const uint8_t *in, int in_size)
+{
+    static const char b64[] =
+        "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+
+    if (in_size >= UINT_MAX / 4 ||
+        out_size < AV_BASE64_SIZE(in_size))
+        return NULL;
+
+    return ff_base64_encode_neon(out, in, in_size, b64);
+}
+#endif
+
+char *(*base64_func)(char *, int, const uint8_t *, int);
+
+static void base64_encode_init_fn(void) {
+#if ARCH_AARCH64
+    int cpu_flags = av_get_cpu_flags();
+    if (have_neon(cpu_flags))
+        base64_func = base64_encode_neon;
+    else
+#endif
+        base64_func = base64_encode_ref;
+}
+
+static void check_base64_encode(void)
+{
+    LOCAL_ALIGNED_32(uint8_t, buf, [BUF_SIZE]);
+    LOCAL_ALIGNED_32(uint8_t, out1, [AV_BASE64_SIZE(BUF_SIZE)]);
+    LOCAL_ALIGNED_32(uint8_t, out2, [AV_BASE64_SIZE(BUF_SIZE)]);
+
+    for (int i = 0; i < BUF_SIZE; i++)
+        buf[i] = rnd();
+
+    base64_encode_init_fn();
+    declare_func(char *, char *out, int out_size, const uint8_t *in, int in_size);
+
+    func_type *fn = base64_func;
+
+    if (check_func(fn, "av_base64_encode")) {
+        for (int i = 0; i < 48 * 5 - 1; i++) {
+            size_t offset = rnd() % (BUF_SIZE - i);
+            call_new(out1, AV_BASE64_SIZE(BUF_SIZE), buf + offset, i);
+            call_ref(out2, AV_BASE64_SIZE(BUF_SIZE), buf + offset, i);
+            if (memcmp(out1, out2, AV_BASE64_SIZE(i)))
+                fail();
+        }
+
+        bench_new(out1, AV_BASE64_SIZE(BUF_SIZE), buf, BUF_SIZE);
+    }
+}
+
+
+void checkasm_check_base64(void)
+{
+    check_base64_encode();
+    report("base64");
+}
diff --git a/tests/checkasm/checkasm.c b/tests/checkasm/checkasm.c
index 4f54db8e1d..5858c8c03a 100644
--- a/tests/checkasm/checkasm.c
+++ b/tests/checkasm/checkasm.c
@@ -304,6 +304,7 @@ static const CheckasmTest tests[] = {
 #endif
 #if CONFIG_AVUTIL
         { "aes",       checkasm_check_aes },
+        { "base64",    checkasm_check_base64 },
         { "crc",       checkasm_check_crc },
         { "fixed_dsp", checkasm_check_fixed_dsp },
         { "float_dsp", checkasm_check_float_dsp },
diff --git a/tests/checkasm/checkasm.h b/tests/checkasm/checkasm.h
index 612ad0f9b9..2fad8f197e 100644
--- a/tests/checkasm/checkasm.h
+++ b/tests/checkasm/checkasm.h
@@ -44,6 +44,7 @@ void checkasm_check_alacdsp(void);
 void checkasm_check_apv_dsp(void);
 void checkasm_check_audiodsp(void);
 void checkasm_check_av_tx(void);
+void checkasm_check_base64(void);
 void checkasm_check_blackdetect(void);
 void checkasm_check_blend(void);
 void checkasm_check_blockdsp(void);
-- 
2.52.0

_______________________________________________
ffmpeg-devel mailing list -- [email protected]
To unsubscribe send an email to [email protected]
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.