[1m[0m[1mHello Beastie and Google Summer of Code![0m

    01-06-2024

    [m<!-- markdown-toc start - Don't edit this section. Run M-x markdown-toc-refresh-toc -->[0m

    [1mTable of Contents[0m

    [m  · [0m[1mHello Beastie and Google Summer of Code![0m [0m
        [4m#hello-beastie-and-google-summer-of-code[0m 
        [m  · [0m[1mBackground[0m [4m#background[0m 
        [m  · [0m[1mWhy write assembly[0m [4m#why-write-assembly[0m 
            [m  · [0m[1mArchitectural levels[0m [4m#architectural-levels[0m 
        [m  · [0m[1mamd64 strlen implementation[0m [4m#amd64-strlen-implementation[0m 
            [m  · [0m[1mSubstituting missing instructions[0m [0m
                [4m#substituting-missing-instructions[0m 
        [m  · [0m[1mstrlen PoC[0m [4m#strlen-poc[0m 
            [m  · [0m[1mSimple strlen[0m [4m#simple-strlen[0m 
            [m  · [0m[1mNaïve implementation[0m [4m#naïve-implementation[0m 
            [m  · [0m[1mImproved implementation[0m [4m#improved-implementation[0m 
            [m  · [0m[1mFCMP to avoid GPR move[0m [4m#fcmp-to-avoid-gpr-move[0m 
        [m  · [0m[1mlibc integration[0m [4m#libc-integration[0m 
        [m  · [0m[1mTests[0m [4m#tests[0m 
        [m  · [0m[1mBenchmarks[0m [4m#benchmarks[0m 
        [m  · [0m[1mWhat[0m[1m’[0m[1ms next[0m [4m#whats-next[0m 
        [m  · [0m[1mReferences[0m [4m#references[0m 
    [m  · [0m[1mBonus: Hello World in Aarch64 Assembly[0m [0m
        [4m#bonus-hello-world-in-aarch64-assembly[0m 

    [m<!-- markdown-toc end -->[0m

    I have been admitted to be a part of Google Summer of Code (GSoC)
    2024! [[1m1[0m [4mhttps://summerofcode.withgoogle.com/[0m]

    Participating in GSoC has been on my radar for many years but I
    always thought, oh I’ll do it next summer, well that summer is now!
    :D

[1m## [0m[1mBackground[0m

    The admission process to GSoC is as follows: Organizations publish
    some suggested projects with possible mentors, students get in touch
    with said mentors or come up with their own project and convince
    someone to mentor them. Then voting takes place and the top N
    applicants for the organizations are accepted where N is the numbers
    of slots Google has allocated for that organization. So I found a
    project that I thought sounded interesting, got in touch with the
    mentor and sent in a proposal [[1m2[0m [0m
    [4mhttps://dflund.se/~getz/GSOC/FreeBSDproposal.txt[0m] through google’s
    portal, the project was posted on the FreeBSD GSoC ideas page [[1m3[0m [0m
    [4mhttps://wiki.freebsd.org/SummerOfCodeIdeas#Port_of_libc_SIMD_enhancements_to_other_architectures[0m].
    The project is to port SIMD enhanced string functions in libc from
    amd64 (x86_64) to arm64 (Aarch64).

    A great incentive to the program is that you get the chance to be
    mentored by member(s) of the community. I have the pleasure to be
    mentored by two wonderful people, Robert Clausecker <fuz> and Ed
    Maste <emaste>. Another GSoC contributor is porting the same
    algorithms to RISC-V, he is basing his implementation on the base
    RISC-V ISA using SIMD Within A Register (SWAR) techniques, and thus
    not having any dependency on processor extensions such as the
    recently ratified 1.0 RISC-V Vector Extension for which almost no
    hardware is currently available (and no hardware running FreeBSD).
    His blog documenting his adventures is available at [[1m4[0m [0m
    [4mhttps://strajabot.com/[0m].

    As for me, I’ve been using FreeBSD for a few years now, before that I
    used Linux but after frustration over lack of good documentation and
    a fragmented system I switched and haven’t looked back. I still enjoy
    the linux kernel but userland and distros is something I just want
    out of my way. 

    But this is not an article about why FreeBSD is superior to Linux, if
    it even is (yes, in some regards, but ymmv). But rather about my
    project for the summer.

[1m## [0m[1mWhy write assembly[0m

    Most libc functions on other platforms already benefit from being
    handwritten in assembly, both scalar and SIMD variants. Using SIMD
    instructions for string functions are particularly unfit for a
    autovectorizing compiler as we make atypical use of SIMD
    instructions. For the scalar implementations we may use some Bit
    Twiddling Hacks such as those on Sean Eron Anderson’s site [[1m5[0m [0m
    [4mhttps://graphics.stanford.edu/~seander/bithacks.html[0m].

    Compilers also struggle reasoning to decide which operations to do in
    GPRs and which to do in vector registers. Register allocation is also
    a problem for amd64 and the compiler may spill into the stack whereas
    for handwritten assembly you would be left with registers to spare.
    This is not really as extreme of a case on arm64 as we have way more
    registers than amd64 to play around with.

    Another compelling reason to have performance critical libc functions
    written in assembly is that all programs that link against libc will
    benefit from these improvements. Although this will put some
    additional pressure on me as an implementer as the code cant break
    other peoples programs just because they abused libc in interesting
    ways. An example of that is how memcmp on FreeBSD differs from the
    ISO/IEC 9899:1999 requirement’s. In particular FreeBSD documents that
    memcmp returns the difference between the first two mismatching
    characters, as opposed to merely returning a negative/positive
    integer or zero.

    But my project will solely deal with using Arm NEON instructions to
    simd-ify the string functions, although bit-twiddling GPRs does sound
    enticing.

[1m### [0m[1mArchitectural levels[0m

    The AMD64 SysV ABI supplement defines the following architecture
    levels, where for FreeBSD we have implementations for most of the
    string functions in libc for scalar and baseline. Users are able to
    choose which level of enhancements to use using the [1mARCHLEVEL[0m flag. A
    complete list of enhanced functions are available in the simd(7)
    manpage [[1m6[0m [0m
    [4mhttps://man.freebsd.org/cgi/man.cgi?query=simd&manpath=FreeBSD+15.0-CURRENT[0m].

    [1m  | [0m[1mscalar    scalar enhancements only (no SIMD)[0m
    [1m  | [0m[1m[0m
    [1m  | [0m[1mbaseline      cmov, cx8, x87 FPU, fxsr, MMX, osfxsr, SSE, SSE2[0m
    [1m  | [0m[1m[0m
    [1m  | [0m[1mx86-64-v2  cx16,    lahf/sahf, popcnt, SSE3, SSSE3, SSE4.1, SSE4.2[0m
    [1m  | [0m[1m[0m
    [1m  | [0m[1mx86-64-v3  AVX, AVX2, BMI1, BMI2, F16C, FMA, lzcnt, movbe, osxsave[0m
    [1m  | [0m[1m[0m
    [1m  | [0m[1mx86-64-v4  AVX-512F/BW/CD/DQ/VL[0m

[1m## [0m[1mamd64 strlen implementation[0m

    The amd64 strlen implementation consists of two parts, a scalar one
    (using bit-twiddling) and a Vectorized one (baseline). I’ll focus on
    the SIMD one here, the interested reader can navigate to
    /usr/src/lib/libc/amd64/string or point their browser to [[1m7[0m [0m
    [4mhttps://github.com/freebsd/freebsd-src/blob/main/lib/libc/amd64/string/strlen.S[0m]
    for the scalar implementation.

    [1m  | [0m[1mARCHENTRY(strlen, baseline)[0m
    [1m  | [0m[1m    mov %rdi, %rcx[0m
    [1m  | [0m[1m    pxor    %xmm1, %xmm1[0m
    [1m  | [0m[1m    and $~0xf, %rdi         # align string[0m
    [1m  | [0m[1m    pcmpeqb (%rdi), %xmm1   # compare head (with junk before string)[0m
    [1m  | [0m[1m    mov %rcx, %rsi          # string pointer copy for later[0m
    [1m  | [0m[1m    and $0xf, %ecx          # amount of bytes rdi is past 16 byte alignment[0m
    [1m  | [0m[1m    pmovmskb %xmm1, %eax[0m
    [1m  | [0m[1m    add $32, %rdi           # advance to next iteration[0m
    [1m  | [0m[1m    shr %cl, %eax           # clear out matches in junk bytes[0m
    [1m  | [0m[1m    test    %eax, %eax      # any match? (can't use ZF from SHR as CL=0 is possible)[0m
    [1m  | [0m[1m    jnz 2f[0m
    [1m  | [0m[1m[0m
    [1m  | [0m[1m    ALIGN_TEXT[0m
    [1m  | [0m[1m1:  pxor    %xmm1, %xmm1[0m
    [1m  | [0m[1m    pcmpeqb -16(%rdi), %xmm1    # find NUL bytes[0m
    [1m  | [0m[1m    pmovmskb %xmm1, %eax[0m
    [1m  | [0m[1m    test    %eax, %eax          # were any NUL bytes present?[0m
    [1m  | [0m[1m    jnz 3f[0m
    [1m  | [0m[1m[0m
    [1m  | [0m[1m    /* the same unrolled once more */[0m
    [1m  | [0m[1m    pxor    %xmm1, %xmm1[0m
    [1m  | [0m[1m    pcmpeqb (%rdi), %xmm1[0m
    [1m  | [0m[1m    pmovmskb %xmm1, %eax[0m
    [1m  | [0m[1m    add $32, %rdi           # advance to next iteration[0m
    [1m  | [0m[1m    test    %eax, %eax[0m
    [1m  | [0m[1m    jz  1b[0m
    [1m  | [0m[1m[0m
    [1m  | [0m[1m    /* match found in loop body */[0m
    [1m  | [0m[1m    sub $16, %rdi           # undo half the advancement[0m
    [1m  | [0m[1m3:  tzcnt   %eax, %eax      # find the first NUL byte[0m
    [1m  | [0m[1m    sub %rsi, %rdi          # string length until beginning of (%rdi)[0m
    [1m  | [0m[1m    lea -16(%rdi, %rax, 1), %rax    # that plus loc. of NUL byte: full string length[0m
    [1m  | [0m[1m    ret[0m
    [1m  | [0m[1m[0m
    [1m  | [0m[1m    /* match found in head */[0m
    [1m  | [0m[1m2:  tzcnt   %eax, %eax      # compute string length[0m
    [1m  | [0m[1m    ret[0m
    [1m  | [0m[1mARCHEND(strlen, baseline)[0m

    Most of these instructions aren’t anything odd, [1mMOV[0m,[1mXOR[0m,[1mAND[0m,[1mSHR[0m, but
    what stands out is [1mPCMPEQB[0m and [1mPMOVMSKV[0m.

    [1mPMOVMSKB[0m [[1m8[0m [4mhttps://www.felixcloutier.com/x86/pmovmskb[0m] is one of the
    most useful instructions for finding where the the index of our [1mNULL[0m
    character. The string functions in libc obviously function on C style
    string so they are [1mNULL[0m terminated. So what we do there is a compare
    followed by figuring out where the match was.

    But heres the kicker, there is no [1mPMOVMSKB[0m instruction for Aarch64
    which has caused a whole lot of headache. Im basing this on the
    amount of posts online regarding substitutions for [1mPMOVMSKB[0m on
    Aarch64 whereas assembly instructions are otherwise often rarely
    complained about. [[1m9[0m [0m
    [4mhttps://branchfree.org/2019/04/01/fitting-my-head-through-the-arm-holes-or-two-sequences-to-substitute-for-the-missing-pmovmskb-instruction-on-arm-neon/[0m][[1m10[0m[0m
    [4mhttps://community.arm.com/arm-community-blogs/b/infrastructure-solutions-blog/posts/porting-x86-vector-bitmask-optimizations-to-arm-neon[0m][[1m11[0m[0m
    [4mhttps://www.corsix.org/content/whirlwind-tour-aarch64-vector-instructions[0m]

[1m### [0m[1mSubstituting missing instructions[0m

    The most promising substitution to [1mPMOVMSKB[0m appears to be [1mSHRN[0m. With
    it we can take a 128-bit vector, shift by #imm and truncate to 8
    bits. So basically end up with a mask of either chunks of all 0’s or
    all 1’s, then by truncation we end up with single but halfbytes which
    correspond to whether we had a match or not. Hopefully thats comes
    out clearly, otherwise there is an excellent video courtesy of [[1m10[0m [0m
    [4mhttps://community.arm.com/arm-community-blogs/b/infrastructure-solutions-blog/posts/porting-x86-vector-bitmask-optimizations-to-arm-neon[0m]
    which shows it in action.

    [m<figure>[0m
    [m    <video src='./shrn_explained.mp4' controls autoplay />[0m
    [m</figure>[0m
    [m<figure>[0m
    [m    <figcaption><code>SHRN</code> explained,  courtesy of <a href="https://community.arm.com/arm-community-blogs/b/infrastructure-solutions-blog/posts/porting-x86-vector-bitmask-optimizations-to-arm-neon">[10]</a></figcaption>[0m
    [m</figure> [0m

[1m## [0m[1mstrlen PoC[0m

    Here is an evolution of my attempt of porting strlen to Aarch64. They
    also come in the form of a git repository [[1m12[0m [0m
    [4mhttps://git.sr.ht/~getz/aarch64_string.h/[0m] where I keep my
    experiments before theyre ready to be integrated into libc at my fork
    of freebsd-src that I keep on github. [[1m13[0m [0m
    [4mhttps://github.com/soppelmann/freebsd-src[0m]
    It’s also good to know while reading this code that registers [1mx9-x15[0m
    are “corruptible” meaning that a function can change them to whatever
    without having to restore them afterwards. Registers [1md0-d7[0m are “parameter
    and results registers”. So according to the [1mARM procedure call[0m
    [1mstandard[0m, [1mx0[0m etc. is where the input to a function is stored and you
    can do whatever you want with [1mx9-x15[0m without breaking anything.

[1m### [0m[1mSimple strlen[0m

    The most simple variant simply checks the first chunk without any
    loop. This simple example will only give valid results for short
    strings which are 16 byte aligned. That is that the data begins on a
    memory address that is a multiple of 16. You can create such a string
    to test it out like this:

    [1m  | [0m[1m#include <stdalign.h>[0m
    [1m  | [0m[1m#include <stdio.h>[0m
    [1m  | [0m[1m#include <string.h>[0m
    [1m  | [0m[1m[0m
    [1m  | [0m[1mextern size_t _strlen(const char * ptr);[0m
    [1m  | [0m[1m[0m
    [1m  | [0m[1mint[0m
    [1m  | [0m[1mmain() {[0m
    [1m  | [0m[1m    alignas(16) char string[] = "str";[0m
    [1m  | [0m[1m    printf("strlen: %zu\n", _strlen(string));[0m
    [1m  | [0m[1m}[0m

    Now for the simple strlen implementation. Note that [1m<machine/asm.h>[0m
    has nice little macros [1mENTRY()[0m and [1mEND()[0m which are used throughout.

    [1m  | [0m[1mENTRY(_strlen)[0m
    [1m  | [0m[1m    BIC x10,x0,#0xf       // alignment[0m
    [1m  | [0m[1m    LDR q0,[x10]          // load input to Vector register[0m
    [1m  | [0m[1m    CMEQ v0.16b,v0.16b,#0 // look for 0's[0m
    [1m  | [0m[1m    SHRN v0.8b,v0.8h,#4   // ^[0m
    [1m  | [0m[1m    FMOV x0,d0            // move to GPR[0m
    [1m  | [0m[1m    RBIT x0,x0            // reverse bits as NEON has no ctz[0m
    [1m  | [0m[1m    CLZ x0,x0             // count leading zeros[0m
    [1m  | [0m[1m    LSR x0,x0,#2          // get offset index[0m
    [1m  | [0m[1m    RET[0m
    [1m  | [0m[1mEND(_strlen)[0m

[1m### [0m[1mNaïve implementation[0m

    Now for a simple but naïve solution to creating a loop and handling
    strings which are not already 16 byte aligned. We simply calculate
    the offset to the nearest 16 byte boundary and traverse to the
    boundary with a Scalar implementation and then turn to the SIMD
    variant.

    [1m  | [0m[1mENTRY(_strlen)[0m
    [1m  | [0m[1m    BIC x10,x0,#0xf[0m
    [1m  | [0m[1m    AND x9,x0,#0xf[0m
    [1m  | [0m[1m    MOV x11,#0[0m
    [1m  | [0m[1m    CBZ x9,.Laligned_loop[0m
    [1m  | [0m[1m[0m
    [1m  | [0m[1m.Lunaligned_start:[0m
    [1m  | [0m[1m    LDR x5,[x10][0m
    [1m  | [0m[1m    ADD x10,x10,#1[0m
    [1m  | [0m[1m    CBZ x5,.Lfound_null[0m
    [1m  | [0m[1m    SUB x9,x9,#1[0m
    [1m  | [0m[1m    CBNZ x9,.Lunaligned_start[0m
    [1m  | [0m[1m    B .Laligned_loop[0m
    [1m  | [0m[1m[0m
    [1m  | [0m[1m.Lnext_it:[0m
    [1m  | [0m[1m    ADD x11,x11,#16[0m
    [1m  | [0m[1m    ADD x10,x10,#16[0m
    [1m  | [0m[1m[0m
    [1m  | [0m[1m.Laligned_loop:[0m
    [1m  | [0m[1m    LDR q0,[x10][0m
    [1m  | [0m[1m    CMEQ v0.16b,v0.16b,#0[0m
    [1m  | [0m[1m    SHRN v0.8b,v0.8h,#4[0m
    [1m  | [0m[1m    FMOV x0,d0[0m
    [1m  | [0m[1m    CBZ x0,.Lnext_it[0m
    [1m  | [0m[1m    RBIT x0,x0[0m
    [1m  | [0m[1m    CLZ x0,x0[0m
    [1m  | [0m[1m    LSR x0,x0,#2[0m
    [1m  | [0m[1m    ADD x0,x0,x11[0m
    [1m  | [0m[1m    RET[0m
    [1m  | [0m[1m[0m
    [1m  | [0m[1m.Lfound_null:[0m
    [1m  | [0m[1m    RET[0m
    [1m  | [0m[1mEND(_strlen)[0m

    This doesn’t make use of handy Aarch64 instruction paramaters such as
    [1mLDR qreg,[xreg,#imm]![0m which increments by the immediate value before
    the load.

[1m### [0m[1mImproved implementation[0m

    Now we use improve our previous implementation by using SIMD
    Instructions for the first chunk. We use a GPR load after the first
    check to improve performance for short strings as a GPR move is
    required for the results and statistically short strings are the most
    common in real world scenarios. This statement is backed by a survey
    conducted by the LLVM project. [9mI have not been able to find a direct[0m
    [9mlink to those results, but I will update this post when I find them.[0m
    See: [0m
    [4mhttps://code.ornl.gov/llvm-doe/llvm-project/-/tree/doe/libc/benchmarks/distributions[0m

    [1m  | [0m[1mENTRY(_strlen)[0m
    [1m  | [0m[1m    BIC x10,x0,#0xf[0m
    [1m  | [0m[1m    LDR q0,[x10][0m
    [1m  | [0m[1m    CMEQ    v0.16b,v0.16b,#0[0m
    [1m  | [0m[1m    SHRN    v0.8b,v0.8h,#4[0m
    [1m  | [0m[1m    FMOV    x1,d0       // move to GPR[0m
    [1m  | [0m[1m    LSL x2,x0,#2        // get the byte offset of the last processed[0m
    [1m  | [0m[1m    LSR x1,x1,x2        // align with offset[0m
    [1m  | [0m[1m    CBZ x1,.Lloop       // jump if no hit[0m
    [1m  | [0m[1m    RBIT    x1,x1[0m
    [1m  | [0m[1m    CLZ x0,x1[0m
    [1m  | [0m[1m    LSR x0,x0,#2[0m
    [1m  | [0m[1m    RET[0m
    [1m  | [0m[1m[0m
    [1m  | [0m[1m.Lloop:[0m
    [1m  | [0m[1m    LDR q0,[x10,#16]!    // increment by 16 and load[0m
    [1m  | [0m[1m    CMEQ v0.16b,v0.16b,#0[0m
    [1m  | [0m[1m    SHRN v0.8b,v0.8h,#4[0m
    [1m  | [0m[1m    fmov x1,d0           // get offset in case of hit[0m
    [1m  | [0m[1m    cbz x1,.Lloop        // x1 is zero if no hit in segment[0m
    [1m  | [0m[1m    .Ldone:[0m
    [1m  | [0m[1m    SUB x0,x10,x0[0m
    [1m  | [0m[1m    RBIT x1,x1[0m
    [1m  | [0m[1m    CLZ x3,x1[0m
    [1m  | [0m[1m    LSR x3,x3,#2[0m
    [1m  | [0m[1m    ADD x0,x0,x3[0m
    [1m  | [0m[1m    RET[0m
    [1m  | [0m[1mEND(_strlen)[0m

[1m### [0m[1mFCMP to avoid GPR move[0m

    After improving on the naïve implementation I realized that we can
    avoid a move from a SIMD register to a GPR by using FCMP in the loop.
    I also realized that we can avoid a few instructions if the input is
    already 16 byte aligned (see [1m.Laligned[0m), although it does introduce a
    new branch to be resolved.
    The later benchmarks will indicate whether or not this is a
    worthwhile improvement.

    [1m  | [0m[1mENTRY(_strlen)[0m
    [1m  | [0m[1m    BIC x10,x0,#0xf[0m
    [1m  | [0m[1m    AND x9,x0,#0xf[0m
    [1m  | [0m[1m    LDR q0,[x10][0m
    [1m  | [0m[1m    CMEQ    v0.16b,v0.16b,#0[0m
    [1m  | [0m[1m    SHRN    v0.8b,v0.8h,#4[0m
    [1m  | [0m[1m    CBZ x9,.Laligned[0m
    [1m  | [0m[1m    FMOV    x1,d0[0m
    [1m  | [0m[1m    LSL x2,x0,#2[0m
    [1m  | [0m[1m    LSR x1,x1,x2[0m
    [1m  | [0m[1m    CBZ x1,.Lloop[0m
    [1m  | [0m[1m    RBIT    x1,x1[0m
    [1m  | [0m[1m    CLZ x0,x1[0m
    [1m  | [0m[1m    LSR x0,x0,#2[0m
    [1m  | [0m[1m    RET[0m
    [1m  | [0m[1m[0m
    [1m  | [0m[1m.Laligned:[0m
    [1m  | [0m[1m    FMOV    x1,d0[0m
    [1m  | [0m[1m    CBNZ    x1,.Ldone[0m
    [1m  | [0m[1m[0m
    [1m  | [0m[1m.Lloop:[0m
    [1m  | [0m[1m    LDR q0,[x10,#16]![0m
    [1m  | [0m[1m    CMEQ v0.16b,v0.16b,#0[0m
    [1m  | [0m[1m    SHRN v0.8b,v0.8h,#4[0m
    [1m  | [0m[1m    fcmp d0,#0.0[0m
    [1m  | [0m[1m    B.EQ .Lloop[0m
    [1m  | [0m[1m    FMOV x1,d0[0m
    [1m  | [0m[1m.Ldone:[0m
    [1m  | [0m[1m    SUB x0,x10,x0[0m
    [1m  | [0m[1m    RBIT x1,x1[0m
    [1m  | [0m[1m    CLZ x3,x1[0m
    [1m  | [0m[1m    LSR x3,x3,#2[0m
    [1m  | [0m[1m    ADD x0,x0,x3[0m
    [1m  | [0m[1m    RET[0m
    [1m  | [0m[1mEND(_strlen)[0m

    Now we can look at what further improvements can be done. We could
    avoid loop carried dependencies, for each iteration a post increment
    is currently used to go to next iteration, we could unroll the loop
    twice and increment x10 every two iterations to make it easier for
    the CPU to run two iterations at once. Aarch64 also has instructions
    for loading several SIMD registers at once using the [1mLD1[0m,[1mLD2[0m… family
    of instructions. [[1m16[0m [0m
    [4mhttps://www.scs.stanford.edu/~zyedidia/arm64/ld2_advsimd_mult.html[0m]

[1m## [0m[1mlibc integration[0m

    Getting a string function written in assembly integrated into libc on
    FreeBSD isn’t as big of an ordeal as it may sound like. We simply use
    the [1mENTRY()[0m and [1mEND()[0m macros in the code and add the filenames to the
    associated [1mMakefile.inc[0m located at [0m
    [1mlib/libc/aarch64/string/Makefile.inc[0m like the following:

    [1m  | [0m[1m@@ -15,11 +15,12 @@ AARCH64_STRING_FUNCS= \[0m
    [1m  | [0m[1m        strchrnul \[0m
    [1m  | [0m[1m        strcmp \[0m
    [1m  | [0m[1m        strcpy \[0m
    [1m  | [0m[1m-       memcmp \[0m
    [1m  | [0m[1m        strncmp \[0m
    [1m  | [0m[1m        strnlen \[0m
    [1m  | [0m[1m        strrchr[0m
    [1m  | [0m[1m[0m
    [1m  | [0m[1m+MDSRCS+= \[0m
    [1m  | [0m[1m+       memcmp.S[0m
    [1m  | [0m[1m #[0m
    [1m  | [0m[1m # Add the above functions. Generate an asm file that includes the needed[0m
    [1m  | [0m[1m # Arm Optimized Routines file defining the function name to the libc name.[0m

    We can then build libc as a shared library and load it using [0m
    [1mLD_PRELOAD[0m for running regression tests as running FreeBSD with a
    broken libc makes FreeBSD very sad and prone to severe errors. It’s
    always nice to avoid a broken install while debugging.

    Building libc is as simple as

    [1m  | [0m[1mcd /usr/src/lib/libnetbsd && make[0m
    [1m  | [0m[1mcd /usr/src/lib/libc && make[0m
    [1m  | [0m[1m[0m
    [1m  | [0m[1m# OR[0m
    [1m  | [0m[1m[0m
    [1m  | [0m[1mmake -C /usr/src/lib/libc MAKEOBJDIRPREFIX=/tmp/objdir WITHOUT_TESTS=yes[0m

    As for debugging it’s as simple as loading up a test binary with [0m
    [1mlldb[0m and setting a breakpoint for the string function being
    developed, [1mstrlen[0m in our case.

[1m## [0m[1mTests[0m

    FreeBSD comes bundled with an excellent Test Suite [[1m14[0m [0m
    [4mhttps://wiki.freebsd.org/TestSuite[0m], they are written using a test
    framework [1mKyua[0m with the [1mATF[0m library. The FreeBSD wiki page has all
    the information necessary for this and there’s no need for my to
    repeat it here. :-) 

    Running the tests is as simple as going to [0m
    [1m/usr/src/lib/libc/tests/string[0m and running [1mmake check[0m. If you haven’t
    already run [1mbuildworld[0m then you will need to build [1mlib/libnetbsd[0m
    first. This as FreeBSD also borrows some tests from upstream NetBSD
    located at [1m/usr/src/contrib/netbsd-tests/lib/libc/string[0m.

[1m## [0m[1mBenchmarks[0m

    Benchmarks are executed using fuz’ strperf program [[1m17[0m [0m
    [4mhttps://github.com/clausecker/strperf[0m], it’s output is compatible
    with benchstat from /devel/go-perf. I benchmark all the
    implementations against the implementations in libc, I also tried
    running the code on a Raspberry Pi 5 running debian to see strlen
    holds up against glibc’s implementation.

    I have benchmarked the previously described implementations. Seeing
    performance impact of substituting a GPR move followed by a [1mbnz[0m
    compared to a [1mFCMP[0m followed by a [1mb.eq[0m and the impact of branching
    immediately in the case of an already aligned string.

    It’s also important to note that these implementations are hardware
    dependent as different cores may utilize more or fewer pipelines for
    specific instructions.

    To test against glibc I borrowed a Raspbery Pi5 running debian and
    installed [1mbsd-make[0m to compile strperf, [1mapt install bmake[0m.

    Then to generate benchmark results it’s as simple as:
    [1mfor i in {1..20}; do ./strlen >> results/${TEST}; done[0m

    This produced the following results when evaluated with [1mbenchstat[0m.
    You might need to scroll horizontally to view all the results.

    [1m  | [0m[1mos: FreeBSD[0m
    [1m  | [0m[1march: arm64[0m
    [1m  | [0m[1mcpu: ARM Cortex-A76 r4p1[0m
    [1m  | [0m[1m        │ libc_Scalar  │              libc_ARM               │                 GPR                 │             GPR_aligned             │                FCMP                 │            FCMP_aligned             │[0m
    [1m  | [0m[1m        │    sec/op    │   sec/op     vs base                │   sec/op     vs base                │   sec/op     vs base                │   sec/op     vs base                │   sec/op     vs base                │[0m
    [1m  | [0m[1mShort      186.9µ ± 1%   134.6µ ± 0%  -28.01% (p=0.000 n=20)   121.0µ ± 0%  -35.26% (p=0.000 n=20)   118.5µ ± 0%  -36.62% (p=0.000 n=20)   121.8µ ± 0%  -34.85% (p=0.000 n=20)   119.8µ ± 0%  -35.91% (p=0.000 n=20)[0m
    [1m  | [0m[1mMid        45.05µ ± 1%   37.07µ ± 0%  -17.73% (p=0.000 n=20)   33.36µ ± 0%  -25.96% (p=0.000 n=20)   30.43µ ± 1%  -32.45% (p=0.000 n=20)   33.37µ ± 0%  -25.93% (p=0.000 n=20)   29.98µ ± 1%  -33.45% (p=0.000 n=20)[0m
    [1m  | [0m[1mLong      13.894µ ± 0%   4.442µ ± 0%  -68.03% (p=0.000 n=20)   6.978µ ± 0%  -49.78% (p=0.000 n=20)   6.977µ ± 0%  -49.79% (p=0.000 n=20)   6.852µ ± 0%  -50.68% (p=0.000 n=20)   5.627µ ± 0%  -59.50% (p=0.000 n=20)[0m
    [1m  | [0m[1mgeomean    48.91µ        28.09µ       -42.58%                  30.43µ       -37.79%                  29.30µ       -40.09%                  30.31µ       -38.03%                  27.24µ       -44.31%[0m
    [1m  | [0m[1m[0m
    [1m  | [0m[1m        │ libc_Scalar  │                libc_ARM                │                  GPR                  │              GPR_aligned              │                  FCMP                  │              FCMP_aligned              │[0m
    [1m  | [0m[1m        │     B/s      │      B/s       vs base                 │      B/s       vs base                │      B/s       vs base                │      B/s       vs base                 │      B/s       vs base                 │[0m
    [1m  | [0m[1mShort     637.7Mi ± 1%    885.9Mi ± 0%   +38.91% (p=0.000 n=20)    985.1Mi ± 0%  +54.47% (p=0.000 n=20)   1006.1Mi ± 0%  +57.77% (p=0.000 n=20)    978.9Mi ± 0%   +53.49% (p=0.000 n=20)    995.0Mi ± 0%   +56.02% (p=0.000 n=20)[0m
    [1m  | [0m[1mMid       2.584Gi ± 1%    3.141Gi ± 0%   +21.55% (p=0.000 n=20)    3.490Gi ± 0%  +35.07% (p=0.000 n=20)    3.825Gi ± 1%  +48.03% (p=0.000 n=20)    3.488Gi ± 0%   +35.01% (p=0.000 n=20)    3.883Gi ± 1%   +50.26% (p=0.000 n=20)[0m
    [1m  | [0m[1mLong      8.379Gi ± 0%   26.210Gi ± 0%  +212.81% (p=0.000 n=20)   16.684Gi ± 0%  +99.12% (p=0.000 n=20)   16.686Gi ± 0%  +99.15% (p=0.000 n=20)   16.990Gi ± 0%  +102.77% (p=0.000 n=20)   20.690Gi ± 0%  +146.93% (p=0.000 n=20)[0m
    [1m  | [0m[1mgeomean   2.380Gi         4.145Gi        +74.15%                   3.826Gi       +60.76%                   3.973Gi       +66.92%                   3.841Gi        +61.37%                   4.274Gi        +79.56%[0m
    [1m  | [0m[1m[0m
    [1m  | [0m[1mos: Linux[0m
    [1m  | [0m[1march: aarch64[0m
    [1m  | [0m[1m        │ strlen_glibc │[0m
    [1m  | [0m[1m        │    sec/op    │[0m
    [1m  | [0m[1mShort      132.1µ ± 0%[0m
    [1m  | [0m[1mMid        36.29µ ± 1%[0m
    [1m  | [0m[1mLong       4.365µ ± 4%[0m
    [1m  | [0m[1mgeomean    27.55µ[0m
    [1m  | [0m[1m[0m
    [1m  | [0m[1m        │ strlen_glibc │[0m
    [1m  | [0m[1m        │     B/s      │[0m
    [1m  | [0m[1mShort     902.7Mi ± 0%[0m
    [1m  | [0m[1mMid       3.208Gi ± 1%[0m
    [1m  | [0m[1mLong      26.67Gi ± 4%[0m
    [1m  | [0m[1mgeomean   4.225Gi[0m

[1m## [0m[1mWhat[0m[1m’[0m[1ms next[0m

    Despite resulting in worse perfomance for longer strings I will now
    continue the porting effort and translate [1mmemcmp[0m to Aarch64 NEON. I
    will continue optimizing [1mstrlen[0m by unrolling the main loop twice as
    previously mentioned. So instead of the current [1mLDR q0,[x10,#16]![0m I’ll
    do a [1mLDP q1, q2, [x10, #32]![0m

    Hopefully I can get that strlen done by the end of the week and
    submit it for review to the FreeBSD Phabricator instance.

    I also attended the LundLinuxCon [[1m18[0m [4mhttps://lundlinuxcon.org[0m] last
    week and saw a really interesting talk regarding safer flexible
    arrays in the Linux kernel [[1m19[0m [0m
    [4mhttps://embeddedor.com/slides/2024/llc/llc2024.pdf[0m]. It involved a
    new warning flag and some niceties that were recently merged into
    GCC15 [[1m20[0m [4mhttps://gcc.gnu.org/bugzilla/show_bug.cgi?id=108896[0m] and
    LLVM18 [[1m21[0m [4mhttps://github.com/llvm/llvm-project/pull/76348[0m], I will
    try building FreeBSD with those flags and see how many warning are
    present, but first I need to read up a littlebit whether or not
    FreeBSD even permits the use of flexible arrays in the kernel. :-) 

[1m## [0m[1mReferences[0m

    [[1m1[0m [4mhttps://summerofcode.withgoogle.com/[0m] [0m
    [4mhttps://summerofcode.withgoogle.com/[0m
    [[1m2[0m [4mhttps://dflund.se/~getz/GSOC/FreeBSDproposal.txt[0m] [0m
    [4mhttps://dflund.se/~getz/GSOC/FreeBSDproposal.txt[0m
    [[1m3[0m [0m
    [4mhttps://wiki.freebsd.org/SummerOfCodeIdeas#Port_of_libc_SIMD_enhancements_to_other_architectures[0m]
    [4mhttps://wiki.freebsd.org/SummerOfCodeIdeas#Port_of_libc_SIMD_enhancements_to_other_architectures[0m
    [[1m4[0m [4mhttps://strajabot.com/[0m] [4mhttps://strajabot.com/[0m
    [[1m5[0m [4mhttps://graphics.stanford.edu/~seander/bithacks.html[0m] [0m
    [4mhttps://graphics.stanford.edu/~seander/bithacks.html[0m
    [[1m6[0m [0m
    [4mhttps://man.freebsd.org/cgi/man.cgi?query=simd&manpath=FreeBSD+15.0-CURRENT[0m]
    [4mhttps://man.freebsd.org/cgi/man.cgi?query=simd&manpath=FreeBSD+15.0-CURRENT[0m
    [[1m7[0m [0m
    [4mhttps://github.com/freebsd/freebsd-src/blob/main/lib/libc/amd64/string/strlen.S[0m]
    [4mhttps://github.com/freebsd/freebsd-src/blob/main/lib/libc/amd64/string/strlen.S[0m
    [[1m8[0m [4mhttps://www.felixcloutier.com/x86/pmovmskb[0m] [0m
    [4mhttps://www.felixcloutier.com/x86/pmovmskb[0m
    [[1m9[0m [0m
    [4mhttps://branchfree.org/2019/04/01/fitting-my-head-through-the-arm-holes-or-two-sequences-to-substitute-for-the-missing-pmovmskb-instruction-on-arm-neon/[0m]
    [4mhttps://branchfree.org/2019/04/01/fitting-my-head-through-the-arm-holes-or-two-sequences-to-substitute-for-the-missing-pmovmskb-instruction-on-arm-neon/[0m
    [[1m10[0m [0m
    [4mhttps://community.arm.com/arm-community-blogs/b/infrastructure-solutions-blog/posts/porting-x86-vector-bitmask-optimizations-to-arm-neon[0m]
    [4mhttps://community.arm.com/arm-community-blogs/b/infrastructure-solutions-blog/posts/porting-x86-vector-bitmask-optimizations-to-arm-neon[0m
    [[1m11[0m [0m
    [4mhttps://www.corsix.org/content/whirlwind-tour-aarch64-vector-instructions[0m]
    [4mhttps://www.corsix.org/content/whirlwind-tour-aarch64-vector-instructions[0m
    [[1m12[0m [4mhttps://git.sr.ht/~getz/aarch64_string.h/[0m] [0m
    [4mhttps://git.sr.ht/~getz/aarch64_string.h/[0m
    [[1m13[0m [4mhttps://github.com/soppelmann/freebsd-src[0m] [0m
    [4mhttps://github.com/soppelmann/freebsd-src/[0m
    [[1m14[0m [4mhttps://wiki.freebsd.org/TestSuite[0m] [0m
    [4mhttps://wiki.freebsd.org/TestSuite/[0m
    [[1m15[0m [0m
    [4mhttps://developer.arm.com/documentation/PJDOC-466751330-593177/latest/[0m]
    [4mhttps://developer.arm.com/documentation/PJDOC-466751330-593177/latest/[0m
    [[1m16[0m [0m
    [4mhttps://www.scs.stanford.edu/~zyedidia/arm64/ld2_advsimd_mult.html[0m] [0m
    [4mhttps://www.scs.stanford.edu/~zyedidia/arm64/ld2_advsimd_mult.html[0m
    [[1m17[0m [4mhttps://github.com/clausecker/strperf[0m] [0m
    [4mhttps://github.com/clausecker/strperf[0m
    [[1m18[0m [4mhttps://lundlinuxcon.org[0m] [4mhttps://lundlinuxcon.org[0m
    [[1m19[0m [4mhttps://embeddedor.com/slides/2024/llc/llc2024.pdf[0m] [0m
    [4mhttps://embeddedor.com/slides/2024/llc/llc2024.pdf[0m
    [[1m20[0m [4mhttps://gcc.gnu.org/bugzilla/show_bug.cgi?id=108896[0m] [0m
    [4mhttps://gcc.gnu.org/bugzilla/show_bug.cgi?id=108896[0m
    [[1m21[0m [4mhttps://github.com/llvm/llvm-project/pull/76348[0m] [0m
    [4mhttps://github.com/llvm/llvm-project/pull/76348[0m 

[1m[0m[1mBonus: Hello World in Aarch64 Assembly[0m

    The FreeBSD Developers’ Handbook has a section on writing assembly,
    but it’s targeted towards x86 and is rather outdated. Using it’s
    suggestions Hello World would look like this.

    [1m  | [0m[1m    .text[0m
    [1m  | [0m[1m    .global _start[0m
    [1m  | [0m[1m[0m
    [1m  | [0m[1mkernel:[0m
    [1m  | [0m[1m    int $0x80[0m
    [1m  | [0m[1m    ret[0m
    [1m  | [0m[1m[0m
    [1m  | [0m[1m_start:[0m
    [1m  | [0m[1m    mov     $4, %rax[0m
    [1m  | [0m[1m    mov     $1, %rdi[0m
    [1m  | [0m[1m    mov     $message, %rsi[0m
    [1m  | [0m[1m    mov     $13, %rdx[0m
    [1m  | [0m[1m    call    kernel[0m
    [1m  | [0m[1m    mov     $1, %rax[0m
    [1m  | [0m[1m    mov     $69, %rdi[0m
    [1m  | [0m[1m    syscall[0m
    [1m  | [0m[1m[0m
    [1m  | [0m[1mmessage:[0m
    [1m  | [0m[1m    .ascii "Hello, world\n"[0m

    But [1mINT 80h[0m is much slower than [1msyscall[0m or simply using [1msvc #0[0m on
    arm64, this is because a lot of microcode is run when [1mINT[0m is
    executed, whereas the microcode for [1msyscall[0m is much simpler. Although
    [1mINT 80h[0m still works on amd64!

    Whereas [1m0x80[0m is the i386 syscall interface it incidentally works for
    amd64 tasks because we don’t check if the process doing a syscall is
    a 32 bit or 64 bit one. But arguments are truncated to 32 bits, so
    you can’t e.g. pass pointers to the stack! And arm64 ofcourse doesn’t
    have the [1mINT[0m instruction.

    In general, the method of doing syscalls is different on each
    architecture. FreeBSD is moving towards what win32 and solaris
    already pioneered: syscalls should be done by calling library
    functions so the kernel ABI and API can be adapted in the future. For
    this reason syscalls will be split into a new library [1mlibsys[0m in
    FreeBSD 15.

    If you’re wondering how to figure out what number each syscall
    corresponds to then you can check [0m
    [4mhttps://cgit.freebsd.org/src/tree/sys/kern/syscalls.master[0m

    [1m  | [0m[1m/*[0m
    [1m  | [0m[1mCompile with the following for non Aarch64 host:[0m
    [1m  | [0m[1maarch64-unknown-freebsd14.0-gcc13 --sysroot /usr/local/freebsd-sysroot/aarch64 hello.S -nostdlib[0m
    [1m  | [0m[1m[0m
    [1m  | [0m[1mRun with:[0m
    [1m  | [0m[1mqemu-aarch64-static ./a.out[0m
    [1m  | [0m[1m*/[0m
    [1m  | [0m[1m[0m
    [1m  | [0m[1m.text[0m
    [1m  | [0m[1m[0m
    [1m  | [0m[1m/* Our application's entry point. */[0m
    [1m  | [0m[1m.global _start[0m
    [1m  | [0m[1m[0m
    [1m  | [0m[1m_start:[0m
    [1m  | [0m[1m    /* syscall write(int fd, const void *buf, size_t count) */[0m
    [1m  | [0m[1m    mov     x0, #1      /* fd := STDOUT_FILENO */[0m
    [1m  | [0m[1m    ldr     x1, = msg    /* buf := msg */[0m
    [1m  | [0m[1m    ldr     x2, = len    /* count := len */[0m
    [1m  | [0m[1m    mov     w8, #4     /* write is syscall #4 */[0m
    [1m  | [0m[1m    svc     #0          /* invoke syscall */[0m
    [1m  | [0m[1m[0m
    [1m  | [0m[1m    /* syscall exit(int status) */[0m
    [1m  | [0m[1m    mov     x0, #69      /* status := 69 */[0m
    [1m  | [0m[1m    mov     w8, #1     /* exit is syscall #1 */[0m
    [1m  | [0m[1m    svc     #0          /* invoke syscall */[0m
    [1m  | [0m[1m[0m
    [1m  | [0m[1m/* Data segment: define our message string and calculate it's length. */[0m
    [1m  | [0m[1m.data[0m
    [1m  | [0m[1mmsg:[0m
    [1m  | [0m[1m    .ascii "Hello, world\n"[0m
    [1m  | [0m[1mlen = . - msg[0m
