<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Validark&apos;s Blog</title><description>dev things</description><link>https://validark.dev/</link><language>en</language><item><title>Deus Lex Machina</title><link>https://validark.dev/posts/deus-lex-machina/</link><guid isPermaLink="true">https://validark.dev/posts/deus-lex-machina/</guid><description>A new compaction-based tokenizer for the Zig programming language</description><pubDate>Wed, 16 Apr 2025 06:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&amp;lt;!-- Scanic Sanic --&amp;gt;
&amp;lt;!-- I Think, Therefore I Scan --&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- The Vector Compact --&amp;gt;
&amp;lt;!-- Strength of Character --&amp;gt;
&amp;lt;!-- Lex Machina --&amp;gt;
&amp;lt;!-- A token of appreciation --&amp;gt;
&amp;lt;!-- Breaking character --&amp;gt;
&amp;lt;!-- In the fast lane --&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- Token gesture --&amp;gt;
&amp;lt;!-- Scan and deliver --&amp;gt;
&amp;lt;!-- Vector Lexicon --&amp;gt;
&amp;lt;!-- Compress to Impress --&amp;gt;
&amp;lt;!-- Packed and ready --&amp;gt;
&amp;lt;!-- All Your Tokens Are Belong to Us --&amp;gt;
&amp;lt;!-- A parsing glance --&amp;gt;&lt;/p&gt;
&lt;p&gt;Today, I am excited to announce the alpha release of a brand new compacting Zig tokenizer! You may find it in the following repository:&lt;/p&gt;
&lt;p&gt;::github{repo=&quot;Validark/Accelerated-Zig-Parser&quot; license=&quot;MIT&quot;}&lt;/p&gt;
&lt;p&gt;If you want to help motivate me to keep working on this, give it a star and come back!&lt;/p&gt;
&lt;p&gt;Please note it is not ready for prime-time just yet, since there are still more optimizations to be had, as well as support for more architectures. &lt;strong&gt;At the moment, only AMD64 machines with AVX-512 instructions are supported.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;That being said, the new implementation can tokenize up to &lt;strong&gt;2.75x faster&lt;/strong&gt; than the mainline implementation, currently at &lt;strong&gt;1.4GB/s&lt;/strong&gt; on a single core on my laptop, with a lot of improvements coming soon!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;src/assets/images/Zero_2.svg&quot; alt=&quot;Zero the Ziguana&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;All Your Tokens Are Belong to Us&lt;/h2&gt;
&lt;p&gt;The above repository benchmarks 3 Zig tokenizers:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The tokenizer in the Zig Standard Library used by the 0.14 compiler.&lt;/li&gt;
&lt;li&gt;&lt;s&gt;A heat-seeking tokenizer (&lt;a href=&quot;https://www.youtube.com/watch?v=oN8LDpWuPWw&amp;amp;t=530s&quot;&gt;talk 1&lt;/a&gt;, &lt;a href=&quot;https://www.youtube.com/live/FDiUKafPs0U&amp;amp;t=210s&quot;&gt;talk 2&lt;/a&gt;)&lt;/s&gt; (had to temporarily remove it, but will add back by July)&lt;/li&gt;
&lt;li&gt;A &lt;a href=&quot;https://github.com/Validark/Accelerated-Zig-Parser/blob/7fa80343eb177c0220276492d467cdf6d3dabb73/src/main.zig#L1912&quot;&gt;compacting tokenizer&lt;/a&gt; (&lt;a href=&quot;https://www.youtube.com/watch?v=NM1FNB5nagk&quot;&gt;talk 3&lt;/a&gt;)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Benchmark results on my laptop with a Ryzen AI 9 HX 370:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;       Read in files in 26.479ms (1775.63 MB/s) and used 47.018545MB memory with 3504899722 lines across 3253 files
Legacy Tokenizing took              91.419ms (0.51 GB/s, 38.34B loc/s) and used 40.07934MB memory
Tokenizing with compression took    33.301ms (1.41 GB/s, 105.25B loc/s) and used 16.209284MB memory
       That&apos;s 2.75x faster and 2.47x less memory than the mainline implementation!
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Headline features&lt;/h2&gt;
&lt;p&gt;The new &lt;a href=&quot;https://github.com/Validark/Accelerated-Zig-Parser/blob/7fa80343eb177c0220276492d467cdf6d3dabb73/src/main.zig#L1912&quot;&gt;compacting tokenizer&lt;/a&gt; processes an entire 64-byte chunk of source code at once (soon to be 512!). It includes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A fully SIMDized &lt;a href=&quot;https://github.com/Validark/Accelerated-Zig-Parser/blob/7fa80343eb177c0220276492d467cdf6d3dabb73/src/main.zig#L3160&quot;&gt;UTF-8 validator&lt;/a&gt; ported from &lt;a href=&quot;https://github.com/simdjson/simdjson/&quot;&gt;simdjson&lt;/a&gt;/&lt;a href=&quot;https://github.com/simdutf/simdutf/&quot;&gt;simdutf&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;A fully branchless bit-manipulation routine to &lt;a href=&quot;https://github.com/Validark/Accelerated-Zig-Parser/blob/7fa80343eb177c0220276492d467cdf6d3dabb73/src/main.zig#L1012&quot;&gt;determine which characters are escaped by backslashes&lt;/a&gt;. (Thanks to John Keiser (@jkeiser) for &lt;a href=&quot;https://github.com/simdjson/simdjson/pull/2042&quot;&gt;designing this algorithm for simdjson&lt;/a&gt;.)&lt;/li&gt;
&lt;li&gt;A loop which &lt;a href=&quot;https://github.com/Validark/Accelerated-Zig-Parser/blob/7fa80343eb177c0220276492d467cdf6d3dabb73/src/main.zig#L2007&quot;&gt;parses all lines in a given chunk in parallel for strings/comments/character_literals/line_strings&lt;/a&gt;. Characters inside of these constructs must be exempted from comprising their own tokens.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/Validark/Accelerated-Zig-Parser/blob/7fa80343eb177c0220276492d467cdf6d3dabb73/src/main.zig#L1302&quot;&gt;A multi-purpose vectorized table-lookup&lt;/a&gt;. It performs Vectorized-Classification (making it so we can produce a bitstring of any group of characters we care about in one additional instruction), as well as mapping all characters that may appear in a multi-char symbol into a range of [0, 15] while everything else is mapped into the range [128, 255]. Cognoscenti would recognize this as matching the semantics of a &lt;code&gt;vpshufb&lt;/code&gt; instruction. It also is itself a mostly-correct vector of &lt;code&gt;kinds&lt;/code&gt; fields (one of the pieces of information we need to output for each token).&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/Validark/Accelerated-Zig-Parser/blob/7fa80343eb177c0220276492d467cdf6d3dabb73/src/main.zig#L698&quot;&gt;A mini non-deterministic finite state machine&lt;/a&gt; implemented using &lt;code&gt;vpshufb&lt;/code&gt; instructions on the result of the vectorized table-lookup for multi-char-symbol matching, as well as an effectively-branchless (for real code) &lt;a href=&quot;https://github.com/Validark/Accelerated-Zig-Parser/blob/7fa80343eb177c0220276492d467cdf6d3dabb73/src/main.zig#L810&quot;&gt;reconciliation loop&lt;/a&gt; which accepts bitstrings indicating where valid 2 and 3 character multi-char symbols end and deleting the ones that are impossible due to overlap.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/Validark/Accelerated-Zig-Parser/blob/7fa80343eb177c0220276492d467cdf6d3dabb73/src/main.zig#L1518&quot;&gt;SIMD hasher of up to 16 multi-char symbols at once&lt;/a&gt;, which works across chunk boundaries to produce the proper &lt;code&gt;kind&lt;/code&gt; of multi-char symbols.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/Validark/Accelerated-Zig-Parser/blob/7fa80343eb177c0220276492d467cdf6d3dabb73/src/main.zig#L1370&quot;&gt;A fully vectorized cross-chunk keyword hasher and validator&lt;/a&gt; that pulls from a &lt;a href=&quot;https://github.com/Validark/Accelerated-Zig-Parser/blob/7fa80343eb177c0220276492d467cdf6d3dabb73/src/main.zig#L490&quot;&gt;common superstring&lt;/a&gt; holding all keywords in a total of 4 64-byte vectors. &lt;code&gt;vpgather&lt;/code&gt; instructions are not used.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/Validark/Accelerated-Zig-Parser/blob/7fa80343eb177c0220276492d467cdf6d3dabb73/src/main.zig#L2196&quot;&gt;Token-matching logic implemented almost entirely using bit-manipulation and SIMD operations&lt;/a&gt;, the things CPU&apos;s are the fastest at.&lt;/li&gt;
&lt;li&gt;Logic void of almost all branches-- they are only used where they provide a performance benefit.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Simplified Explanation&lt;/h2&gt;
&lt;p&gt;ELI5 How do we accomplish such speed? By processing entire chunks of 64-bytes (soon 512-bytes!) at once. Here&apos;s a basic implementation:&lt;/p&gt;
&lt;p&gt;First, we produce a few bitstrings where each bit tells us a piece of information about a corresponding byte in the 64-byte &lt;code&gt;chunk&lt;/code&gt; we process at once:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const V = @Vector(64, u8);
const chunk: V = ptr[0..64].*;

const alphas_lower: u64 =
    @as(u64, @bitCast(chunk &amp;gt;= @as(V, @splat(&apos;a&apos;)))) &amp;amp;
    @as(u64, @bitCast(chunk &amp;lt;= @as(V, @splat(&apos;z&apos;))));

const alphas_upper: u64 =
    @as(u64, @bitCast(chunk &amp;gt;= @as(V, @splat(&apos;A&apos;)))) &amp;amp;
    @as(u64, @bitCast(chunk &amp;lt;= @as(V, @splat(&apos;Z&apos;))));

const underscores: u64 =
    @bitCast(chunk == @as(V, @splat(&apos;_&apos;)));

const numerics: u64 =
    @as(u64, @bitCast(chunk &amp;gt;= @as(V, @splat(&apos;0&apos;)))) &amp;amp;
    @as(u64, @bitCast(chunk &amp;lt;= @as(V, @splat(&apos;9&apos;))));

const alpha_underscores = alphas_lower | alphas_upper | underscores;
const alpha_numeric_underscores = alpha_underscores | numerics;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, we do a series of bitmanipulations to figure out the start and end positions of all tokens within our &lt;code&gt;chunk&lt;/code&gt;. To find the starts and ends of identifiers, we do something similar to the following:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const identifier_starts = alpha_underscores &amp;amp; ~(alpha_numeric_underscores &amp;lt;&amp;lt; 1);
const identifier_ends = alpha_numeric_underscores &amp;amp; ~(alpha_numeric_underscores &amp;gt;&amp;gt; 1);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let&apos;s walk through how &lt;code&gt;identifier_starts&lt;/code&gt; is computed very slowly:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;alpha_numeric_underscores &amp;lt;&amp;lt; 1&lt;/code&gt; Shifting left by &lt;code&gt;1&lt;/code&gt; semantically moves our bits in the rightwards direction in our source file. This is due to the fact that this is written from a little-endian perspective, and every bit in &lt;code&gt;alpha_numeric_underscores&lt;/code&gt; corresponds to a byte of &lt;code&gt;chunk&lt;/code&gt;, therefore the bit-order inherits the byte order. This might seem like an indictment of little-endian machines, but actually little-endian is better for this kind of processing because when we do a subtraction we want the carry-bits to propagate in the direction of the last byte (rather than the reverse on a big-endian machine). If you don&apos;t understand that right now, that&apos;s okay, just take my word for it. The point is, &lt;code&gt;alpha_numeric_underscores &amp;lt;&amp;lt; 1&lt;/code&gt; produces a bitstring that indicates which positions in &lt;code&gt;chunk&lt;/code&gt; had a alphanumeric/underscore &lt;strong&gt;before&lt;/strong&gt; it. We could express this as a regular expression like &lt;code&gt;/(?&amp;lt;=\w)./g&lt;/code&gt; (&lt;a href=&quot;https://regexr.com/8e4r5&quot;&gt;regexr link&lt;/a&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;We take the inverse of the previous bitstring. Overall we have &lt;code&gt;~(alpha_numeric_underscores &amp;lt;&amp;lt; 1)&lt;/code&gt;. This produces a bitstring that indicates which positions in &lt;code&gt;chunk&lt;/code&gt; had a NON-alphanumeric/underscore before it. Also note that the start of the &lt;code&gt;chunk&lt;/code&gt; is considered a NON-alphanumeric/underscore. This is because, in the first bit position, we shift in a 0, then unconditionally invert that to a &lt;code&gt;1&lt;/code&gt;. &lt;code&gt;~(alpha_numeric_underscores &amp;lt;&amp;lt; 1)&lt;/code&gt; effectively matches the regular expression &lt;code&gt;/(?&amp;lt;=^|\W)./g&lt;/code&gt; (&lt;a href=&quot;https://regexr.com/8e4qs&quot;&gt;regexr link&lt;/a&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Next, we AND the last expression with &lt;code&gt;alpha_underscores&lt;/code&gt;. This leaves us with a bitstring that tells us where we have an alpha/underscore character that was preceded by a NON-alpha_numeric_underscore character. As a regular expression, this would be &lt;code&gt;/(?&amp;lt;=^|\W)[a-zA-Z_]/g&lt;/code&gt; (&lt;a href=&quot;https://regexr.com/8e4rq&quot;&gt;regexr link&lt;/a&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;identifier_ends&lt;/code&gt; is computed in much the same way but in the opposite direction.&lt;/p&gt;
&lt;p&gt;With these two bistrings in hand, we can pass each of these into a &lt;em&gt;vector compaction&lt;/em&gt; operation (called a &quot;vector compression&quot; on AVX-512 and RISC-V) to figure out where &lt;strong&gt;all&lt;/strong&gt; identifiers in a chunk start and end simultaneously. A vector compaction accepts a bitstring and a vector, and keeps all elements in the vector which in the corresponding position in the bitstring have a 1 bit. The &quot;kept&quot; elements are concentrated in the front of the resulting vector, and the rest are discarded. In our case, we want to pass a vector which counts from 0 to 63 inclusive, so we can determine at which position in the chunk all tokens began and ended. See &lt;a href=&quot;https://validark.dev/presentations/simd-intro#keywords-lookup&quot;&gt;this animation&lt;/a&gt; as an example. For an exploration of how to do a vector compaction on ARM, see &lt;a href=&quot;/posts/vector-compression-in-interleaved-space-on-arm/&quot;&gt;my article on the subject&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Future plans&lt;/h2&gt;
&lt;p&gt;There is still work to do, and several optimizations to be had.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;I have done a lot of work intended to try processing 512-bytes at once, since we can fit 512-bit bitstrings in an AVX-512 vector.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I wrote custom lowerings for u512 &lt;a href=&quot;https://github.com/Validark/Accelerated-Zig-Parser/blob/7fa80343eb177c0220276492d467cdf6d3dabb73/src/main.zig#L1770&quot;&gt;shl&lt;/a&gt;/&lt;a href=&quot;https://github.com/Validark/Accelerated-Zig-Parser/blob/7fa80343eb177c0220276492d467cdf6d3dabb73/src/main.zig#L1781&quot;&gt;shr&lt;/a&gt;, borrowed a u512 &lt;a href=&quot;https://github.com/Validark/Accelerated-Zig-Parser/blob/7fa80343eb177c0220276492d467cdf6d3dabb73/src/main.zig#L1802&quot;&gt;sub&lt;/a&gt; implementation, and even wrote a &lt;a href=&quot;https://github.com/Validark/Accelerated-Zig-Parser/blob/7fa80343eb177c0220276492d467cdf6d3dabb73/src/main.zig#L3878-L3946&quot;&gt;~70 line compiler&lt;/a&gt; for convenient &lt;a href=&quot;http://0x80.pl/notesen/2015-03-22-avx512-ternary-functions.html&quot;&gt;vpternlogq&lt;/a&gt; optimization.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The way that &lt;a href=&quot;https://github.com/Validark/Accelerated-Zig-Parser/blob/7fa80343eb177c0220276492d467cdf6d3dabb73/src/main.zig#L2455&quot;&gt;loop-carried variables are handled could probably be more efficient&lt;/a&gt;, either through &lt;a href=&quot;https://github.com/Validark/Accelerated-Zig-Parser/blob/7fa80343eb177c0220276492d467cdf6d3dabb73/src/main.zig#L2471&quot;&gt;packing them all into a single register&lt;/a&gt; (less likely to win) or using 2 or 3 enums instead of so many individual carried-bits (more likely to win). Luckily I wrote &lt;code&gt;carry.get&lt;/code&gt;/&lt;code&gt;carry.set&lt;/code&gt; methods so it shouldn&apos;t hurt too badly to swap the implementations out.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;I intend to support multiple &lt;a href=&quot;https://github.com/Validark/Accelerated-Zig-Parser/blob/7fa80343eb177c0220276492d467cdf6d3dabb73/src/main.zig#L317&quot;&gt;comptime-switches&lt;/a&gt; for different ways of consuming the tokenizer. Some might prefer comments to be emitted, others prefer them to be omitted. Some people should use my idea of having a token be a 2-byte &lt;code&gt;len+tag&lt;/code&gt; (with a 0 in the len indicating we need more than a byte, then using the next 4 bytes), others might want a more conventional 4-byte &lt;code&gt;start_index&lt;/code&gt; + 4-byte &lt;code&gt;end_index&lt;/code&gt; or &lt;code&gt;len&lt;/code&gt;, and a 1-byte &lt;code&gt;tag&lt;/code&gt;. Either way, iterators will be provided which abstract over the memory differences/implications.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I currently disabled the code which expands the len of keyword/symbol tokens to include surrounding whitespace+comments. This will come back under a flag soon.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I also intend to give the best talk I have ever given on how all of the components work together this July at &lt;a href=&quot;https://www.youtube.com/@UtahZig&quot;&gt;Utah Zig&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Running the Benchmark&lt;/h2&gt;
&lt;p&gt;Want to run the benchmark?&lt;/p&gt;
&lt;p&gt;Well, unfortunately, at the moment, &lt;strong&gt;only x86-64 machines supporting the AVX-512 instruction set are supported&lt;/strong&gt;. That means you need one of the last two generations of AMD hardware or a beefy Intel server.&lt;/p&gt;
&lt;p&gt;If you do have a qualifying machine: First, clone my repository and then clone some Zig projects into &lt;code&gt;src/files_to_parse&lt;/code&gt;. E.g.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git clone https://github.com/Validark/Accelerated-Zig-Parser.git --depth 1
cd Accelerated-Zig-Parser
cd src/files_to_parse
git clone https://github.com/ziglang/zig.git --depth 1
git clone https://github.com/tigerbeetle/tigerbeetle.git --depth 1
git clone https://github.com/oven-sh/bun.git --depth 1
# Whatever else you want
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, make sure you have Zig 0.14 installed. Here is the one-off script from &lt;a href=&quot;https://webinstall.dev/webi/&quot;&gt;Webi&lt;/a&gt; to download and install Zig:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -sS https://webi.sh/zig@0.14 | sh; \
source ~/.config/envman/PATH.env
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Personally, I use &lt;a href=&quot;https://webinstall.dev/webi/&quot;&gt;Webi&lt;/a&gt;&apos;s helper script, which can be installed like so:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -sS https://webi.sh/webi | sh; \
source ~/.config/envman/PATH.env
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The helper script reduces the noise:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;webi zig@0.14
# could also try @latest or @stable
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then build it and execute!&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;zig build -Doptimize=ReleaseFast
./zig-out/bin/exe
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you are on Linux, you can enable performance mode like so:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo cpupower frequency-set -g performance
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I typically bind to a single core, which can help with reliability, especially when testing on devices with separate performance and efficiency cores:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;taskset -c 0 ./zig-out/bin/exe
&lt;/code&gt;&lt;/pre&gt;
&lt;h6&gt;(It feels like this would also prevent the OS from moving your running process to another core in the middle of a benchmark?)&lt;/h6&gt;
</content:encoded></item><item><title>Local Compiler Explorer Setup for Zig</title><link>https://validark.dev/posts/local-zig-compiler-explorer-how-to/</link><guid isPermaLink="true">https://validark.dev/posts/local-zig-compiler-explorer-how-to/</guid><description>How to setup a local compiler explorer instance for Zig</description><pubDate>Tue, 04 Feb 2025 14:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Compiler Explorer is known for its ability to automatically handle all the necessary setup to view source code side-by-side with assembly code. One simply navigates to &lt;a href=&quot;https://godbo.lt/&quot;&gt;godbo.lt&lt;/a&gt;, and, after the download completes, everything just works.&lt;/p&gt;
&lt;p&gt;However, often times I prefer to use a local setup because it is faster and does not require sending a request to an external server to compile my code.&lt;/p&gt;
&lt;p&gt;This article will walk through how to set up Compiler Explorer on a local machine.&lt;/p&gt;
&lt;h2&gt;Prerequisites&lt;/h2&gt;
&lt;p&gt;First, open a terminal with &lt;a href=&quot;https://nodejs.org/en&quot;&gt;Node.js&lt;/a&gt; and &lt;a href=&quot;https://ziglang.org/&quot;&gt;Zig&lt;/a&gt; installed. If done, skip to the &lt;a href=&quot;#setup&quot;&gt;setup section of this article&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If you are unsure whether you have &lt;a href=&quot;https://nodejs.org/en&quot;&gt;Node.js&lt;/a&gt;, you can always check by pasting the following command into your terminal and hitting enter:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;node -v
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If &lt;a href=&quot;https://nodejs.org/en&quot;&gt;Node.js&lt;/a&gt; exists on your system, you will see some version number like so:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;v22.13.1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you see something like &quot;command not found&quot;, you will have to install it. You may install it via any package manager, but I prefer using &lt;a href=&quot;https://webinstall.dev/webi/&quot;&gt;Webi&lt;/a&gt;. Simply paste this into your terminal and hit enter.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -sS https://webi.sh/node | sh; \
source ~/.config/envman/PATH.env
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you are unsure whether you have &lt;a href=&quot;https://ziglang.org/&quot;&gt;Zig&lt;/a&gt;, you can always check by pasting the following command into your terminal and hitting enter:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;zig version
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If it says something like &quot;command not found&quot;, you can install it via &lt;a href=&quot;https://webinstall.dev/webi/&quot;&gt;Webi&lt;/a&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -sS https://webi.sh/zig | sh; \
source ~/.config/envman/PATH.env
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://webinstall.dev/webi/&quot;&gt;Webi&lt;/a&gt; also conveniently comes with a little helper script that will allow you to update or switch versions via &lt;code&gt;webi node@&amp;lt;tag&amp;gt;&lt;/code&gt; (you can use &lt;code&gt;@lts&lt;/code&gt; for long-term support, &lt;code&gt;@beta&lt;/code&gt; for pre-releases, or &lt;code&gt;@x.y.z&lt;/code&gt; for a specific version).&lt;/p&gt;
&lt;h2&gt;Setup&lt;/h2&gt;
&lt;p&gt;Next, navigate in the terminal to the folder where you want the eventual &quot;compiler-explorer&quot; folder to live. I usually install GitHub stuff in &lt;code&gt;Documents/github&lt;/code&gt;. So I would do:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd Documents/github
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, &lt;code&gt;git clone&lt;/code&gt; the Compiler Explorer and &lt;code&gt;cd&lt;/code&gt; into it:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git clone https://github.com/compiler-explorer/compiler-explorer.git --depth 1
cd compiler-explorer/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, we want to download all the dependencies of &lt;code&gt;Compiler Explorer&lt;/code&gt;. Luckily, they have a very easy-to-use helper:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git pull origin main
make prebuild EXTRA_ARGS=&apos;--language zig&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can re-run that command any time you want to pull the latest changes from GitHub and rebuild locally.&lt;/p&gt;
&lt;p&gt;(I use &lt;code&gt;EXTRA_ARGS=&apos;--language zig&apos;&lt;/code&gt; here because I only care about the dependencies necessary for using Godbolt with the Zig compiler.)&lt;/p&gt;
&lt;p&gt;Next, we are going to download a little script I wrote (disclosure: AI wrote the first draft) and name it &lt;code&gt;zig.sh&lt;/code&gt;. Then we give it permission to be &quot;executable&quot;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -o zig.sh https://raw.githubusercontent.com/Validark/Zig-Compiler-Explorer-Shim/refs/heads/main/zig.sh
chmod +x zig.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Usage&lt;/h2&gt;
&lt;p&gt;Now we can start the Compiler Explorer Server by simply running:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;./zig.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This script will try to automatically find all of your zig compilers that are installed in the same place as the one in your PATH and emit a &lt;code&gt;zig.local.properties&lt;/code&gt; file which lists each one for Compiler Explorer. If it doesn&apos;t, feel free to &lt;a href=&quot;https://github.com/Validark/Zig-Compiler-Explorer-Shim/issues&quot;&gt;open an issue here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If everything worked properly, it should open on a local port you can open in your browser.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./listening_on_localhost.png&quot; alt=&quot;A screenshot of the output that shows it opened a port on localhost&quot; /&gt;&lt;/p&gt;
&lt;p&gt;For me, I can access it by navigating to http://localhost:10240/&lt;/p&gt;
&lt;h2&gt;Configuration&lt;/h2&gt;
&lt;p&gt;I recommend changing some of the default settings of Compiler Explorer. In your browser, click &quot;More&quot; and then &quot;Settings&quot;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./select_settings_small.png&quot; alt=&quot;A screenshot of where the &amp;quot;More&amp;quot; and &amp;quot;Settings&amp;quot; buttons are located&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Then navigate to the &quot;Compilation&quot; tab, untick &quot;Use automatic delay before compiling&quot;, and move the slider below it all the way to 0.25s. This makes the compiler feel a lot snappier.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./change_delay_before_compiling.png&quot; alt=&quot;A screenshot of how to follow the previous steps&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Now you can enjoy Compiler Explorer locally!&lt;/p&gt;
&lt;h2&gt;Changing the Compiler Target&lt;/h2&gt;
&lt;p&gt;In the &quot;Compiler Options&quot; field you might want to try different targets.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./compiler_options.png&quot; alt=&quot;A screenshot showing the &amp;quot;Compiler Options&amp;quot; field&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The format is &lt;code&gt;-target &amp;lt;arch&amp;gt;&amp;lt;sub&amp;gt;-&amp;lt;os&amp;gt;-&amp;lt;abi&amp;gt;&lt;/code&gt; (ABI is optional). Examples:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-target aarch64-macos
# OR
-target x86_64-windows
# OR
-target riscv64-linux
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can also add an &lt;code&gt;-mcpu=&lt;/code&gt; flag and if you click the &quot;Output&quot; button at the bottom it will show you a list of options for a given architecture.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./list_cpu_arches.png&quot; alt=&quot;A screenshot showing how to enter &amp;quot;-mcpu=&amp;quot; in the right spot and clicking &amp;quot;Output&amp;quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;I personally use &lt;code&gt;-target x86_64-linux -mcpu=znver5&lt;/code&gt;. If you want to target an M-series MacBook, you could use &lt;code&gt;-target aarch64-macos -mcpu=apple_m3&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;My &lt;code&gt;zig.sh&lt;/code&gt; script automatically sets the compiler to use &lt;code&gt;-O ReleaseFast&lt;/code&gt;. You can override this in the same &quot;Compiler Options&quot; field if you want. You could try:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-O ReleaseSafe
# OR
-O ReleaseSmall
# OR
-O Debug
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note that, for some reason, source-mapping does not work with &lt;code&gt;ReleaseSmall&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Have fun!&lt;/p&gt;
&lt;p&gt;‒ Validark&lt;/p&gt;
</content:encoded></item><item><title>Eine Kleine Vectorized Classification</title><link>https://validark.dev/posts/eine-kleine-vectorized-classification/</link><guid isPermaLink="true">https://validark.dev/posts/eine-kleine-vectorized-classification/</guid><description>A simple technique for vectorized classification</description><pubDate>Sun, 29 Sep 2024 09:35:00 GMT</pubDate><content:encoded>&lt;p&gt;For the new version of my SIMD Zig parser &lt;a href=&quot;https://www.youtube.com/watch?v=NM1FNB5nagk&quot;&gt;I gave a talk about&lt;/a&gt; on October 10, I came up with a slightly better technique for &lt;em&gt;Vectorized Classification&lt;/em&gt; than the one used by Langdale and Lemire (&lt;a href=&quot;https://arxiv.org/pdf/1902.08318&quot;&gt;2019&lt;/a&gt;) for &lt;a href=&quot;https://github.com/simdjson/simdjson&quot;&gt;simdjson&lt;/a&gt;, in that it stacks slightly better.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Vectorized Classification&lt;/em&gt; solves the problem of quickly mapping some bytes to some sets. For my use case, I just want to figure out which characters in a vector called &lt;code&gt;chunk&lt;/code&gt; match any of the following:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const single_char_ops = [_]u8{ &apos;~&apos;, &apos;:&apos;, &apos;;&apos;, &apos;[&apos;, &apos;]&apos;, &apos;?&apos;, &apos;(&apos;, &apos;)&apos;, &apos;{&apos;, &apos;}&apos;, &apos;,&apos; };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To do this, I create a shuffle vector that we will pass into the &lt;code&gt;table&lt;/code&gt; parameter of &lt;code&gt;vpshufb&lt;/code&gt;. &lt;code&gt;vpshufb&lt;/code&gt; is an x86-64 instruction that takes a &lt;code&gt;table&lt;/code&gt; vector and an &lt;code&gt;indices&lt;/code&gt; vector, and returns a vector where the value at position &lt;code&gt;i&lt;/code&gt; becomes &lt;code&gt;table[indices[i]]&lt;/code&gt; for each 16-byte section of the &lt;code&gt;table&lt;/code&gt; and &lt;code&gt;indices&lt;/code&gt;. Depending on how new a machine is, this allows us to lookup 32 or 64 bytes simultaneously into a 16-byte lookup table (one could also use a different 16-byte table for each 16-byte chunk, but typically we duplicate the same 16-byte table for each chunk). Here is how it is depicted on &lt;a href=&quot;https://www.officedaytime.com/simd512e/simdimg/si.php?f=pshufb&quot;&gt;officedaytime.com&lt;/a&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./pshufb_3.png&quot; alt=&quot;An image depicting a vpshufb instruction. It is shown looking up 32 indices into a 32 byte vector. Each half of these vectors are operated on separately. That means the first 16 byte indices only look at the first 16 bytes in the table vector, and the second 16 byte indices only look at the second 16 bytes in the table vector.&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Here is the &lt;code&gt;table&lt;/code&gt; generator:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;comptime var table: @Vector(16, u8) = @splat(0);
inline for (single_char_ops) |c|
	table[c &amp;amp; 0xF] |= 1 &amp;lt;&amp;lt; (c &amp;gt;&amp;gt; 4);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As you can see, the index we store data at is &lt;code&gt;c &amp;amp; 0xF&lt;/code&gt; where &lt;code&gt;c&lt;/code&gt; is each of &lt;code&gt;{ &apos;~&apos;, &apos;:&apos;, &apos;;&apos;, &apos;[&apos;, &apos;]&apos;, &apos;?&apos;, &apos;(&apos;, &apos;)&apos;, &apos;{&apos;, &apos;}&apos;, &apos;,&apos; }&lt;/code&gt;. The data we associate with the low nibble given by &lt;code&gt;c &amp;amp; 0xF&lt;/code&gt; is &lt;code&gt;1 &amp;lt;&amp;lt; (c &amp;gt;&amp;gt; 4)&lt;/code&gt;.
This takes the upper nibble, and then shifts &lt;code&gt;1&lt;/code&gt; left by that amount. This allows us to store up to 8 valid upper nibbles (corresponding to the number of bits in a byte), in the range &amp;lt;span style=&quot;white-space: nowrap&quot;&amp;gt;$\footnotesize \left[0,\ 7\right]$.&amp;lt;/span&amp;gt; This isn&apos;t quite &amp;lt;span style=&quot;whitespace: nowrap&quot;&amp;gt;$\footnotesize \left[0,\ 15\right]$,&amp;lt;/span&amp;gt; the actual range of a nibble (4 bits), but for our use-case, we only are matching ascii characters, so this limitation does not affect us. Then we just have to do the same transform &lt;code&gt;1 &amp;lt;&amp;lt; (c &amp;gt;&amp;gt; 4)&lt;/code&gt; on the data in &lt;code&gt;chunk&lt;/code&gt; and do a bitwise &lt;code&gt;&amp;amp;&lt;/code&gt; to test if the upper nibble we found matches one of the valid options.&lt;/p&gt;
&lt;p&gt;E.g. &lt;code&gt;;&lt;/code&gt; is &lt;code&gt;0x3B&lt;/code&gt; in hex, so we do &lt;code&gt;table[0x3B &amp;amp; 0xF] |= 1 &amp;lt;&amp;lt; (0x3B &amp;gt;&amp;gt; 4);&lt;/code&gt;, which reduces to &lt;code&gt;table[0xB] |= 1 &amp;lt;&amp;lt; 0x3;&lt;/code&gt;, which becomes &lt;code&gt;table[0xB] |= 0b00001000;&lt;/code&gt;. &lt;code&gt;[&lt;/code&gt; is &lt;code&gt;0x5B&lt;/code&gt;, so we do &lt;code&gt;table[0xB] |= 0b00100000;&lt;/code&gt;. &lt;code&gt;{&lt;/code&gt; is &lt;code&gt;0x7B&lt;/code&gt;, so we do &lt;code&gt;table[0xB] |= 0b10000000;&lt;/code&gt;. In the end, &lt;code&gt;table[0xB]&lt;/code&gt; is set to &lt;code&gt;0b10101000&lt;/code&gt;. This tells us that &lt;code&gt;3&lt;/code&gt;, &lt;code&gt;5&lt;/code&gt;, and &lt;code&gt;7&lt;/code&gt; are the valid upper nibbles corresponding to a lower nibble of &lt;code&gt;0xB&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;We can query &lt;code&gt;table&lt;/code&gt; for each value in &lt;code&gt;chunk&lt;/code&gt; like so:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vpshufb(table, chunk);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Just ~2 cycles later, we will have completed 32 or 64 lookups simultaneously! Note that we don&apos;t have to take the lower 4 bits via &lt;code&gt;&amp;amp; 0xF&lt;/code&gt;, because &lt;code&gt;vpshufb&lt;/code&gt; does that automatically unless the upper nibble is 8 or above, i.e. when the byte is 128 or higher. For those bytes, &lt;code&gt;vpshufb&lt;/code&gt; will zero the result, regardless of what&apos;s in the table. However, we already said we don&apos;t care about non-ascii bytes for this problem, so we are fine with those being zeroed out.&lt;/p&gt;
&lt;p&gt;Now all we need to do is verify that the upper nibble matches the data we stored in &lt;code&gt;table&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;To do so, we can produce a vector like so:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const upper_nibbles_as_bit_pos = @as(@TypeOf(Chunk), @splat(1)) &amp;lt;&amp;lt; (chunk &amp;gt;&amp;gt; @splat(4));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Unfortunately, at the moment, LLVM gives us a pretty expensive assembly routine for the above line of code (&lt;a href=&quot;https://zig.godbolt.org/#z:OYLghAFBqd5QCxAYwPYBMCmBRdBLAF1QCcAaPECAMzwBtMA7AQwFtMQByARg9KtQYEAysib0QXACx8BBAKoBnTAAUAHpwAMvAFYgAzKVpMGoAF55gpJfWQE8Ayo3QBhVLQCuLBiABMpJwAyeAyYAHKeAEaYxPoAbKQADqgKhPYMrh5evonJqQJBIeEsUTF68daYtmlCBEzEBBme3n4VVQI1dQQFYZHRcVa19Y1ZLYNdwT3FfWUAlFao7sTI7BxoDAoEANQb6JsApHoAIpsAAngsSfUQez4%2BOzc%2BMwcAQnsaAIJrG5sR7nR2DH2R1O50uBGut1%2B/2CDyeeleHze70wqjBmyogP4qAgyAQ7gYAGsQKcAGqVIjECB6PybdwADhmM1OABUAJ4JTAAeSoOLxhKZewA7Aj3psxZtiJgCItAScmAoICc2Rzubz8QS5qcFAkjOCuIygc4Ds5TgRiPjRARMGr%2BS8kULDvaPhj0ahUD4bUTSeSSFSafSDUr2Vyebj1QLhUjxZsvltahF6EDjmgLnY2K7sTsAHQpFjoLOSjlMcEnFKmEOK5UVsP8zXZ3P5%2By1CD00ibLixAV6I3drU64sQSSMuEi6OS6XEQEANwSCjxVAiEHj9DbNYJQOwB2wfd1g8ZdsRgsdiOd09n88Xy/Ym2MrIIwbbwXwywUxKDKp5V8DVdVX/2kY%2BaM8CoTZFWCVxU3Oa0DSFUdo3FKc6glTAFHcWgCDfH8eSfPAXy7Y58SwGgQnQA9RXgsV%2BGIUCNCzLMTgiQghDwctVXfCscLwpkAHpNgZf9nDwIUTVgqMKPg2NNifFEkykhhnxQvYAFZniEpTHXhMTxPFSVUPQ5TVOU44DmOYDQOk1QNxMzYNFUOkNCZDRNkwWglE2K8DIs/YfCU0CGKYliK3Y38mATTADV4hkjLI7SHSdciKPHGVkL0ggYvFOKTwSmMBG%2BNgCAQDAFFkjZzVsf84IolErUndFZQeWhaCnFgszs2IsyYKdVCUrgfCzc93CoLMIizHqfAeRUyVsX1YmkWkGTbE4popCBZrbAMmSWn1KTW%2BaRy0%2BDquiTF6tuRrmtaul2s61Q%2BoGoaIgmrbpspal1oW70Xr9d7A2W303r29KqtUGqTtOBqmpatqcwUJQ9H6udBuGrNerpJ6/spDsfsWjGICxvbNtx/GAyB/8NJFA6ktqhQAHdCFxPysKXUL6BggDsujZ6Vt2gMky3KSQJxVBIPTbNmTqYApUu9qqEwYtFkwIQpQACXlCAoXQ4Is2QBJ3CzWX5d0tsOq6saIhpg18sK9AFHoiGLuhm6xoR%2BdkbGiar0feTcJQpkXLck4UwSOgcGIYhfQeZxlDkdyJalTYjGQAlitQhI0SozYZ0Rhd3duOYDs53GAd5kz%2BbMoWRcwbYCHzcXiElghpf1uWJ0VlW1Y1gFtd15vDZQ42bseJkraKu2zshpvB5dpHHtuZmwq9hSFD91yq8D4Xg/obAw4j24o5jwYG4Tpgk5T9w05ILYM6zi8fNiWFSAL8Uud9Yn%2BNLvRt3LoO0yrsW48btDA2rclYEFVgqTuWsdZ62AQrBQxtYZw0tlKa2tsTj2yhldGGcNp4PRRj4NGc9PZySXivAOQcQ7b3DpSSO0dY713jonZO2xz7pxIJne6ERUYPyfmKf2VcP7bnXhcShO9KTZioCwRuP8oLKGIMEcEDwACSDBEK0DwLsCWnhGBxmDJsBI8olC7CIBw7OERiSiSPMpZwDAHjG1gsFT8LNwoOkZI/QCGUrGaUPIcDgcxaCcCUrwbw3BeCoE4AALQsNsBYSwBE%2BD0HoXgGEOBaDcQgOWWAYgQDmESJStEuB0h8BoQUiS9BcEFFwJSkgDABI4JIXgLAJAaA0KQEJWhSDhI4LwV8rSUlpNIHAWASAKH0DIBQCum8%2BimA0FwPQrSaDoWiK%2BdWmheCMWYMQVknAeCkA2XUVknIIjaHJDs3gKY2CCE5AwWg2zUm8CwL8YAzgxCuTOaQLALBjDAHEPcj5eBJRVCnChNZ/hVCVHcFad5CiXKgvUREYgBzXBYFBWac47zgXEAiMkTAhxMBfJMOokway5hUCMMABQJI8CYBppyDkITdn8EECIMQ7ApAyEEIoFQ6g/m6C4IYb5IBzCWHha%2BSAcxUAJDsLlTgABaTkmwABKLk5ZKAAGLyi2LKw%2B8c2oAH1ZqyqJe4SysqWDQJMqYVR0QkmdMxfIrAYqclWBVW0bwEAnDDG8PywIEwiglAkDkFI0r0huCaIGpIwa0jdH9X0flrQQ0dCGGGrI8bXWJrGDG3oMR41jC9YGjYnQs1TBzXMBQsTlj6H8YE4JoKumbGFTZFG8MnIQFwIQdhNxEkzGSSSuYGSmBZMoLkkAkgACcWY6QVLHT4WIc6il0kkEpOkSlDCcAaaQJpXAWltLrZwHpIA%2Bl9rXRwHwta/ldN7fctxmKUgOEkEAA%3D%3D&quot;&gt;Godbolt link&lt;/a&gt;):&lt;/p&gt;
&lt;p&gt;&amp;lt;div id=&quot;issue-dump&quot; style=&quot;display: flex; flex-direction: column; align-items: center; align-self: flex-start&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;script&amp;gt;
document.getElementById(&quot;issue-dump&quot;).innerHTML = [110317].map(i =&amp;gt; `&amp;lt;div class=&quot;individual-issue&quot;&amp;gt;
&amp;lt;div&amp;gt;&amp;lt;svg id=&quot;issue-indicator-${i}&quot; class=&quot;issue-indicator issue-indicator-unknown&quot; viewBox=&quot;0 0 16 16&quot; version=&quot;1.1&quot; width=&quot;20&quot; height=&quot;20&quot; aria-hidden=&quot;true&quot;&amp;gt;&amp;lt;path stroke=&quot;none&quot; fill=&quot;#59636e&quot; d=&quot;M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Zm9.78-2.22-5.5 5.5a.749.749 0 0 1-1.275-.326.749.749 0 0 1 .215-.734l5.5-5.5a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042Z&quot;&amp;gt;&amp;lt;/path&amp;gt;&amp;lt;/svg&amp;gt;&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;div&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;p&amp;gt;&amp;lt;a href=&quot;https://github.com/llvm/llvm-project/issues/${i}&quot;&amp;gt;llvm/llvm-project#${i}&amp;lt;/a&amp;gt;&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;`).join(&quot;\n\n&quot;);
&amp;lt;/script&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.LCPI0_1:
        .zero   32,16
.LCPI0_2:
        .zero   32,252
.LCPI0_3:
        .zero   32,224
.LCPI0_4:
        .byte   1
foo:
        vpsllw  ymm0, ymm0, 5
        vpbroadcastb    ymm1, byte ptr [rip + .LCPI0_4]
        vpblendvb       ymm1, ymm1, ymmword ptr [rip + .LCPI0_1], ymm0
        vpand   ymm0, ymm0, ymmword ptr [rip + .LCPI0_3]
        vpsllw  ymm2, ymm1, 2
        vpand   ymm2, ymm2, ymmword ptr [rip + .LCPI0_2]
        vpaddb  ymm0, ymm0, ymm0
        vpblendvb       ymm1, ymm1, ymm2, ymm0
        vpaddb  ymm0, ymm0, ymm0
        vpaddb  ymm2, ymm1, ymm1
        vpblendvb       ymm0, ymm1, ymm2, ymm0
        ret
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Luckily, there is a very easy way to map nibbles to a byte. Use &lt;code&gt;vpshufb&lt;/code&gt; again! Actually this works out better for us because we can map upper nibbles in the range $\footnotesize \left[8,\ 15\right]$ to &lt;code&gt;0xFF&lt;/code&gt;. We&apos;ll see why later.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;comptime var powers_of_2_up_to_128: [16]u8 = undefined;
inline for (&amp;amp;powers_of_2_up_to_128, 0..) |*slot, i| slot.* = if (i &amp;lt; 8) @as(u8, 1) &amp;lt;&amp;lt; i else 0xFF;

const upper_nibbles_as_bit_pos = vpshufb(powers_of_2_up_to_128, chunk &amp;gt;&amp;gt; @splat(4));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Much better emit! (&lt;a href=&quot;https://zig.godbolt.org/#z:OYLghAFBqd5QCxAYwPYBMCmBRdBLAF1QCcAaPECAMzwBtMA7AQwFtMQByARg9KtQYEAysib0QXACx8BBAKoBnTAAUAHpwAMvAFYgAzKVpMGoAF55gpJfWQE8Ayo3QBhVLQCuLBiABMpJwAyeAyYAHKeAEaYxBJ%2BAA6oCoT2DK4eXr6kCUl2AkEh4SxRMVx%2B1pi2KUIETMQEaZ7eZZg2uQzVtQT5YZHRsVY1dQ0ZZYNdwT1FfaUAlFao7sTI7BxoDAoEANQb6JsApHoAIpsAAngsCXUQez4%2BOzc%2BMwcAQnsaAIJrG5sR7nR2DH2R1O50uBGut1%2B/2CDyeeleHze7yogPccTi0QA%2Bgw8BEIvQFJimISIoRMdlsUw8AA3TAQZAIdwMADWIFOADUKkRiBA9H5Nu4ABwzGanAAqAE8MQB5Kj0xks0V7ADsCPemw1m2ImAIi0BJ2JEBOkplcoZTOZc1OCjiRnBXBFQOcB2cpwIxCZogIdPNipeSJVhwDH0wqjBmxRAvRWJxeIJRJJZOy8otbJOnNsJF5/KFjuNUswspTiv2qqRms2aAudjYm2ptU2CQA7tFCagqJifJi0ZiiJjSoK2XsAKzPLgANhHhyFQOOTKwNBC6H9HwrwVoEwjJE2EPHzdbmPbne7cV7qH7PkFpE2GgAdLelcrnAAqBS0VAEa94FWut8f2/PrOmx4FQO54E6mzCqchpCteDpOi6wGbC0Sg3qoABi6EruqmrarqxCAtScQKIyVARBA%2B7EG2HZdj2fYDtevrMkC2AHNg1q2kw4KSCK2GBsGyKEcRpHkTU%2BLsJsxgSgQBZfgw%2BDLAobJSTJGKiiBO4nEkpiFnK%2BamhAwQKZgCiOmx1p4DpRb6bpEBifQIqijZRZGXgimiihmDigWRb2Zgj5qhWXxbEIcgAOJhdgQhitghyYuy2DOJiQgAJIAFrYEOejoVWcQ1pgmLBFsBzHDst5JCw6Dle4wDACZBAZtyASMMABAIOhJDOHE7gQLBPx/LQAK3sg3Vwmq5aahpRrBK41bnHSorbqFEVRTFcUJUlqUZbOJWbAw7i0LQAUTRWGr1sQWomQdBBpiatmue5QHzpgi6YMu8Inad/AXRAd63icpLCJZtnOXKD0mY6AD0kGPs435PqWgWncjwXAfJoZAeDCgjs837DkGH2rsjp3ago1043jxy7VNRkY2xu0aKogoaKKGjIbQqF%2BRT6OqPsPjDppgNCMD1l3b5TDiVDMNTthxOlkGiJE8jeF6pdZODbLmr8YrOEalNWki3pYtg/JbkQ0qejsQbVlGz5cp%2BY5iOfRWKsEXWwnuGREBlRVVXahiXFGtpIPG4ZpvuVaDtycZpmaxq2vvJ9%2BvB6Ldt2RLDkW%2Bxy2RdFsXxYlyXpdgt4HOhx1K6d52Np0Sn7KO1sh2nDuitDOerfnG1FxlpfZVO6Zclm7d5%2BthdbSXZfXrmT3o6971I8j66bt9O43HutfXn9sOvu%2Bn7AT%2BTuV3Lf4EABQFESRnvkT75xVaG7pMLY6fiV%2BmyAcPa0F5txe9%2Bh14f53MeP8y5WhvpVW899iCP3BFjV%2B79wq50/l3cev9/4II7qPb%2BPcQFjWdlrZUCtdYkx1KrAGhBnDEnBHEWuuDK4J0%2BqjNgbUMAKCAhsD0thD5EIrPfaIgJIwnAeIdakLBbxM3HLeJg1JVDDlKLeD2VBbwRFvLInwDwjSNSzOOaQAphTXgHpmHk2ip7CicpooxOjcxx1Orwt2AihG0BEWIwUEipGqB8PIy%2BiiIjqIMdybMJirR%2BKzHyQJZjB48lCbo2h3DNS2P4fqBxTjxHlQUEoPQnjSJKNvAOXx5iIATkCfo/JhTonhMMQU8cgTrH8UJonSurtAQKCbIQBkmlQ4ty4XLYJFjAk7UtsBUC9JUBzVrGVMUtQ6qnxSVQTAXFFiYCEDqAAEoaKEg1gjDW6reWZ8zSbXkkdI1REQmyOiYQgFh/0kmiJSW41RmTPbZNUeovy0czamXZqhE4uU6A4GIMQLMDxnDKDkJsQYUzNhGGQMyVhZN0QkC2CvC%2BIlnm3DmHgjUPSAnRP6exKauV8rbAIFVCZxApnOIkbs/CiyVlrIGkNEa7gdlzOpQoA5bjHiinOZcwRtxhE3JcYc9xDzvEvIzpgN5j1PKnB%2BfQbA/zAW3GBaC8FOpIWPxhdsNE4YkUKIiPzScaLSAYo5BEypfSSrmXxSMvK80iUksmTqClzK9k0oIKshQEB1kMu2VShZbLNipPSWcnUFz0AKCuXyxxAqJFpPSSK7JuTbjP3oJKiGnyvLfJtb8%2BVAKeRApBWCx1WwoWarhTq7cyKr5JseMao%2BcSOZeUtQMrNFwc0Kp5GVKgLBT4EvmsoYgRUIQ%2BBSgwesG5diTM8IwLYqkvLULSW9MFqB3ZeIiEOMsBCRzOAYA8A5KpnigxTf5QMIo61ENqeNAhHA5i0E4MOXg3gOBaFIKgTgaULDbAWEsJtPg9B6F4DdZ9N65gIDmVgGIEA5ismHHeLggofAaGVP%2BvQXBlRcGHJIAwd6OCSF4CwCQGgNCkCfS%2Bt9HBeBKWI0BrQcw4CwCQLK6I5BKBMZiKYDQXA9DEZoINVslB13AdIKSZgxAJScB4MJ4ItQJTSgiNoLkEneBVjYIIaUDBaDiaE1gX4wAKGHSUtwXgWAWDGGAOIbTeBtSVFpIZl9oYKjuG9Ep8gggWiaF4BuCIUCxOuCwB50g7pzgudpMQCIiRMCHEwKZkwG4TAebmFQIwwAFDsjwJgJs0oMRPsk/wQQIgxDsCkDIQQigVDqCE7oLghgzMgHMJYLzSlIBzFQLagQhmAC00pNgACUWhzKUOhShmwOuqq2OIzE2iOtxfcLzDrLBGUlVMGO6IAHX2hcHVgJrUGrD9cqA4CAThhhNGI4ECYhRij6GkNkZIAhju%2BGIzdto3QLt9D0NIco%2B32hjHu8qXbrQqhjBe70GIBgNidF%2BwMTowOpig7mAob9yx9C3vvY%2BgL5HNj1ZvDkjJbMIC4EINuG4/6ZiAYS6B8DfQduskkAATlvIKNDtOfDjlZwhwUkhhyCmHIYTgeHSAEa4ERkj6POCUZANR8nvOOA%2BDR0J8jZPgNntC0kBwkggA&quot;&gt;Godbolt link&lt;/a&gt;)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.LCPI0_0:
        ...
.LCPI0_2:
        ...
foo2:
        vpsrlw  ymm0, ymm0, 4
        vpand   ymm0, ymm0, ymmword ptr [rip + .LCPI0_0]
        vpbroadcastb    ymm1, byte ptr [rip + .LCPI0_2]
        vpshufb ymm0, ymm1, ymm0
        ret
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now we simply bitwise &lt;code&gt;&amp;amp;&lt;/code&gt; the two together, and check if it is non-0 on AVX-512 targets, else we check it for equality against the &lt;code&gt;upper_nibbles_as_bit_pos&lt;/code&gt; bitstring. I wrote a helper function for this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fn intersect_byte_halves(a: anytype, b: anytype) std.meta.Int(.unsigned, @typeInfo(@TypeOf(a, b)).vector.len) {
	return @bitCast(if (comptime std.Target.x86.featureSetHas(builtin.cpu.features, .avx512bw))
		@as(@TypeOf(a, b), @splat(0)) != (a &amp;amp; b)
	else
		a == (a &amp;amp; b));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This gives us better emit on AVX-512 because we have &lt;code&gt;vptestmb&lt;/code&gt;, which does the whole &lt;code&gt;@as(@TypeOf(a, b), @splat(0)) != (a &amp;amp; b)&lt;/code&gt; in one instruction, without even using a vector of zeroes! On AVX2 targets, we have to use a bitwise &lt;code&gt;&amp;amp;&lt;/code&gt; either way, and to do a vectorized-not-equal we have to use &lt;code&gt;vpcmpb&lt;/code&gt;+&lt;code&gt;not&lt;/code&gt;. Hence, we avoid that extra &lt;code&gt;not&lt;/code&gt; by instead checking &lt;code&gt;a == (a &amp;amp; b))&lt;/code&gt;, where &lt;code&gt;a&lt;/code&gt; has to be the bitstring that has only a single bit set, which is &lt;code&gt;upper_nibbles_as_bit_pos&lt;/code&gt; for our problem.&lt;/p&gt;
&lt;p&gt;So here is everything put together:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const single_char_ops = [_]u8{ &apos;~&apos;, &apos;:&apos;, &apos;;&apos;, &apos;[&apos;, &apos;]&apos;, &apos;?&apos;, &apos;(&apos;, &apos;)&apos;, &apos;{&apos;, &apos;}&apos;, &apos;,&apos; };
comptime var table: @Vector(16, u8) = @splat(0);
inline for (single_char_ops) |c| table[c &amp;amp; 0xF] |= 1 &amp;lt;&amp;lt; (c &amp;gt;&amp;gt; 4);
comptime var powers_of_2_up_to_128: [16]u8 = undefined;
inline for (&amp;amp;powers_of_2_up_to_128, 0..) |*slot, i| slot.* = if (i &amp;lt; 8) @as(u8, 1) &amp;lt;&amp;lt; i else 0xFF;

const upper_nibbles_as_bit_pos = vpshufb(powers_of_2_up_to_128, chunk &amp;gt;&amp;gt; @splat(4));
const symbol_mask = intersect_byte_halves(upper_nibbles_as_bit_pos, vpshufb(table, chunk));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::note
This properly produces a &lt;code&gt;0&lt;/code&gt; corresponding to bytes in &lt;code&gt;chunk&lt;/code&gt; in the range $\footnotesize \left[\mathrm{0x80},\ \mathrm{0xFF}\right]$. This is because &lt;code&gt;vpshufb(table, chunk)&lt;/code&gt; will produce a &lt;code&gt;0&lt;/code&gt; for bytes in &lt;code&gt;chunk&lt;/code&gt; in the range $\footnotesize \left[\mathrm{0x80},\ \mathrm{0xFF}\right]$ and &lt;code&gt;vpshufb(powers_of_2_up_to_128, chunk &amp;gt;&amp;gt; @splat(4))&lt;/code&gt; will produce &lt;code&gt;0xFF&lt;/code&gt; for them. &lt;code&gt;intersect_byte_halves&lt;/code&gt; on AVX-512 will do &lt;code&gt;0 != (a &amp;amp; b)&lt;/code&gt;, where &lt;code&gt;a&lt;/code&gt; is &lt;code&gt;0xFF&lt;/code&gt; and &lt;code&gt;b&lt;/code&gt; is &lt;code&gt;0&lt;/code&gt;, which will reduce to &lt;code&gt;0 != 0&lt;/code&gt;, which is &lt;code&gt;false&lt;/code&gt;. On non-AVX-512 targets, &lt;code&gt;intersect_byte_halves&lt;/code&gt; will do &lt;code&gt;a == (a &amp;amp; b)&lt;/code&gt;. Substituting the same values for &lt;code&gt;a&lt;/code&gt; and &lt;code&gt;b&lt;/code&gt;, we get &lt;code&gt;0xFF == (0xFF &amp;amp; 0)&lt;/code&gt;. This properly produces &lt;code&gt;false&lt;/code&gt; as well.
:::&lt;/p&gt;
&lt;p&gt;Compiled for Zen 3, we get (&lt;a href=&quot;https://zig.godbolt.org/#z:OYLghAFBqd5QCxAYwPYBMCmBRdBLAF1QCcAaPECAMzwBtMA7AQwFtMQByARg9KtQYEAysib0QXACx8BBAKoBnTAAUAHpwAMvAFYgAzKVpMGoAF55gpJfWQE8Ayo3QBhVLQCuLBiABMAVlInABk8BkwAOU8AI0xiXw1SAAdUBUJ7BlcPL18A5NS7ARCwyJYYuJ8E60xbdKECJmICTM9vaSqagTqGgiKI6Nj4q3rG5uy24Z7QvtKBioBKK1R3YmR2DjQGBQIAai30bYBSPQARbYABPBZkxogDnx89u585o4AhA40AQQ2t7aj3Oh2BiHE7nS7XAi3e7/QGhJ4vPTvL4/HZMBRKRog057AB0WH%2BwBxaIxBDeH0%2B5JR2yEcgA4rTsEIACrYY4AfQAathnGyhABJABa2CxuwI6BxqRY4oU7mAwEwWw51SIxCCjGABAQADESM5Eu4IO4ABykP4A2hAnHIfUIpHfAS/ZwIdwMADWIrOStsJAgNPpjJZ7K5PP5QpxRy1puNtvJ5Mwqgh2yowJoDBcCAaCj5DCEmEhyGdbpA5y9Kogeh8UaNczmovFbHqOOzkJxLtSwDC6FNZwIAE9Ephs/wIGcmf3MAB5KgQAsu101nEAN2VJBx9AYtYOAHY7ds99sqakTPQ2QWGmzUIkFCKDn5Xmzb8djdvXtswBwAH7v03v8C8N8cG834AberzAe%2Bj7gYBehalBEBQXMUEvkhW7HFBpDvocqFkl8%2B4HqgVx2Gw2yLg02y0KgqCuu4iRsvUUTiCWK7EBAXAAGxVpuoJnAoiRGJCGgxrh%2B6hLQUxJiQ2wQEewAnmexAXlem5bs4yDbs45GUdRtH0fQoHIIcPhsdsGiqFqj5Yc4RynFwIJWXoGkziC2BHMKkhCZ8eFoIRlyYCRZHJAA7rECgXlQbI%2BGyNF0agbJcD4RrFqB7GPsaIoulgqaYOgOGeSJDBiWEEnEFJdxsUFIVhRFUU6bF8UmiZOI4spzgAFQKBRBCmng6m7J1OKtSKeBUFJeB2ds1bnGihoNVwXH2RpY2YLQSgmWZ5mIrGwl7lSNEDgpDB4FEDEKmyaJslEhBsnkIqLlezpUFEEAVcQoWoOFkXRUQcUJaas5us5rnnLx/EQO5HleQ6OwKL2pRuGyLBou61nbKEBAhcqF29ujbIZrQy4KIaiT7Wyh3HfQoXnZdBDXSkpp3QoD1PRRVFfUwJ1/YW84Q/uxB5sswIw3DtAI0juXbscW2fMmqOCBjthYzjeMExATDFsY2PjqaUTqwwmsDrWuINkwTaCBArabBYnbdn2A5DqgI5jgOU6q9rC7Lt6xBrowyl2h8ACcxKxJCuKSuKaAugQTLEO4CojskiSuJHqtcZL3HTaO44u0wbvdiDTCQnNNZYijPbjvbjtZ9OOd/O7zHexu4tfAHfMEAL5zU84aKQsNUneYkRF%2BbiTINPKBA4qoRpsTiVCYAXyyYLmBAABLTTCFqhFa%2Boz3Pbd8woppEouqh%2BPFUSBTWW3%2BwHZwZ07k7V7nwN8QXECCbWYBgCjquGcZUQvM3DQ/tlpKCvgHJgJdQQ/zKrXDyEspYywZkzCAul2DbA1rbTA3U0x4FWAoXW%2BtMC1l7iOVIpgH6V2dtOUI%2BA8HFyBjxPA5CXaZyoSg9m9Aay1lYRQmhuCFS1hAX5HhLtUG%2B3JHhEhFwMgEQHr5CA9CdwSLwvuUiJV97uAtMWER1CcF0PSmmTAWUcqbW2io/gJU35NTONTIQTCKE6IgHwuhtYAD0E0Wo9RUlhXcKi/FUhofGIaeiFSgR6n4NOvi/F4Q0RaMJFkUYkMCaoQGoJTJGkEiZbYQjtioLCYYlJdw/BSRsYQOxzDpyOLEW4jxj5crRKwpLQBeU/Gt3brE0kpiWmNKlpIkapD7EsPvi7ZxAiuLCkYRUyhFDqniLMTE/mxBgRIPcI9aSYoJSXHFHzAcr9JkOOGbo2hYzTRiOwcchQPM9zwOaX0kpZCDlV3YSdehehhR%2BgZMyVknJuS8kFNgcMME5ndLwmo7YiRuj4MOHefZQynnVO2O4j5AZvnBj%2BWGCMj5PTMV9HST5gYfkhn%2BYCyM2xowGMylMExUSVGiXEhY0qRkIWNAPo1Zqll2qdW6r1F8yiGl7g6qgceg0UYrLWaHLZOJ4wEGIEwWwzz6DdW2INZFXygy/NDACiMppVUErRZqklCw6ybKlFK1QMq5U9xCaysaKq8UovVUSjFMEdX2rVYS9FWqgVXJUTckFvNFnAlKU0buz1IU%2Br9Xy/CmwdgNgQBga8KMtgx1sD4qNeFpWxBTEGp4tB8YsAnlPI%2BJ94o4nuqsnEUQcSnx8E8EcpYfRsWkGS6s3YG0sSbZxbh7aICdpbT6jN5qs1JhzfcPNi4C2T2nkwY%2BPgy2MwrVEOt2LPblkrP2ttOKKxdqYqu7d/b6nRMzUskd5xc35sLdPdESg9DzoepWnE9Vl09vYpxTdq7X39u7Tiz90ZD3XOwl0qNbST0KECoQAsJTDkKqIcChpK6yx9vJdZIGJD%2B6D2NSPYgY9L073nnzJeq9Cbr0tNadweG94KkPjOktPhz7FzjQmnEZxz0TtwzRmtd7F3VvinW1B5z%2BGXOyStYR/c6A4GIMQH0TxnDKDkLk0eeZyJytdNeGUxMSA7AZWKqINb4SkHTfuBDPp93IZOKh/p6HfKYcU%2BPKdFGF6EbXuaUj29Z74ao9sYtzxayMfQAoZjrHJ1Fpo3O8tVBK18Y4Vg2WFzBEifOGJ%2Bg2BJPSfuLJ%2BTwwx7KeQKp3Ye1NPFRIuFqI/g2L6cM3uYzLFf2TRQ281GlnZEYeHrZ3D7nKNOeIy5zeZGHP70PtezAegGN5njf5wLY6L32aG7e8LD6n33BgwJ/ROSzhJYk1JliMm5MKew0powuW1MFcxNpkri3ngGfmfuHJ9WJkbZS1t9Z4oqAsHHlZtgyhiBoyhD4bMpExL7FHp4RgOxMHgqDvsIgxWF2PSSko7CfhnAMCeIfF8VTosvFQjWK73T4FAdQhwBYtBOB%2BF4N4DgWhSCoE4AKCwuwlgrD8ncPQeheAEE0EThYCA55YDiAo0groQBsVvZIPwbF/ZcC3BoDQRo/D3B8FwQwnBJC8BYBIGXpAKdU5pxwXg%2BCEgc8p0T0gcBYAwEQCgWR4myAUBnNb%2BgcRTAyrnHwQEIVKA62N6QS6zBiC9k4DwH3oQGi9gnFEbQypA%2B8G8mwQQE4CoB%2B9/iWUXc834O4LwLAiNjxrCp/gPmNQCac8CKoao7h0bR/IHLEn3uxJRFlf71wWAS8ysuFX5cxAogpEwMcTAOfZKhFAMbhYVAjDAAUByPAmBAoTgHBToP/BBAiDEOwKQMhBCKBUOob3uhKxGBMCAcwlh6/4MgAsS8BRNicAALQTm2AAJWWnPJQWpu7bBv1lpTU62RNpv4VdwFJG/FgMjayUwBgTvNnanTvb7LAM/AXdoK/RwNMUYbwJXYIKYEoMoCQJIFINIAQVAnAvIfAhgXoLAgYJXRA2oCYQgyg5/DoHMCYMg/oOISgmgtwFoHArYboZgmYVghYBQRnVYfQYnUncnEvXXbYY/XJGOAGCAXAQgSSFnEbdnTnHHHnJgPnSgBYIXPQaePwSQPQWXHwf2SXSQLcCw/2DiWvVXUgdXPwBIbXXgXXfXEAQ3NQ03C3CAJADbW3SgXwo/F3N0N3C0D3CAL3KnX3UPKvKI/3cPSPWwKvWPUHBPWgJPfPTAAkNPFaKvbPYwWSPPLPPAQvOwYvb3eMcvSvTPavdGWvKnevRvXsZvQo0gNvdXaozvbvJQPvAfQqYfLQUfcfSfafWfefKvJfYQUQcQdfCYrfNQEvXQJXA/MwCwQwI6eAi/ORB0W/e/J/egNETAN/X4T/WzbYH/P/AAoAkA/UMAiA2IWw1AGAvAOA%2BAAQ%2BgpAiAJwWgwINMXg7ApXYgq/b4wE9IP4igqwd46g7ob4qgzoJgzAlgrg9grINAoYHghEvgiQAQoQ9gEbZXDgMnLXCQzgKQ%2BnII90eQ/AFUQyVnOYVQkfbnXnAYAXGwtXEABwok73FwqwNw1otQnQ/QfQww4w0wqQCwrcKw/EqApw6nTgekgY0QjgHwcQrkuUvkhk0gTvVIBwSQIAA%3D%3D%3D&quot;&gt;Godbolt link&lt;/a&gt;):&lt;/p&gt;
&lt;p&gt;&amp;lt;div id=&quot;issue-dump2&quot; style=&quot;display: flex; flex-direction: column; align-items: center; align-self: flex-start&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;script&amp;gt;
document.getElementById(&quot;issue-dump2&quot;).innerHTML = [110305].map(i =&amp;gt; `&amp;lt;div class=&quot;individual-issue&quot;&amp;gt;
&amp;lt;div&amp;gt;&amp;lt;svg id=&quot;issue-indicator-${i}&quot; class=&quot;issue-indicator issue-indicator-unknown&quot; viewBox=&quot;0 0 16 16&quot; version=&quot;1.1&quot; width=&quot;20&quot; height=&quot;20&quot; aria-hidden=&quot;true&quot;&amp;gt;&amp;lt;path stroke=&quot;none&quot; fill=&quot;#59636e&quot; d=&quot;M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Zm9.78-2.22-5.5 5.5a.749.749 0 0 1-1.275-.326.749.749 0 0 1 .215-.734l5.5-5.5a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042Z&quot;&amp;gt;&amp;lt;/path&amp;gt;&amp;lt;/svg&amp;gt;&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;div&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;p&amp;gt;&amp;lt;a href=&quot;https://github.com/llvm/llvm-project/issues/${i}&quot;&amp;gt;llvm/llvm-project#${i}&amp;lt;/a&amp;gt; gives us some dead data, which I manually removed&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;`).join(&quot;\n\n&quot;);
&amp;lt;/script&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.LCPI0_0:
        .zero   32,15
.LCPI0_3:
        ...
.LCPI0_4:
        ...
findCharsInSet:
        vpsrlw  ymm1, ymm0, 4
        vbroadcasti128  ymm3, xmmword ptr [rip + .LCPI0_3]
        vpand   ymm1, ymm1, ymmword ptr [rip + .LCPI0_0]
        vbroadcasti128  ymm2, xmmword ptr [rip + .LCPI0_4]
        vpshufb ymm1, ymm2, ymm1
        vpshufb ymm0, ymm3, ymm0
        vpand   ymm0, ymm0, ymm1
        vpcmpeqb        ymm0, ymm0, ymm1
        vpmovmskb       eax, ymm0
        vzeroupper
        ret
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;script is:inline&amp;gt;
{
const open = &apos;&amp;lt;path stroke=&quot;none&quot; fill=&quot;#1a7f37&quot; d=&quot;M8 9.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z&quot;&amp;gt;&amp;lt;/path&amp;gt;&amp;lt;path stroke=&quot;none&quot; fill=&quot;#1a7f37&quot; d=&quot;M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Z&quot;&amp;gt;&amp;lt;/path&amp;gt;&apos;;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const closed_completed = &apos;&amp;lt;path stroke=&quot;none&quot; fill=&quot;#8250df&quot; d=&quot;M11.28 6.78a.75.75 0 0 0-1.06-1.06L7.25 8.69 5.78 7.22a.75.75 0 0 0-1.06 1.06l2 2a.75.75 0 0 0 1.06 0l3.5-3.5Z&quot;&amp;gt;&amp;lt;/path&amp;gt;&amp;lt;path stroke=&quot;none&quot; fill=&quot;#8250df&quot; d=&quot;M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0Zm-1.5 0a6.5 6.5 0 1 0-13 0 6.5 6.5 0 0 0 13 0Z&quot;&amp;gt;&amp;lt;/path&amp;gt;&apos;;

const closed_not_planned = &apos;&amp;lt;path stroke=&quot;none&quot; fill=&quot;#59636e&quot; d=&quot;M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Zm9.78-2.22-5.5 5.5a.749.749 0 0 1-1.275-.326.749.749 0 0 1 .215-.734l5.5-5.5a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042Z&quot;&amp;gt;&amp;lt;/path&amp;gt;&apos;;
for (const issue_id of [110317, 110305]) {
    fetch(`https://api.github.com/repos/llvm/llvm-project/issues/${issue_id}`)
        .then(e =&amp;gt; e.json())
        .then(e =&amp;gt; {
            const svg = e.state === &quot;open&quot; ? open : e.state_reason === &quot;completed&quot; ? closed_completed : closed_not_planned;

            for (const e of document.getElementsByClassName(&quot;issue-indicator&quot;)) {
                if (e.id === `issue-indicator-${issue_id}`) {
                    e.classList.remove(&quot;issue-indicator-unknown&quot;);
                    e.innerHTML = svg;
                }
            }
        })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}
&amp;lt;/script&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;style&amp;gt;
.issue-indicator-unknown {
transform: &quot;rotate(45)&quot;;
}&lt;/p&gt;
&lt;p&gt;.individual-issue {
display: flex; flex-direction: row; align-items: center; align-self: flex-start;
}&lt;/p&gt;
&lt;p&gt;.individual-issue &amp;gt; div {
margin-right: 0.5em;
}&lt;/p&gt;
&lt;p&gt;.individual-issue &amp;gt; div + div {
height: 2.3em;
}&lt;/p&gt;
&lt;p&gt;.individual-issue &amp;gt; div + div &amp;gt; p {
margin-top: 0;
margin-bottom: 0;
line-height: 1.5;
white-space: nowrap;
}&lt;/p&gt;
&lt;p&gt;p + div#issue-dump {
margin-bottom: 0.5em;
margin-top: -1.5em;
}
&amp;lt;/style&amp;gt;&lt;/p&gt;
&lt;p&gt;Compiled for Zen 4, we get (&lt;a href=&quot;https://zig.godbolt.org/#z:OYLghAFBqd5QCxAYwPYBMCmBRdBLAF1QCcAaPECAMzwBtMA7AQwFtMQByARg9KtQYEAysib0QXACx8BBAKoBnTAAUAHpwAMvAFYgAzKVpMGoAF55gpJfWQE8Ayo3QBhVLQCuLBhI0AmUk4AMngMmAByngBGmMQSBgAOqAqE9gyuHl4%2B/onJdgLBoREs0bFcBtaYtqlCBEzEBOme3lx%2BVpg2eQw1dQQF4VExcVa19Y2ZLf4KI70h/cWDZQCUVqjuxMjsHGgMUwDUU%2Bi7AKR6ACK7AAJ4LIn1EEe%2BvgcPvosnAEJHGgCC23uR7jodgYxzOl2utwI90eAKBIRebz0nx%2BfwIuyYCiU9VB5wOADosADgHiMViCB8vt9KajdkI5ABxenYIQAFWwpwA%2BgA1bDODlCACSAC1sDj9gR0HjkixJQp3MBgJgplzKkRiIFGMACAgAGIkZzxdwQdwADlIu1htGBeOQhsRyN%2BAj2zgQ7gYAGsxRcVbYSBA6YzmWzOTy%2BYKRXiTjrzab7ZTKZhVJDdlQQTQGC4EHUFAKupgochXR6QJcfWqIHp/LtY4txZK2LU8bmoXi3clgKF0OaLgQAJ7xTC5/gQC4s/uYADyVAghbd7sWizxADdVSQ8fQGLWjgB2B27fe7GnJEz0DmFuoc1DxBRio4AVneHPvp1NO/euzAHAAfp/zZ/wLwH4cB8v5Afe7ygZ%2Bz6QcBeg6jBEAwYsMFvih26nDBpCfsc6EUj8B6HqgNx2GwuxLnUuy0KgqDuu48QcrUkTiKWq7EBAXAAGwxiaW5ghcCjxEYUIaHG%2BEHiEtCzCmJC7BAx7AKe57EJe15btuzjIDuziUdRtH0Yx9Dgcgxy%2BBxuwaKoOrPjhzgnOcXCgrZejaTOoLYCcoqSKJ3wEWgxHXJgZEUYkADuMQKJeVAcr4HJ0QxqAclwvgmiW4Gcc%2Bppim6WDppg6B4T54kMJJoTScQskPBxoXhZF0WxfpCVJWa5l4nianOAAVAoVEEOaeBafsPV4h1Yp4FQsl4I5uw8ZcGLGs1XC8U52mTe0SjmZZVlIvGYn7jSdEDspDB4JETFKhyGIcpEhAcjkYpLterpUJEEDVcQEWoFFMVxUQiXJeas4em5HmXAJQkQF53m%2BU6aIKL2xRuByLAYp6dm7CEBDhaqV29pjHJZrQK4KMa8SHRyx2nfQEWXddBC3Uk5oPQoT0vVRNE/UwZ0A0W85QwexD5msIJwwjtBIyjBU7qcO3fKm6OCFjtg43jBNExATAlsYuPjuakSaww2sDrW%2BINkwTaCBArY7BYnbdn2A5DqgI5jgOU7q7rC7Lqx66MGpDpfAAnKSMRQvi0qSmgboECyxDuEqI6JPErhR%2BrvHS3xc2juObtMB73Zg0wUKLQuOJoz246O872fTrnFqeyuvrED7m6Sz8gcCwQQuXLTzgYlCY2yX58QkYF%2BIsnUioEHiqgmhxeJUJghdrJgQj5gAEnNlrWra7jz4vncCwo5okkuqh3klkQhQuO0B4HFyZy7k413noOCYXEAibWYBgGj6smWZkQ3htw0AHNamAb6ByYKXMEf9Kp128lLGWcsmYswgAZdg6IDb20wH1DMeANgKH1obTAtYB4jmSKYJ%2BVdXbThCPgAhJcQb8TwJQt2WcaFoM5vQBctZ2FULofgpUtYwGXEfm7dBftKQETIVcNIRFh4BQgIw3cUiCIHnIuVQ%2B7grQlj4W7ARDCsoZkwLlfK21dpqP4OVD%2BrULi0yECwqhejaF4IYbWAA9NNdq/V1I4T3GogJNI6GJlGq4pU4F%2Bp3nTv4gJBEtFWgidZNGZDgmqGBmCCyJoRLmV2CI9BETjFpIeHeWSdjCAONYdOZxnCzol08TxZ8BVYk4WlsAwqASO5d3ieScx7SWky2keNchji2FiJcfQoRvFRTMMqdQqhEjlExLUZ04gIIUHuGenJCUUpriSgFgOd%2BMynFjIgAYyZ5oJG4ImQoPm%2B5EFtMGaUihxzq41O4VM2kDImSsnZNyXk/JhTYEjHBSRFiCIaN2PEHohDjgPiOaM15CzdieIDN84MfywyAuBVZKJ3pWL%2Bi%2BUGX5oYAURijNxNO1ZjGmKabEiSUkrEVVMlC%2BoR8WptRsl1HqfUBpvlUc0/c3VUBTxGmjdZmyw67LxImAgxAmC2DeTg9GuwRqoqJSGf54YgXks%2BYGH5GrMVkpBeaSVMppWqFlfK/uYS2WTVVYS/VGLSXarguaNVjqSVauxdfXpzT7l9LiYLVZ3dCC9ymK9aFtz%2BkPIPDSBsCAMA3jRlMWOtg/H8rUTKmIaYQQXBeLQQmLBp6zxPmfJKeJHobLxJEPE59fAvBHGWP0HFpDVh4t2JtbEW0Ut4Z2iA3a21RoIlm4Ncs82PALUuItM855MFPr4CtzMq2RAbXixuFYqyxg7fiysPaWLrt3YO2lASR05suPmwtxa56YiUHoRdT1q14iaquvtnEKXbvXW%2Bwdvb8VftjMe/pvSM0rOFiFQghZSknKRXysFB413lgHbGaBooyFDxHnWPE49iCTyvXvJeAtV4EA3sTLeIQbSGjwwfJUx851lt8JfEu8bE14nHb4Sd06S20brfe5dtakoNvQVcwRNzcm0HWhcIedAcDEGIH6F4zhlByF2CMSelF5XuhvHKUmJA0SMvFZEOtCJSAZoIvBv0h6kN2RBqh%2BR6Gx4T3zLhhe%2BGV7r03oCK0ZGd6UeXmy0trxaxMfQAoFjF6p24dowuytVBq0Ca4Uqs5ImRESfkVJ7AMm5OPAU0plT%2BY1PIA0/sA6OmypkWi5EXwd4OJGZM3B19XFB3IfRkMtDAUMNYZwzOnzBG3MkY89vCjzmqN%2BZvZgPQjH8wJuC6Fidl6uujbvdFx9z7HiKqE4Y5Lkn6Dpdk2xeTinlMObREYArmnivYj0%2BVlbrxjOwf3CIqzehplbek7trZkoqAsCnq1tgyhiAY2hL4XM5FJKHAnp4RgaJsGQuDocIgZWl3PVSio3Cd5nAMBeMfN81SJFSwXLdvpiCgPoQ4MsWgnA7y8G8BwLQpBUCcCFBYfYqx1iBQeHoPQvACCaFJ8sBAi8sCxCUaQd0IAOJ3skFVgOXBtwaA0CaO8jxfBcEMJwSQvAWA%2BA0KQantP6ccF4IQ7X3Oaek9IHAWAMBEAoFS/QMgFAZy28GKYWVc4%2BBAnCpQPWpvSDXWYMQXsnAeC%2B5CHUXsE5IjaFVEH3gfk2CCAnMVQPPvCTyl7gWwh3BeBYGRieTYtP8ACyqETHnARVCVHcJjGP5AFbk595JSIcqA%2BuCwKX2V1xq8rmIJEJImBTiYFzwpEIoBTfLCoEYYACguR4EwCFCcA5qfB/4IIEQYh2BSBkIIRQKh1A%2B90P4IwJgQDmEsA3whkBlhXk6JngAtBOXYAAldoi8lA6j7rsG/uW0Qzo5C2m/JV3A0kb8WAd47JTAGAu9Oc6cu9/ssBz9hcKgqgHAIAnAxhmgAgMw%2BgigSgJBSAcgUgBA0DcD8DOgsCBhSg2gOhqhpgiCVdEDOhuh6gyD5gKCpgehaDhgehmCcDFoVg1gNh9AycKcqdS99ddgT9lNY4gYIBcBCAZJ2dxsuced8d%2BcmBBdKBlhRc9A547xJA9B5dfAA5pdJBtxTCA4uI691dSBNc7xtdddeB9dDcQBjdlDzcrcIAkAXt7dKAvDj9XcPR3crRPcIBvdac/cw9q9wiA8I8o9bBq849IdE9aBk8C9MAiR08xNq8c9jAFJ89s88Ai87AS8fdEwK8q8s8a9MY69acG8m9ewW88jSB29NcKiu8e8lB%2B9B8SoR8tAx8J8p8Z858F9q9l9hBRBxAN9Rjt81BS9dAVdD8zALBDATp4DL8FEnROA79H9n8MRMA389hP8jtdgf8/8ACgCQDDQwCICYgrDUAYC8A4D4Blh6DUhHAMwOCghZhsCFg8CkgCC5EMh0CSDUhuCfiXiBBGCGg3AmhcDwSuhphQTWCaDoTxhOCmCvjyCJBniWcBDxtVcOBKcddRDOBxCmd/DPQZD8A1QTIOdFglDR8%2BcBdBhhdLCNcQBbCiSfdHCrBnCmjlDND9AdC9CDCjCpBTDtxzD8SoD7C6dOB6TeihCOBfARCuS5S%2BSGTSAu9kgHBJAgA&quot;&gt;Godbolt link&lt;/a&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.LCPI0_3:
        ...
.LCPI0_4:
        ...
.LCPI0_5:
        ...
findCharsInSet:
        vgf2p8affineqb  ymm1, ymm0, qword ptr [rip + .LCPI0_3]{1to4}, 0
        vbroadcasti128  ymm2, xmmword ptr [rip + .LCPI0_4]
        vbroadcasti128  ymm3, xmmword ptr [rip + .LCPI0_5]
        vpshufb ymm0, ymm3, ymm0
        vpshufb ymm1, ymm2, ymm1
        vptestmb        k0, ymm0, ymm1
        kmovd   eax, k0
        vzeroupper
        ret
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The advantage of this strategy over the one used by Langdale and Lemire (&lt;a href=&quot;https://arxiv.org/pdf/1902.08318&quot;&gt;2019&lt;/a&gt;) for &lt;a href=&quot;https://github.com/simdjson/simdjson&quot;&gt;simdjson&lt;/a&gt; is that we can reuse the vector containing the upper nibbles as a bit position (&lt;code&gt;1 &amp;lt;&amp;lt; (c &amp;gt;&amp;gt; 4)&lt;/code&gt;) if we want to do more &lt;em&gt;Vectorized Classification&lt;/em&gt;. That means we can add more &lt;em&gt;Vectorized Classification&lt;/em&gt; routines and only have to pay the cost for the lower nibbles, avoiding the need for an additional &lt;code&gt;vbroadcasti128+vpshufb&lt;/code&gt; pair for the upper nibbles. To add another table, the additional overhead for Zen 3 is just &lt;code&gt;vbroadcasti128+vpshufb+vpand+vpcmpeqb+vpmovmskb&lt;/code&gt;. For Zen 4, it&apos;s just &lt;code&gt;vbroadcasti128+vpshufb+vptestmb+kmovd&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Dare I say this is...&lt;/p&gt;
&lt;p&gt;&amp;lt;div alt=&quot;Blazingly Fast&quot; class=&quot;blazingly-fast&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;‒ Validark&lt;/p&gt;
&lt;p&gt;:::note[Note from &lt;a href=&quot;https://www.linkedin.com/feed/update/urn:li:activity:7246181875826733058?commentUrn=urn%3Ali%3Acomment%3A%28activity%3A7246181875826733058%2C7246455526627147776%29&amp;amp;dashCommentUrn=urn%3Ali%3Afsd_comment%3A%287246455526627147776%2Curn%3Ali%3Aactivity%3A7246181875826733058%29&quot;&gt;Geoff Langdale&lt;/a&gt;:]
&amp;lt;span style=&quot;font-size: smaller; line-height: 0&quot;&amp;gt;The &lt;a href=&quot;https://github.com/simdjson/simdjson&quot;&gt;simdjson&lt;/a&gt; PSHUFB lookup is essentially borrowed from &lt;a href=&quot;https://github.com/intel/hyperscan/&quot;&gt;Hyperscan&lt;/a&gt;&apos;s own &lt;a href=&quot;https://github.com/intel/hyperscan/blob/master/src/nfa/shufti.c&quot;&gt;shufti&lt;/a&gt;/Teddy (&lt;a href=&quot;https://github.com/intel/hyperscan/blob/master/src/nfa/shufti.c&quot;&gt;shufti&lt;/a&gt; is a acceleration technique for NFA/DFA execution, while Teddy is a full-on string matcher, but both use similar techniques). The code in question is in https://github.com/intel/hyperscan/blob/master/src/nfa/shufti.c and https://github.com/intel/hyperscan/blob/master/src/nfa/truffle.c albeit kind of difficult to read (since there&apos;s a lot of extra magic for all the various platforms etc). Shufti is a 2-PSHUFB thing that is used &quot;usually&quot;, truffle is &quot;this will always work&quot; and uses a technique kind of similar to yours (albeit for different reasons).&amp;lt;/span&amp;gt;
:::&lt;/p&gt;
</content:encoded></item><item><title>Eliminating Shifted Geometric Recursion</title><link>https://validark.dev/posts/eliminating-shifted-geometric-recursion/</link><guid isPermaLink="true">https://validark.dev/posts/eliminating-shifted-geometric-recursion/</guid><description>How to eliminate a shifted geometric math loop using an approximation method</description><pubDate>Sat, 21 Sep 2024 09:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A while back, I noticed &lt;a href=&quot;https://github.com/ziglang/zig/blob/91b4729962ddec96d1ee60d742326da828dae94a/lib/std/array_list.zig#L374-L377&quot;&gt;this code&lt;/a&gt; in Zig&apos;s &lt;a href=&quot;https://ziglang.org/documentation/master/std/#std.array_list.ArrayListAlignedUnmanaged.ensureTotalCapacity&quot;&gt;ArrayList.ensureTotalCapacity&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var better_capacity = self.capacity;
while (true) {
    better_capacity +|= better_capacity / 2 + 8;
    if (better_capacity &amp;gt;= new_capacity) break;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In assembly:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ensureTotalCapacity:
        mov     rcx, -1
        mov     rax, rdi
.LBB0_1:
        mov     rdx, rax
        shr     rdx
        add     rdx, 8
        add     rax, rdx
        cmovb   rax, rcx
        cmp     rax, rsi
        jb      .LBB0_1
        ret
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Just for fun, in this article I will investigate replacing this with a branchless approximation.&lt;/p&gt;
&lt;p&gt;First, I will temporarily disregard the fact that we are dealing with 64 bit integers, disregard the fact that doing an integer division by 2 can floor the true quotient by 0.5 when the dividend is odd, and disregard the saturating arithmetic and just write this in terms of a recursive sequence.&lt;/p&gt;
&lt;p&gt;&amp;lt;svg version=&quot;1.1&quot; width=&quot;100%&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 10 940 100&quot;&amp;gt;
&amp;lt;foreignObject width=&quot;100%&quot; height=&quot;100%&quot;&amp;gt;
&amp;lt;div xmlns=&quot;http://www.w3.org/1999/xhtml&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;$$
\LARGE
\begin{equation}
\begin{split}
U_0 &amp;amp;= \texttt{capacity}\
U_n &amp;amp;= U_{n-1} \times 1.5  + 8\
\end{split}
\end{equation}
$$
&amp;lt;/div&amp;gt;
&amp;lt;/foreignObject&amp;gt;
&amp;lt;/svg&amp;gt;&lt;/p&gt;
&lt;p&gt;If you remember your pre-calculus class, this recursive sequence is called &quot;shifted geometric&quot;, because it has a multiply that is being shifted by an addition. For &amp;lt;span style=&quot;white-space: nowrap&quot;&amp;gt;$\footnotesize U_0 = c$,&amp;lt;/span&amp;gt; the expansion of this recursive sequence looks like:&lt;/p&gt;
&lt;p&gt;&amp;lt;svg version=&quot;1.1&quot; width=&quot;100%&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 10 940 250&quot;&amp;gt;
&amp;lt;foreignObject width=&quot;100%&quot; height=&quot;100%&quot;&amp;gt;
&amp;lt;div xmlns=&quot;http://www.w3.org/1999/xhtml&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;$$
\LARGE
\begin{equation}
\begin{split}
U_1 = c \times 1.5 + 8\
U_2 = (c \times 1.5 + 8) \times 1.5 + 8\
U_3 = ((c \times 1.5 + 8) \times 1.5 + 8) \times 1.5 + 8\
U_4 = (((c \times 1.5 + 8) \times 1.5 + 8) \times 1.5 + 8) \times 1.5 + 8\
U_5 = ((((c \times 1.5 + 8) \times 1.5 + 8) \times 1.5 + 8) \times 1.5 + 8) \times 1.5 + 8\
\end{split}
\end{equation}
$$
&amp;lt;/div&amp;gt;
&amp;lt;/foreignObject&amp;gt;
&amp;lt;/svg&amp;gt;&lt;/p&gt;
&lt;p&gt;To get the general equation, let&apos;s replace $\footnotesize 1.5$ with $\footnotesize r$ and $\footnotesize 8$ with &amp;lt;span style=&quot;white-space: nowrap&quot;&amp;gt;$\footnotesize d$:&amp;lt;/span&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;svg version=&quot;1.1&quot; width=&quot;100%&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 10 940 310&quot;&amp;gt;
&amp;lt;foreignObject width=&quot;100%&quot; height=&quot;100%&quot;&amp;gt;
&amp;lt;div xmlns=&quot;http://www.w3.org/1999/xhtml&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;$$
\LARGE
\begin{equation}
\begin{split}
U_0 = c \
U_1 = c \times r + d\
U_2 = (c \times r + d) \times r + d\
U_3 = ((c \times r + d) \times r + d) \times r + d\
U_4 = (((c \times r + d) \times r + d) \times r + d) \times r + d\
U_5 = ((((c \times r + d) \times r + d) \times r + d) \times r + d) \times r + d\
\end{split}
\end{equation}
$$
&amp;lt;/div&amp;gt;
&amp;lt;/foreignObject&amp;gt;
&amp;lt;/svg&amp;gt;&lt;/p&gt;
&lt;p&gt;Let&apos;s apply the distributive property of multiplication:&lt;/p&gt;
&lt;p&gt;&amp;lt;svg version=&quot;1.1&quot; width=&quot;100%&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 10 940 260&quot;&amp;gt;
&amp;lt;foreignObject width=&quot;100%&quot; height=&quot;100%&quot;&amp;gt;
&amp;lt;div xmlns=&quot;http://www.w3.org/1999/xhtml&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;$$
\LARGE \begin{equation}
\begin{split}
U_1 =cr^1    + dr^0\
U_2 = cr^2 + dr^1                                        + dr^0\
U_3 = cr^3 + dr^2 + dr^1                           + dr^0\
U_4 = cr^4 + dr^3 + dr^2 + dr^1              + dr^0\
U_5 = cr^5 + dr^4 + dr^3 + dr^2 + dr^1 + dr^0\
\end{split}
\end{equation}
$$
&amp;lt;/div&amp;gt;
&amp;lt;/foreignObject&amp;gt;
&amp;lt;/svg&amp;gt;&lt;/p&gt;
&lt;p&gt;The pattern here is pretty obvious. We can express it using $\footnotesize \Sigma$ notation:&lt;/p&gt;
&lt;p&gt;&amp;lt;svg version=&quot;1.1&quot; width=&quot;100%&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 10 940 115&quot;&amp;gt;
&amp;lt;foreignObject width=&quot;100%&quot; height=&quot;100%&quot;&amp;gt;
&amp;lt;div xmlns=&quot;http://www.w3.org/1999/xhtml&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;$$
\LARGE U_n = cr^n + \sum_{i=1}^{n} dr^{i-1}
$$
&amp;lt;/div&amp;gt;
&amp;lt;/foreignObject&amp;gt;
&amp;lt;/svg&amp;gt;&lt;/p&gt;
&lt;p&gt;You may notice that the $\footnotesize \Sigma$ term is the &quot;sum of a finite geometric sequence&quot;. Replacing that term with the well-known formula for that allows us to write an explicit function:&lt;/p&gt;
&lt;p&gt;&amp;lt;svg version=&quot;1.1&quot; width=&quot;100%&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 5 940 100&quot;&amp;gt;
&amp;lt;foreignObject width=&quot;100%&quot; height=&quot;100%&quot;&amp;gt;
&amp;lt;div xmlns=&quot;http://www.w3.org/1999/xhtml&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;$$
\LARGE f(n) = cr^n + d \left(\frac{1 - r^n}{1 - r}\right)
$$
&amp;lt;/div&amp;gt;
&amp;lt;/foreignObject&amp;gt;
&amp;lt;/svg&amp;gt;&lt;/p&gt;
&lt;p&gt;Let&apos;s put $\footnotesize 1.5$ back in for $\footnotesize r$ and $\footnotesize 8$ back in for $\footnotesize d$ and assess the damage:&lt;/p&gt;
&lt;p&gt;&amp;lt;svg version=&quot;1.1&quot; width=&quot;100%&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 5 940 100&quot;&amp;gt;
&amp;lt;foreignObject width=&quot;100%&quot; height=&quot;100%&quot;&amp;gt;
&amp;lt;div xmlns=&quot;http://www.w3.org/1999/xhtml&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;$$
\LARGE f(n) = c \times 1.5^n + 8 \left(\frac{1 - 1.5^n}{1 - 1.5}\right)
$$
&amp;lt;/div&amp;gt;
&amp;lt;/foreignObject&amp;gt;
&amp;lt;/svg&amp;gt;&lt;/p&gt;
&lt;p&gt;Luckily, we can simplify $\footnotesize (1 - 1.5)$ to &amp;lt;span style=&quot;white-space: nowrap&quot;&amp;gt;$\footnotesize -0.5$.&amp;lt;/span&amp;gt; Dividing by $\footnotesize -0.5$ is equivalent to multiplying by &amp;lt;span style=&quot;white-space: nowrap&quot;&amp;gt;$\footnotesize -2$,&amp;lt;/span&amp;gt; which we can combine with the $\footnotesize 8$ term to get &amp;lt;span style=&quot;white-space: nowrap&quot;&amp;gt;$\footnotesize -16$:&amp;lt;/span&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;svg version=&quot;1.1&quot; width=&quot;100%&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 10 940 55&quot;&amp;gt;
&amp;lt;foreignObject width=&quot;100%&quot; height=&quot;100%&quot;&amp;gt;
&amp;lt;div xmlns=&quot;http://www.w3.org/1999/xhtml&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;$$
\LARGE f(n) = c \times 1.5^n + -16 (1 - 1.5^n)
$$
&amp;lt;/div&amp;gt;
&amp;lt;/foreignObject&amp;gt;
&amp;lt;/svg&amp;gt;&lt;/p&gt;
&lt;p&gt;We could stop here, but let&apos;s distribute the &amp;lt;span style=&quot;white-space: nowrap&quot;&amp;gt;$\footnotesize -16$:&amp;lt;/span&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;svg version=&quot;1.1&quot; width=&quot;100%&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 10 940 55&quot;&amp;gt;
&amp;lt;foreignObject width=&quot;100%&quot; height=&quot;100%&quot;&amp;gt;
&amp;lt;div xmlns=&quot;http://www.w3.org/1999/xhtml&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;$$
\LARGE f(n) = c \times 1.5^n  - 16 + 16 \times 1.5^n
$$
&amp;lt;/div&amp;gt;
&amp;lt;/foreignObject&amp;gt;
&amp;lt;/svg&amp;gt;&lt;/p&gt;
&lt;p&gt;Since we have two terms being added which each are multiplied by &amp;lt;span style=&quot;white-space: nowrap&quot;&amp;gt;$\footnotesize 1.5^n$,&amp;lt;/span&amp;gt; we can factor it out like so:&lt;/p&gt;
&lt;p&gt;&amp;lt;svg version=&quot;1.1&quot; width=&quot;100%&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 10 940 55&quot;&amp;gt;
&amp;lt;foreignObject width=&quot;100%&quot; height=&quot;100%&quot;&amp;gt;
&amp;lt;div xmlns=&quot;http://www.w3.org/1999/xhtml&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;$$
\LARGE f(n) = (c+16) \times 1.5^n - 16
$$
&amp;lt;/div&amp;gt;
&amp;lt;/foreignObject&amp;gt;
&amp;lt;/svg&amp;gt;&lt;/p&gt;
&lt;p&gt;This looks how we probably expected it would, and it is relatively easy to deal with. Now let&apos;s try to apply this to our original problem. The first thing we want to do, is find an $\footnotesize n$ for which &amp;lt;span style=&quot;white-space: nowrap&quot;&amp;gt;$\footnotesize x \ge f(n)$,&amp;lt;/span&amp;gt; where $\footnotesize x$ is the requested &lt;code&gt;new_capacity&lt;/code&gt;. To find &amp;lt;span style=&quot;white-space: nowrap&quot;&amp;gt;$\footnotesize n$,&amp;lt;/span&amp;gt; we have to isolate it on the right-hand side:&lt;/p&gt;
&lt;p&gt;&amp;lt;svg version=&quot;1.1&quot; width=&quot;100%&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 10 940 830&quot;&amp;gt;
&amp;lt;foreignObject width=&quot;100%&quot; height=&quot;100%&quot;&amp;gt;
&amp;lt;div xmlns=&quot;http://www.w3.org/1999/xhtml&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;$$
\LARGE \begin{equation}
\begin{split}
x &amp;amp;\ge (c+16) \times 1.5^n - 16          \
\small \texttt{(+16 to both sides)}       \
x + 16 &amp;amp;\ge (c+16) \times 1.5^n          \
\small \texttt{(divide by (c+16) on both sides)}   \
\frac{x + 16}{c+16} &amp;amp;\ge 1.5^n            \
\small \texttt{(take the log of both sides)}   \
\log{\left(\frac{x + 16}{c+16}\right)} &amp;amp;\ge \log{(1.5^n)}            \
\small \texttt{(property of logarithms on the right-hand side)}   \
\log{\left(\frac{x + 16}{c+16}\right)} &amp;amp;\ge n\log{(1.5)}            \
\small \texttt{(divide each side by log(1.5))}   \
\frac{ \log{\left(\frac{x + 16}{c+16}\right)}}{\log{(1.5)}} &amp;amp;\ge n            \
\small \texttt{(property of logarithms on the left-hand side)}   \
\log_{1.5}{\left(\frac{x + 16}{c+16}\right)} &amp;amp;\ge n            \
\small \texttt{(property of logarithms on the left-hand side)}   \
\log_{1.5}{(x + 16)} - \log_{1.5}{(c + 16)} &amp;amp;\ge n            \
\end{split}
\end{equation}
$$
&amp;lt;/div&amp;gt;
&amp;lt;/foreignObject&amp;gt;
&amp;lt;/svg&amp;gt;&lt;/p&gt;
&lt;p&gt;Now this is usable for our problem. We can compute $\footnotesize n$ by doing &amp;lt;span style=&quot;white-space: nowrap&quot;&amp;gt;$\footnotesize \lceil\log_{1.5}{(x + 16)} - \log_{1.5}{(c + 16)}\rceil$,&amp;lt;/span&amp;gt; then plug that in to $\footnotesize n$ in &amp;lt;span style=&quot;white-space: nowrap&quot;&amp;gt;$\footnotesize f(n) = (c+16) \times 1.5^n - 16$.&amp;lt;/span&amp;gt; Together, that&apos;s:&lt;/p&gt;
&lt;p&gt;&amp;lt;svg version=&quot;1.1&quot; width=&quot;100%&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 10 940 60&quot;&amp;gt;
&amp;lt;foreignObject width=&quot;100%&quot; height=&quot;100%&quot;&amp;gt;
&amp;lt;div xmlns=&quot;http://www.w3.org/1999/xhtml&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;$$
\LARGE (c+16) \times 1.5^{\lceil(\log_{1.5}{(x + 16)} - \log_{1.5}{(c + 16)})\rceil} - 16
$$
&amp;lt;/div&amp;gt;
&amp;lt;/foreignObject&amp;gt;
&amp;lt;/svg&amp;gt;&lt;/p&gt;
&lt;p&gt;For those of you who skipped ahead, $\footnotesize c$ is &lt;code&gt;self.capacity&lt;/code&gt; and $\footnotesize x$ is &lt;code&gt;new_capacity&lt;/code&gt;, and this formula gives you the &lt;code&gt;better_capacity&lt;/code&gt;. Note that this formula will give numbers a bit higher than the original while loop, because the original while loop loses some 0.5&apos;s when dividing an odd number by 2.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;Now, the remaining question is how to compute the previous expression, or rather, an approximation of it, efficiently.&lt;/p&gt;
&lt;p&gt;Sadly, efficiently computing the base $\footnotesize 1.5$ logarithm of an integer is not ideal. If we were allowed to change the original problem such that we could use the base $\footnotesize 2$ logarithm, that would be much easier to compute, that&apos;s just &lt;code&gt;@typeInfo(@TypeOf(c)).int.bits - 1 - @clz(c)&lt;/code&gt; (obviously, this would be an integer, so we should be careful on how the flooring of the true answer affects rounding error). Let&apos;s use this information to make an approximation. Using the change of base property of logarithms, we can rewrite the equation like so:&lt;/p&gt;
&lt;p&gt;&amp;lt;svg version=&quot;1.1&quot; width=&quot;100%&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 10 940 95&quot;&amp;gt;
&amp;lt;foreignObject width=&quot;100%&quot; height=&quot;100%&quot;&amp;gt;
&amp;lt;div xmlns=&quot;http://www.w3.org/1999/xhtml&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;$$
\LARGE \frac{\log_2{(x + 16)}}{\log_2{1.5}} - \frac{\log_2{(c + 16)}}{\log_2{1.5}}
$$
&amp;lt;/div&amp;gt;
&amp;lt;/foreignObject&amp;gt;
&amp;lt;/svg&amp;gt;&lt;/p&gt;
&lt;p&gt;Equivalently:&lt;/p&gt;
&lt;p&gt;&amp;lt;svg version=&quot;1.1&quot; width=&quot;100%&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 10 940 95&quot;&amp;gt;
&amp;lt;foreignObject width=&quot;100%&quot; height=&quot;100%&quot;&amp;gt;
&amp;lt;div xmlns=&quot;http://www.w3.org/1999/xhtml&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;$$
\LARGE (\log_2{(x + 16)} - \log_2{(c + 16)}) \times \frac{1}{\log_2{1.5}}
$$
&amp;lt;/div&amp;gt;
&amp;lt;/foreignObject&amp;gt;
&amp;lt;/svg&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;span style=&quot;white-space: nowrap&quot;&amp;gt;$\footnotesize \frac{1}{\log_2{1.5}} \approx 1.7095112913514547$,&amp;lt;/span&amp;gt; so we can approximate the above expression like so:&lt;/p&gt;
&lt;p&gt;&amp;lt;svg version=&quot;1.1&quot; width=&quot;100%&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 10 940 55&quot;&amp;gt;
&amp;lt;foreignObject width=&quot;100%&quot; height=&quot;100%&quot;&amp;gt;
&amp;lt;div xmlns=&quot;http://www.w3.org/1999/xhtml&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;$$
\LARGE (\log_2{(x + 16)} - \log_2{(c + 16)}) \times 1.7095112913514547
$$
&amp;lt;/div&amp;gt;
&amp;lt;/foreignObject&amp;gt;
&amp;lt;/svg&amp;gt;&lt;/p&gt;
&lt;p&gt;As hinted to earlier, we can find $\footnotesize \lceil\log_2{(x + 16)}\rceil - \lceil\log_2{(c + 16)}\rceil$ by doing &lt;code&gt;@clz(c + 15) - @clz(x + 15)&lt;/code&gt;. Note that the terms are now in reverse order because the answer returned by &lt;code&gt;@clz(b)&lt;/code&gt; is actually &amp;lt;span style=&quot;white-space: nowrap&quot;&amp;gt;$\footnotesize 63 - \lfloor\log_2{b}\rfloor$.&amp;lt;/span&amp;gt; We also subtracted $\footnotesize 1$ from $\footnotesize 16$ because we probably want the ceil base $\footnotesize 2$ logarithm instead, and the algorithm for that is &lt;code&gt;64 - @clz(x - 1)&lt;/code&gt;. &lt;code&gt;(64 - @clz((x + 16) - 1)) - (64 - @clz((c + 16) - 1))&lt;/code&gt; reduces to &lt;code&gt;@clz(c + 15) - @clz(x + 15)&lt;/code&gt;. That&apos;s slightly different than what we want, which is to ceil only after multiplying by &amp;lt;span style=&quot;white-space: nowrap&quot;&amp;gt;$\footnotesize 1.7095112913514547$,&amp;lt;/span&amp;gt; but if we&apos;re careful about which way the rounding works, we should be fine.&lt;/p&gt;
&lt;p&gt;We can change $\footnotesize 1.7095112913514547$ to a nicer number like $\footnotesize 2$ by working backwards. To make it so we would multiply by $\footnotesize 2$ instead, we would change our recursive sequence to:&lt;/p&gt;
&lt;p&gt;&amp;lt;svg version=&quot;1.1&quot; width=&quot;100%&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 10 940 110&quot;&amp;gt;
&amp;lt;foreignObject width=&quot;100%&quot; height=&quot;100%&quot;&amp;gt;
&amp;lt;div xmlns=&quot;http://www.w3.org/1999/xhtml&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;$$
\LARGE \begin{equation}
\begin{split}
U_0 = \texttt{capacity}\
U_n = U_{n-1} \times \sqrt 2  + 8\
\end{split}
\end{equation}
$$
&amp;lt;/div&amp;gt;
&amp;lt;/foreignObject&amp;gt;
&amp;lt;/svg&amp;gt;&lt;/p&gt;
&lt;p&gt;This works because $\footnotesize \frac{1}{\log_2{\sqrt 2}}$ is &amp;lt;span style=&quot;white-space: nowrap&quot;&amp;gt;$\footnotesize 2$.&amp;lt;/span&amp;gt; This is still pretty close to our original formula, as $\footnotesize \sqrt 2 \approx 1.41421$ and &amp;lt;span style=&quot;white-space: nowrap&quot;&amp;gt;$\footnotesize 1.41421 \approx 1.5$.&amp;lt;/span&amp;gt; If we did the same steps as before, $\footnotesize \frac{8}{1 - \sqrt 2} \approx 19.313708498984756$ would be in all the places where we had $\footnotesize 16$ in our original equations. Let&apos;s round that up to $\footnotesize 20$ this time, since we rounded $\footnotesize 1.5$ down to &amp;lt;span style=&quot;white-space: nowrap&quot;&amp;gt;$\footnotesize \sqrt 2$.&amp;lt;/span&amp;gt; To do that, we change the common difference of $\footnotesize 8$ to &amp;lt;span style=&quot;white-space: nowrap&quot;&amp;gt;$\footnotesize -20 (1 - \sqrt 2)$,&amp;lt;/span&amp;gt; which is about &amp;lt;span style=&quot;white-space: nowrap&quot;&amp;gt;$\footnotesize 8.2842712474619$.&amp;lt;/span&amp;gt; Reminder: the point here is that when we divide this value by &amp;lt;span style=&quot;white-space: nowrap&quot;&amp;gt;$\footnotesize (1 - \sqrt 2)$,&amp;lt;/span&amp;gt; we get $\footnotesize -20$ rather than the $\footnotesize -16$ we had earlier.&lt;/p&gt;
&lt;p&gt;&amp;lt;svg version=&quot;1.1&quot; width=&quot;100%&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 10 940 160&quot;&amp;gt;
&amp;lt;foreignObject width=&quot;100%&quot; height=&quot;100%&quot;&amp;gt;
&amp;lt;div xmlns=&quot;http://www.w3.org/1999/xhtml&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;$$
\LARGE \begin{equation}
\begin{split}
U_0 &amp;amp;= \texttt{capacity}\
U_n &amp;amp;= U_{n-1} \times \sqrt 2 - 20 (1 - \sqrt 2)\
U_n &amp;amp;\approx U_{n-1} \times 1.41421 + 8.2842712474619\
\end{split}
\end{equation}
$$
&amp;lt;/div&amp;gt;
&amp;lt;/foreignObject&amp;gt;
&amp;lt;/svg&amp;gt;&lt;/p&gt;
&lt;p&gt;By the same steps shown above, this gives us the coveted:&lt;/p&gt;
&lt;p&gt;&amp;lt;svg version=&quot;1.1&quot; width=&quot;100%&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 10 940 65&quot;&amp;gt;
&amp;lt;foreignObject width=&quot;100%&quot; height=&quot;100%&quot;&amp;gt;
&amp;lt;div xmlns=&quot;http://www.w3.org/1999/xhtml&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;$$
\LARGE (c+20) \times \sqrt 2^{\lceil 2(\log_2{(x + 20)} - \log_2{(c + 20)})\rceil} - 20
$$
&amp;lt;/div&amp;gt;
&amp;lt;/foreignObject&amp;gt;
&amp;lt;/svg&amp;gt;&lt;/p&gt;
&lt;p&gt;I.e.:&lt;/p&gt;
&lt;p&gt;&amp;lt;svg version=&quot;1.1&quot; width=&quot;100%&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 10 940 65&quot;&amp;gt;
&amp;lt;foreignObject width=&quot;100%&quot; height=&quot;100%&quot;&amp;gt;
&amp;lt;div xmlns=&quot;http://www.w3.org/1999/xhtml&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;$$
\LARGE (c+20) \times \sqrt 2^{\lceil \log_{\sqrt 2}{(x + 20)} - \log_{\sqrt 2}{(c + 20)}\rceil} - 20
$$
&amp;lt;/div&amp;gt;
&amp;lt;/foreignObject&amp;gt;
&amp;lt;/svg&amp;gt;&lt;/p&gt;
&lt;p&gt;As mentioned before, we can find $\footnotesize \lceil\log_2{(x + 20)}\rceil - \lceil\log_2{(c + 20)}\rceil$ by doing &lt;code&gt;@clz(c + 19) - @clz(x + 19)&lt;/code&gt;. However, this is not close enough to $\footnotesize \lceil \log_{\sqrt 2}{(x + 20)} - \log_{\sqrt 2}{(c + 20)}\rceil$ for our use-case because we need at least the granularity of a $\footnotesize \log_{\sqrt 2}{}$ either way (ideally, we could use even more precision in some cases). This could be accomplished via a lookup table, or via another approximation. As an approximation, we could pretend that each odd power of $\footnotesize \sqrt 2$ is half-way between powers of $\footnotesize 2$ that fall on even powers of &amp;lt;span style=&quot;white-space: nowrap&quot;&amp;gt;$\footnotesize \sqrt 2$.&amp;lt;/span&amp;gt; If you think about it, this is kind of semantically in line with what we are doing when we subtract the &lt;code&gt;@clz&lt;/code&gt; of two numbers, now with slightly more granularity. Here is how we could accomplish that:&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- By AND&apos;ing the bit directly under the most significant bit with the most significant bit, then moving it to the 1&apos;s place, we can add it with double the bit index of the highest set bit: --&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Basically @clz but with double the normal granularity
fn log_sqrt_2_int(x: u64) u64 {
    assert(x != 0);
    const fls = 63 - @as(u64, @clz(x));
    const is_bit_under_most_significant_bit_set = (x &amp;amp; (x &amp;lt;&amp;lt; 1)) &amp;gt;&amp;gt; @intCast(fls);
    return (fls * 2) | is_bit_under_most_significant_bit_set;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is kind of what we are looking for, with a bit more accuracy than before. We can also scale this up even more if desired:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Kinda an approximation of 16 log2(x). Will be divided by 8 to approximate 2 log2(x).
export fn log_approx_helper(y: usize) usize {
    const COMPLEMENT = @typeInfo(usize).int.bits - 1;
    const BITS_TO_PRESERVE = @as(comptime_int, COMPLEMENT - @clz(@as(usize, 20)));

    const x = y +| 20;
    const fls: std.math.Log2Int(usize) = @intCast(COMPLEMENT - @clz(x)); // [4, 63]

    const pack_bits_under_old_msb = switch (builtin.cpu.arch) {
        // the `btc` instruction saves us a cycle on x86
        .x86, .x86_64 =&amp;gt; (x ^ (@as(usize, 1) &amp;lt;&amp;lt; fls)) &amp;gt;&amp;gt; (fls - BITS_TO_PRESERVE),
        else =&amp;gt; @as(std.meta.Int(.unsigned, BITS_TO_PRESERVE), @truncate((x &amp;gt;&amp;gt; (fls - BITS_TO_PRESERVE)))),
    };
    return (@as(usize, fls) &amp;lt;&amp;lt; BITS_TO_PRESERVE) | pack_bits_under_old_msb; // [16, 1023] on 64-bit
}

// usage:
const n = 1 + (log_approx_helper(x + 19) - log_approx_helper(c + 19)) / 8;
// i.e.:
const n = 1 + (log_approx_helper(new_capacity + 19) - log_approx_helper(self.capacity + 19)) / 8;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now that we have calculated &amp;lt;span style=&quot;white-space: nowrap&quot;&amp;gt;$\footnotesize n$,&amp;lt;/span&amp;gt; the last problem is approximating &amp;lt;span style=&quot;white-space: nowrap&quot;&amp;gt;$\footnotesize \sqrt 2^n$.&amp;lt;/span&amp;gt; Again, this can be done with a lookup table, or we could pretend once more that odd powers of $\footnotesize \sqrt 2$ are directly in the middle of powers of &amp;lt;span style=&quot;white-space: nowrap&quot;&amp;gt;$\footnotesize 2$.&amp;lt;/span&amp;gt; Let&apos;s try that.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fn approx_sqrt_2_pow(y: u7) u64 {
    // y is basically a fixed point integer, with the 1&apos;s place being after the decimal point
    const shift = @intCast(u6, y &amp;gt;&amp;gt; 1);
    return (@as(u64, 1) &amp;lt;&amp;lt; shift) | (@as(u64, y &amp;amp; 1) &amp;lt;&amp;lt; (shift -| 1));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And here are the estimates versus what we would get from &lt;code&gt;std.math.pow(f64, std.math.sqrt2, n)&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pow: est vs double  &amp;lt;- format
√2^1: 1 vs 1.4142135623730951
√2^3: 3 vs 2.8284271247461907
√2^5: 6 vs 5.656854249492383
√2^7: 12 vs 11.313708498984768
√2^9: 24 vs 22.627416997969544
√2^11: 48 vs 45.254833995939094
√2^13: 96 vs 90.50966799187822
√2^15: 192 vs 181.01933598375646
√2^17: 384 vs 362.038671967513
√2^19: 768 vs 724.0773439350261
√2^21: 1536 vs 1448.1546878700526
√2^23: 3072 vs 2896.3093757401057
√2^25: 6144 vs 5792.618751480213
√2^27: 12288 vs 11585.237502960428
√2^29: 24576 vs 23170.475005920864
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;More&amp;lt;/summary&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;√2^31: 49152 vs 46340.950011841735
√2^33: 98304 vs 92681.9000236835
√2^35: 196608 vs 185363.80004736703
√2^37: 393216 vs 370727.60009473417
√2^39: 786432 vs 741455.2001894685
√2^41: 1572864 vs 1482910.4003789374
√2^43: 3145728 vs 2965820.800757875
√2^45: 6291456 vs 5931641.601515752
√2^47: 12582912 vs 11863283.203031506
√2^49: 25165824 vs 23726566.406063017
√2^51: 50331648 vs 47453132.81212604
√2^53: 100663296 vs 94906265.62425211
√2^55: 201326592 vs 189812531.24850425
√2^57: 402653184 vs 379625062.4970086
√2^59: 805306368 vs 759250124.9940174
√2^61: 1610612736 vs 1518500249.9880352
√2^63: 3221225472 vs 3037000499.976071
√2^65: 6442450944 vs 6074000999.952143
√2^67: 12884901888 vs 12148001999.904287
√2^69: 25769803776 vs 24296003999.808582
√2^71: 51539607552 vs 48592007999.61717
√2^73: 103079215104 vs 97184015999.23438
√2^75: 206158430208 vs 194368031998.46878
√2^77: 412316860416 vs 388736063996.9377
√2^79: 824633720832 vs 777472127993.8755
√2^81: 1649267441664 vs 1554944255987.7512
√2^83: 3298534883328 vs 3109888511975.503
√2^85: 6597069766656 vs 6219777023951.008
√2^87: 13194139533312 vs 12439554047902.018
√2^89: 26388279066624 vs 24879108095804.043
√2^91: 52776558133248 vs 49758216191608.09
√2^93: 105553116266496 vs 99516432383216.22
√2^95: 211106232532992 vs 199032864766432.47
√2^97: 422212465065984 vs 398065729532865.06
√2^99: 844424930131968 vs 796131459065730.2
√2^101: 1688849860263936 vs 1592262918131461
√2^103: 3377699720527872 vs 3184525836262922.5
√2^105: 6755399441055744 vs 6369051672525847
√2^107: 13510798882111488 vs 12738103345051696
√2^109: 27021597764222976 vs 25476206690103400
√2^111: 54043195528445952 vs 50952413380206810
√2^113: 108086391056891904 vs 101904826760413630
√2^115: 216172782113783808 vs 203809653520827300
√2^117: 432345564227567616 vs 407619307041654700
√2^119: 864691128455135232 vs 815238614083309600
√2^121: 1729382256910270464 vs 1630477228166619600
√2^123: 3458764513820540928 vs 3260954456333240000
√2^125: 6917529027641081856 vs 6521908912666482000
√2^127: 13835058055282163712 vs 13043817825332965000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;p&gt;With a little polishing, this is the code I ended up with:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const std = @import(&quot;std&quot;);

// Kinda an approximation of 16 log2(x). Will be divided by 8 to approximate 2 log2(x).
fn log_approx_helper(y: usize) usize {
    const COMPLEMENT = @typeInfo(usize).int.bits - 1;
    const BITS_TO_PRESERVE = @as(comptime_int, COMPLEMENT - @clz(@as(usize, 20)));

    const x = y +| 20;
    const fls: std.math.Log2Int(usize) = @intCast(COMPLEMENT - @clz(x)); // [4, 63]
    const pack_bits_under_old_msb: std.meta.Int(.unsigned, BITS_TO_PRESERVE) = @truncate(x &amp;gt;&amp;gt; (fls - BITS_TO_PRESERVE));
    return (@as(usize, fls) &amp;lt;&amp;lt; BITS_TO_PRESERVE) | pack_bits_under_old_msb; // [16, 1023] on 64-bit
}

/// Modify the array so that it can hold at least `new_capacity` items.
/// Invalidates pointers if additional memory is needed.
export fn ensureTotalCapacity(capacity: usize, new_capacity: usize) usize {
    const power = 1 + (log_approx_helper(new_capacity) -| log_approx_helper(capacity)) / 8;
    const shift: std.math.Log2Int(usize) = @intCast(power &amp;gt;&amp;gt; 1);
    const approx_sqrt_2_power = (@as(usize, 1) &amp;lt;&amp;lt; shift) | (@as(usize, power &amp;amp; 1) &amp;lt;&amp;lt; (shift -| 1));
    return @max(capacity +| (capacity / 2 + 8), (capacity +| 20) *| approx_sqrt_2_power - 20);
}

// side note: I decided to just always use 20 instead of 19 where applicable, because it is a mostly trivial difference
// and we can reuse `capacity +| 20` in 2 locations.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here is the &lt;a href=&quot;https://zig.godbolt.org/#z:OYLghAFBqd5QCxAYwPYBMCmBRdBLAF1QCcAaPECAMzwBtMA7AQwFtMQByARg9KtQYEAysib0QXACx8BBAKoBnTAAUAHpwAMvAFYgAzKVpMGoAF55gpJfWQE8Ayo3QBhVLQCuLBiABMpJwAyeAyYAHKeAEaYxL5cpAAOqAqE9gyuHl6%2BCUkpAkEh4SxRMT5x1pi2qUIETMQE6Z7efuWVAtW1BPlhkdGxVjV1DZnNA53B3UW9pQCUVqjuxMjsHGgMCgQA1OvoGwCkegAiGwACeCyJdRC7Pj7b1z7T%2BwBCuxoAgq9vAPRfGwDSwXQTA2xhB8XixFQqjOTDsAg2qCoGy4ADYNrRUMAfBBVNMAHQbADqdFoGyiG3wADc8FgdhEAJ4bAAcGyIYIhUJhBEwGx86Mx2NxeM%2BVAY/OAAH0mODIaoJQhMLR4tEIPSQBt3MlTJhphqtTzdgB2F7vDZmjardYbZwAeQAssoAtg7dhQgAVPaHE4EenKgCSDH4EE1eG1%2BOCBDxEUICg2AFpkc9PuaLQIrU8/W6hBK3TaJcoAErYITYAsANWwnqOxyYCggaHOdjYEojpGt9sdztdHoTx2QtFMEBrdZD2rbPg00ynSfeyfNls2qirG0Z1xehucvI0M7eKYXGyotAU6u2eJYsIQeICAoDBGD%2Bt1%2B2rEectbvtodTpd7vjJ37g9xR49CeDYfj2ABWJ5pA2FE9F2cCDjnM193iJhkAAawlaMCAUCV3AYLBiAlNx0AlFgFAiE8CHQM9MBqPFbwgPF8OSYAQnQNsMyzHM80LYtSwrR8vWOAhiHw0RuRxT1sH2StqCPX8uOzXN8yLEty2wadgKQjZiDohYxSHWt71DTA20PBQhOcfZNyUnjVP4jTHw3DZUIwrCYzwgjomI2hSPIiJnlA354KeVE2y4DQfDghCETFFFJDjbDPiNRDZ3eH5fjtDA8CoRkCAVEFiGIJhGQUVBWQQWENkIC1QQQEiQU2eg3z2FENBCAB3CVRDcwh6V2dqau5cjhQysCA0pMQaVhTBY0SCNoljXKQXQfA4WYUk2BYEhGTwWMQkwWkxreTBVAuTZRQ2RgFAWTA3VQGpaFfPqfXraU0P69VRzMjYup6j7kC%2BvVTN1H69mNHSUNQTromXLg9h8ECIAxSVpQ5OUFSVFV/t6z6fV1OMjU3VGpRlKF5UVZViHe176SnUDmR3Pc002BQEFyggqJo88CqvG9BBMsNl1OQRX3WCBElh4hpNk5EgJNXd51Z9lZQlBQAEc6glHwJSluGnw2IyR31CKrJsrYOaoAhnM3Y2hd%2B/WZeuNEuHNvQ7fZzn42J%2BWFZ0vSCAMk5z1UWn8dXJHffDoGfUZvk12ZWYjbx2PI/XTcJ11AAqX30bVzXtd1p3fyzndUo4WZaE4cDeG8bheFQTgAC0LC2eZFgNaK9F4LmOC0KdSAVJhCMoWZ0JASQNDxABOFEZ9RHwZ5n8DwJnyRDS4cDDE4SReBYCQNA0Uh660Ugm44Xhj2PvuB9IOBYBgKAn4gJAG3iOhonISh38/mJTFEvhdCfA6DcmIMeCAlF%2B68GjMwYgaoG6kFgbUekNoIjaAqH3HgpAGxsEEDaBgtAEFnywBEdwwBXy0CPJwbBWBzwmHENA0g%2BA9KVEpHNTQvAzoVHcNyGhvBFrVyYbQPAEQSrwNcFgThpBRJnH4aQdhxAIhJEwAcTA9DgAiJMJw2Yh4mDAAUGWPAmBOo2mVPXbB/BBAiDEOwKQMhBCKBUOoJhug4hGG0eYSwIiIjHkgLMVA8QNrHg4HGG0GwCyKkwLWTAAAxVqcYBjADohsVQTIUQSgSnGLR7glxxhYMgeI7gnymAYIove59FHEBpBw/xVgomtG8BAJwQxvBxECOMQoxQJDZGSBtVpPTEh9NSF0LpvQygNI2u0QYbhGg9JaFM0YoyegxDKKMAZayOjLMmKs2Y5UFhLH0FXGuddpEXw2F41kYkGDoSNrgQgJBEZ6D0NMXuOjZjD1HhAceIBwLHyERUg%2BfyT5nM4FfEAN8dH3xfkgIgbhv4QFqAYsFpAkUKGUMYRUQgGqdQsbwX%2B9BiChFYMsS5gCblcPWiQGk%2BgHHCFEOIexVj5BKDUNItxpAdrMDQPcogxAAAStYniYEYG8XhqAzGMCOvIrlaFUC8pIJKvFnKBByuqPgZV2xggoq1QwDFIRaDYphsquFtA/ToBAFQykLB8kSWucAzqJV4j8OORwWuIKmEXytTagpwIyX2ruVS523c2yuHOH/J5bs3nQMHp83o3yd4cEBb84%2Bp9G46ohTI95pAJ7AqET3D1Z8L7Rrvg/Z%2B8BX4oFQOGwlCKCW9AAfakBtAwEQKgWfZB8D5GdtQegzB8jcGMAIAQoh0jSHkModQxBdDjCaOWCQvArC7DsJCWfbhyBeHLGwYI6RPjxH0kkfO3u1SD6IMUcopQaiNFaNADGvgRgDFGJMUq%2BRzKbGMukMypxbLXFZA8WYCwhhRF%2BITYE4JnAwkRKiTE%2BJVpElIpSUwWoyAEBZJyXkgpRSnzo3oGRAtqAqk1JA3syZqRHAEQ2f4Ai2zulxCGbkNIszMh0ZyBtGj4z6k2EWR0SjCyqhLM6Ss%2BZ6ymNtP6FswTOyJB7I7ocl5ib3VpvPpwC5bdyW3IgAq4NzzXlZpjR86JXyfl5t3vvFNhb02XysJm2%2BldoUVthageFFBEXEGRaEzDxTDg4cwPkgtaL9VYpxUp%2BtRKSWcH9UAylDzqkWoMG%2BhldjP2yG/S4s%2Bug/Cyp5UGwVChhWivFUq6ViCsvyqDS%2Bkrqq0Dqu1Yg3VOrqLBEC4a4L8jTXmstbQa1trYRNtEiKmlx9HXShdQp05nrODep66p4AVygGBpi08gw1pq0f0JU8h4JbK4GZHvG11ybgVKeLdZyF%2Bmc3mfzeNotKLbOD1zTPPEGg9AaCZHoQ0R9JBT3Aj4JkiafBmciqm0FVnbuuv%2BxZ5TIPs2KOSA4SQQA&quot;&gt;godbolt link&lt;/a&gt;. According to llvm-mca, on a Zen 4 system this &lt;code&gt;ensureTotalCapacity&lt;/code&gt; function would take ~22 cycles to execute. The &lt;a href=&quot;https://www.amd.com/content/dam/amd/en/documents/processor-tech-docs/software-optimization-guides/57647.zip&quot;&gt;Zen 4 optimization manual&lt;/a&gt; says&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The branch misprediction penalty is in the range from 11 to 18 cycles, depending on the type of mispredicted branch and whether or not the instructions are being fed from the Op Cache. The common case penalty is 13 cycles.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That means if we expect our original loop to run for at least 10 cycles, plus a branch mispredict penalty, this approximation would be faster (while reducing the overall pressure on the branch predictor). I&apos;m not sure we actually expect this loop to branch backwards anyway, but if we did, this could be a good alternative.&lt;/p&gt;
&lt;p&gt;Before:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ensureTotalCapacity:
        mov     rcx, -1
        mov     rax, rdi
.LBB0_1:
        mov     rdx, rax
        shr     rdx
        add     rdx, 8
        add     rax, rdx
        cmovb   rax, rcx
        cmp     rax, rsi
        jb      .LBB0_1
        ret
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ensureTotalCapacity:
        add     rsi, 20
        mov     rcx, -1
        mov     dl, 59
        mov     r8b, 59
        cmovb   rsi, rcx
        lzcnt   rax, rsi
        sub     dl, al
        shl     eax, 4
        shrx    rdx, rsi, rdx
        and     edx, 15
        or      rdx, rax
        mov     rax, rdi
        xor     rdx, 1008
        add     rax, 20
        cmovb   rax, rcx
        lzcnt   rsi, rax
        sub     r8b, sil
        shl     esi, 4
        shrx    r8, rax, r8
        and     r8d, 15
        or      r8, rsi
        xor     esi, esi
        xor     r8, 1008
        sub     rdx, r8
        mov     r8d, 0
        cmovae  r8, rdx
        shr     r8d, 3
        inc     r8
        mov     edx, r8d
        and     r8d, 1
        shr     dl
        mov     r9d, edx
        and     r9b, 63
        sub     r9b, 1
        movzx   r9d, r9b
        cmovb   r9d, esi
        shlx    rsi, r8, r9
        mov     r9, rdi
        shr     r9
        bts     rsi, rdx
        add     r9, 8
        add     r9, rdi
        cmovb   r9, rcx
        mul     rsi
        cmovo   rax, rcx
        add     rax, -20
        cmp     r9, rax
        cmova   rax, r9
        ret
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The original version of this article was posted &lt;a href=&quot;https://github.com/ziglang/zig/issues/15574&quot;&gt;here&lt;/a&gt;. Please don&apos;t comment on that issue, but feel free to read the comments there.&lt;/p&gt;
&lt;p&gt;Anyway, if you made it this far, thanks for reading!&lt;/p&gt;
&lt;p&gt;‒ Validark&lt;/p&gt;
</content:encoded></item><item><title>Vector Compression in Interleaved Space on ARM</title><link>https://validark.dev/posts/vector-compression-in-interleaved-space-on-arm/</link><guid isPermaLink="true">https://validark.dev/posts/vector-compression-in-interleaved-space-on-arm/</guid><description>How to emulate a VPCOMPRESSB on interleaved vectors</description><pubDate>Mon, 09 Sep 2024 12:56:30 GMT</pubDate><content:encoded>&lt;p&gt;In my last article, &lt;a href=&quot;../interleaved-vectors-on-arm/&quot;&gt;&lt;em&gt;Use interleaved vectors for parsing on ARM&lt;/em&gt;&lt;/a&gt;, I covered the three main algorithms we need to support on interleaved vectors for high performance parsing of &lt;a href=&quot;https://github.com/simdutf/simdutf/issues/428&quot;&gt;utf8&lt;/a&gt;, &lt;a href=&quot;https://github.com/simdjson/simdjson&quot;&gt;JSON&lt;/a&gt;, or &lt;a href=&quot;https://github.com/Validark/Accelerated-Zig-Parser&quot;&gt;Zig&lt;/a&gt; for aarch64/ARM architectures. Namely, the &lt;a href=&quot;../interleaved-vectors-on-arm/#movemask&quot;&gt;movemask&lt;/a&gt; and &lt;a href=&quot;../interleaved-vectors-on-arm/#unmovemask&quot;&gt;unmovemask&lt;/a&gt; routines, as well as the &lt;a href=&quot;../interleaved-vectors-on-arm/#elementwise-shifts&quot;&gt;elementwise shift&lt;/a&gt; replacement, all of which are more efficient when performed on interleaved vectors (except when shifting by a multiple of 16). I also briefly explained that we can perform prefix sum operations using these elementwise shifts, but I did not explain why we might want to. The answer is vector compression.&lt;/p&gt;
&lt;h2&gt;Vector Compression&lt;/h2&gt;
&lt;p&gt;In high performance parsers, we sometimes produce a vector bytemask and/or a bitmask which indicates certain elements we want to keep, and the rest should be thrown out. On the latest and greatest x86-64 hardware (with &lt;a href=&quot;https://en.wikipedia.org/wiki/AVX-512&quot;&gt;AVX-512&lt;/a&gt;), we have the &lt;code&gt;VPCOMPRESSB&lt;/code&gt; instruction, which can extract just those bytes from a 64-byte vector corresponding to a bitmask we pass in.&lt;/p&gt;
&lt;p&gt;On Arm, however, there is no &lt;code&gt;VPCOMPRESSB&lt;/code&gt;, nor is there an interleaved equivalent. So, per usual, we have to roll our own.&lt;/p&gt;
&lt;p&gt;Luckily, we reproduce the semantics of &lt;code&gt;VPCOMPRESSB&lt;/code&gt; by:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Finding the (exclusive) prefix sum of the bytemask.&lt;/li&gt;
&lt;li&gt;Adding the prefix sum to the identity counting vector.&lt;/li&gt;
&lt;li&gt;Simultaneously shifting each element by their corresponding amount, calculated in step 2.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Prefix Sum&lt;/h2&gt;
&lt;p&gt;As in the previous article, the prefix-sum of a vector can be computed like so:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fn prefixSum(vec_: @Vector(64, u8)) @Vector(64, u8) {
    var vec = vec_;
    inline for (0..6) |i| { // iterates from 0 to 5
        vec += std.simd.shiftElementsRight(vec, 1 &amp;lt;&amp;lt; i, 0);
    }
    return vec;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And, as first shown in the previous article, we can abstract away the &lt;a href=&quot;../interleaved-vectors-on-arm/#elementwise-shifts&quot;&gt;elementwise vector shifts&lt;/a&gt; with a helper function. That way, our interleaved version looks like so:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fn prefixSum(vec_: @Vector(64, u8)) @Vector(64, u8) {
    var vec = vec_;
    inline for (0..6) |i| { // iterates from 0 to 5
        vec += shiftInterleavedElementsRight(vec, 1 &amp;lt;&amp;lt; i, 0);
    }
    return vec;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It looks the same!&lt;/p&gt;
&lt;p&gt;Since we abstracted away the interleaved shift, we can think of it as though it&apos;s just a normal-ordered vector. So here is what a prefix sum looks like for a normally-ordered vector:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Validark/validark.github.io/main/src/content/posts/vector-compression-in-interleaved-space-on-arm/prefix_sum.svg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;As depicted above, we first shift the vector right by one, then add that to itself. Then we shift the result of that addition to the right by two, then add that to our previous result. If we continue with this pattern, every column ends up becoming the sum of all bytes that came before it.&lt;/p&gt;
&lt;p&gt;To apply this to vector compression, start with a &lt;code&gt;-1&lt;/code&gt; in each slot you intend to keep, and a &lt;code&gt;0&lt;/code&gt; otherwise:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./prefix_sum2.svg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Next, shift our result to the right by 1 because we want the exclusive prefix sum (if possible, it&apos;s more efficient to do this beforehand). Then add the final result to the identity counting vector.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./prefix_sum3.svg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;This resulting &lt;code&gt;travel_distances&lt;/code&gt; vector contains, for the (red) elements we care about, how many slots they each need to be shifted leftwards.&lt;/p&gt;
&lt;h2&gt;Compression via prefix-sum&lt;/h2&gt;
&lt;p&gt;Next, we shift each element left by the &lt;code&gt;travel_distances&lt;/code&gt; we calculated from the &lt;code&gt;prefix_sums&lt;/code&gt; vector. To accomplish this, we shift the vector by successive powers of 2, each time keeping only values whose binary representation has a 1 bit in the place value corresponding to the current power of 2, otherwise keeping the previous value. E.g., if we want to shift an element by a total of &lt;code&gt;5&lt;/code&gt; slots, we will shift it during the 1-shift and 4-shift stage, because the binary representation of &lt;code&gt;5&lt;/code&gt; is &lt;code&gt;0b101&lt;/code&gt; (i.e. 1+4).&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./compress.svg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Compresses the elements in `data` corresponding to non-zero bytes in the `condition` vector.
// Note this return a compressed 64-byte vector in interleaved space, meaning that if you want to write
// this out to memory, you need to use the `st4` instruction.
fn compress(data: @Vector(64, u8), prefix_sums: @Vector(64, u8)) @Vector(64, u8) {
    const indices = comptime @as(@Vector(64, u8), @bitCast(std.simd.deinterlace(4, std.simd.iota(u8, 64))));
    var travel_distances = indices +% prefix_sums;
    var compressed_data = data;

    inline for (0..6) |x| {
        const i = 1 &amp;lt;&amp;lt; x;
        const shifted_travel_distances = shiftInterleavedElementsLeft(travel_distances, i, 0));
        const shifted_compressed_data = shiftInterleavedElementsLeft(compressed_data, i, 0));
        const selector = cmtst(shifted_travel_distances, @splat(i));
        travel_distances = bsl(selector, shifted_travel_distances, travel_distances);
        compressed_data = bsl(selector, shifted_compressed_data, compressed_data);
    }

    return compressed_data;
}

fn cmtst(a: anytype, comptime b: @TypeOf(a)) @TypeOf(a) {
    return @select(u8, (a &amp;amp; b) != @as(@TypeOf(a), @splat(0)), @as(@TypeOf(a), @splat(0xff)), @as(@TypeOf(a), @splat(0)));
}

fn bsl(selector: anytype, a: @TypeOf(selector), b: @TypeOf(selector)) @TypeOf(selector) {
    return (a &amp;amp; selector) | (b &amp;amp; ~selector);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Unfortunately, this method requires a logarithmic number of steps, both in the prefix sum and the compression stage. It also has a strict serial dependency chain from start to finish.&lt;/p&gt;
&lt;p&gt;We can do better by finding the prefix sum of each group of 8 or 16, and writing out to memory 8 or 4 times instead of once.&lt;/p&gt;
&lt;h2&gt;Breaking it up&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./vector-compress-2.svg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;In order to prefix-sum and vector-compress groups of 8, 16 or 32, we need to be careful that our &lt;code&gt;shiftInterleavedElementsLeft&lt;/code&gt; and &lt;code&gt;shiftInterleavedElementsRight&lt;/code&gt; functions do not go across the boundaries we set. If we want to vector-compress groups of 8, we want our elementwise shift emulation to not add element 7 to element 8, 15 to 16, 23 to 24, etc. Luckily, we have an instruction that puts barriers between these groups. The &lt;code&gt;shl&lt;/code&gt;/&lt;code&gt;shr&lt;/code&gt; instructions! For groups of 8, we use 2-byte-granularity shifts, for groups of 16, we use 4, and for groups of 32 we would use 8-byte-granularity shifts.&lt;/p&gt;
&lt;p&gt;I define a custom &lt;code&gt;shiftElementsLeft&lt;/code&gt; upon which &lt;code&gt;shiftInterleavedElementsLeft&lt;/code&gt; is built (see definition of &lt;code&gt;shiftInterleavedElementsRight&lt;/code&gt; &lt;a href=&quot;../interleaved-vectors-on-arm/#shiftInterleavedElementsRight&quot;&gt;here&lt;/a&gt;), which has a comptime &lt;code&gt;boundary&lt;/code&gt; parameter. When the &lt;code&gt;boundary&lt;/code&gt; is smaller than a &lt;code&gt;u128&lt;/code&gt;, we will use a 16, 32, or 64 bit-wise shift:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fn shiftElementsLeft(vec: @Vector(16, u8), comptime amount: std.simd.VectorCount(@Vector(64, u8)), comptime boundary: type) @Vector(16, u8) {
    return if (boundary == u128)
        std.simd.shiftElementsLeft(vec, amount, 0)
    else
        @bitCast(@as(@Vector(16 / @sizeOf(boundary), boundary), @bitCast(vec)) &amp;gt;&amp;gt; @splat(8*amount));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This allows us to get our &lt;code&gt;st4&lt;/code&gt; instructions started earlier, with a lot more parallelism:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;inline for (0..64 / WIDTH) |i| {
    st4(
        dest[if (i == 0) 0 else prefix_sum_of_offsets[i*(WIDTH / 8) - 1]..],
        shiftInterleavedElementsLeft(compressed_data, WIDTH*i, u128)
    );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Where &lt;code&gt;prefix_sum_of_offsets&lt;/code&gt; is defined like so:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;comptime var prefix_sum_multiplier = 0;
inline for (0..64 / WIDTH) |i| prefix_sum_multiplier |= 1 &amp;lt;&amp;lt; i*WIDTH;
const prefix_sum_of_offsets: [8]u8 = @bitCast(
@as([2]u64, @bitCast(
    uzp2(
        neg(
            @as([4]@Vector(16, u8), @bitCast(prefix_sums))[3]
        )
    )
))[0] *% prefix_sum_multiplier);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This takes the bottommost prefix-sums vector, corresponding to what was originally &lt;code&gt;3, 7, 11, 15, 19&lt;/code&gt;, etc, then takes the arithmetic negative of each element, then extracts the odd bytes into a &lt;code&gt;u64&lt;/code&gt;, then multiplies by a constant &lt;code&gt;prefix_sum_multiplier&lt;/code&gt; that has every &lt;code&gt;WIDTH&lt;/code&gt; bits set to 1. E.g. when &lt;code&gt;WIDTH&lt;/code&gt; is 8, it will multiply by &lt;code&gt;0x0101010101010101&lt;/code&gt;, which will compute the byte-wise prefix-sum. When &lt;code&gt;WIDTH&lt;/code&gt; is 16, it will multiply by &lt;code&gt;0x0001000100010001&lt;/code&gt;, computing the 2-byte-wise prefix-sum. Quickly producing the &lt;code&gt;prefix_sum_of_offsets&lt;/code&gt; with a multiply instead of a serial dependency chain of add instructions allows us to calculate the destination pointers in parallel.&lt;/p&gt;
&lt;p&gt;Putting it all together: (&lt;a href=&quot;https://zig.godbolt.org/z/6enM4KxK6&quot;&gt;full code here&lt;/a&gt;)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const WIDTH = 16; // How many elements to operate on at once

/// Compresses the elements in `data` corresponding to the `condition` vector.
/// Writes to `dest`, including a number of undefined bytes.
/// In total, this expression gives the number of bytes written past `dest`:
/// switch (WIDTH) {
///    8, 16, 32 =&amp;gt; (64 - WIDTH) + 32,
///    64 =&amp;gt; 64,
/// }
export fn compress(data: @Vector(64, u8), condition: @Vector(64, u8), dest: [*]u8) u8 {
    const U = std.meta.Int(.unsigned, WIDTH*2);
    const indices = comptime @as(@Vector(64, u8), @bitCast(std.simd.deinterlace(4, std.simd.iota(u8, 64) &amp;amp; @as(@Vector(64, u8), @splat(WIDTH - 1)))));

    var prefix_sums = @select(u8, condition != @as(@Vector(64, u8), @splat(0)),
        @as(@Vector(64, u8), @splat(255)),
        @as(@Vector(64, u8), @splat(0)),
    );

    // Next, shift elements right by 1, 2, 4, 8, 16, and 32, and accumulate at each step
    inline for (0..std.math.log2(WIDTH)) |i| {
        prefix_sums +%= shiftInterleavedElementsRight(prefix_sums, 1 &amp;lt;&amp;lt; i, U);
    }

    comptime var prefix_sum_multiplier = 0;
    inline for (0..64 / WIDTH) |i| prefix_sum_multiplier |= 1 &amp;lt;&amp;lt; i*WIDTH;
    const prefix_sum_of_offsets: [8]u8 = @bitCast(
    @as([2]u64, @bitCast(
        uzp2(
            neg(
                @as([4]@Vector(16, u8), @bitCast(prefix_sums))[3]
            )
        )
    ))[0] *% prefix_sum_multiplier);

    // Now take the identity indices and add it to the prefix_sums.
    // This value tells us how far each value should be left-shifted
    var travel_distances = indices +% shiftInterleavedElementsRight(prefix_sums, 1, U);
    var compressed_data = data;

    inline for (0..std.math.log2(WIDTH)) |x| {
        const i = 1 &amp;lt;&amp;lt; x;
        const shifted_left = shiftInterleavedElementsLeft(travel_distances, i, U);
        const shifted_compressed_data = shiftInterleavedElementsLeft(compressed_data, i, U);
        const selector = cmtst(shifted_left, @splat(i));
        travel_distances = bsl(selector, shifted_left, travel_distances);
        compressed_data = bsl(selector, shifted_compressed_data, compressed_data);
    }

    inline for (0..64 / WIDTH) |i| {
        (if (WIDTH == 64) st4 else st4_first_32)(
            dest[if (i == 0) 0 else prefix_sum_of_offsets[i*(WIDTH / 8) - 1]..],
            shiftInterleavedElementsLeft(compressed_data, WIDTH*i, u128)
        );
    }

    return prefix_sum_of_offsets[7];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Subject to the following issues:&lt;/p&gt;
&lt;p&gt;&amp;lt;div id=&quot;issue-dump&quot; style=&quot;display: flex; flex-direction: column; align-items: center; align-self: flex-start&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;script&amp;gt;
document.getElementById(&quot;issue-dump&quot;).innerHTML = [107438, 107423, 107404, 107243, 107099, 107093, 107088].map(i =&amp;gt; `&amp;lt;div class=&quot;individual-issue&quot;&amp;gt;
&amp;lt;div&amp;gt;&amp;lt;svg id=&quot;issue-indicator-${i}&quot; class=&quot;issue-indicator issue-indicator-unknown&quot; viewBox=&quot;0 0 16 16&quot; version=&quot;1.1&quot; width=&quot;20&quot; height=&quot;20&quot; aria-hidden=&quot;true&quot;&amp;gt;&amp;lt;path stroke=&quot;none&quot; fill=&quot;#59636e&quot; d=&quot;M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Zm9.78-2.22-5.5 5.5a.749.749 0 0 1-1.275-.326.749.749 0 0 1 .215-.734l5.5-5.5a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042Z&quot;&amp;gt;&amp;lt;/path&amp;gt;&amp;lt;/svg&amp;gt;&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;div&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;p&amp;gt;&amp;lt;a href=&quot;https://github.com/llvm/llvm-project/issues/${i}&quot;&amp;gt;llvm/llvm-project#${i}&amp;lt;/a&amp;gt;&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;`).join(&quot;\n\n&quot;);
&amp;lt;/script&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;script is:inline&amp;gt;
{
const open = &apos;&amp;lt;path stroke=&quot;none&quot; fill=&quot;#1a7f37&quot; d=&quot;M8 9.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z&quot;&amp;gt;&amp;lt;/path&amp;gt;&amp;lt;path stroke=&quot;none&quot; fill=&quot;#1a7f37&quot; d=&quot;M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Z&quot;&amp;gt;&amp;lt;/path&amp;gt;&apos;;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const closed_completed = &apos;&amp;lt;path stroke=&quot;none&quot; fill=&quot;#8250df&quot; d=&quot;M11.28 6.78a.75.75 0 0 0-1.06-1.06L7.25 8.69 5.78 7.22a.75.75 0 0 0-1.06 1.06l2 2a.75.75 0 0 0 1.06 0l3.5-3.5Z&quot;&amp;gt;&amp;lt;/path&amp;gt;&amp;lt;path stroke=&quot;none&quot; fill=&quot;#8250df&quot; d=&quot;M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0Zm-1.5 0a6.5 6.5 0 1 0-13 0 6.5 6.5 0 0 0 13 0Z&quot;&amp;gt;&amp;lt;/path&amp;gt;&apos;;

const closed_not_planned = &apos;&amp;lt;path stroke=&quot;none&quot; fill=&quot;#59636e&quot; d=&quot;M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Zm9.78-2.22-5.5 5.5a.749.749 0 0 1-1.275-.326.749.749 0 0 1 .215-.734l5.5-5.5a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042Z&quot;&amp;gt;&amp;lt;/path&amp;gt;&apos;;
for (const issue_id of [107438, 107423, 107404, 107243, 107099, 107093, 107088]) {
    fetch(`https://api.github.com/repos/llvm/llvm-project/issues/${issue_id}`)
        .then(e =&amp;gt; e.json())
        .then(e =&amp;gt; {
            const svg = e.state === &quot;open&quot; ? open : e.state_reason === &quot;completed&quot; ? closed_completed : closed_not_planned;

            for (const e of document.getElementsByClassName(&quot;issue-indicator&quot;)) {
                if (e.id === `issue-indicator-${issue_id}`) {
                    e.classList.remove(&quot;issue-indicator-unknown&quot;);
                    e.innerHTML = svg;
                }
            }
        })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}
&amp;lt;/script&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;style&amp;gt;
.issue-indicator-unknown {
transform: &quot;rotate(45)&quot;;
}&lt;/p&gt;
&lt;p&gt;.individual-issue {
display: flex; flex-direction: row; align-items: center; align-self: flex-start;
}&lt;/p&gt;
&lt;p&gt;.individual-issue &amp;gt; div {
margin-right: 0.5em;
}&lt;/p&gt;
&lt;p&gt;.individual-issue &amp;gt; div + div {
height: 2.3em;
}&lt;/p&gt;
&lt;p&gt;.individual-issue &amp;gt; div + div &amp;gt; p {
margin-top: 0;
margin-bottom: 0;
line-height: 1.5;
white-space: nowrap;
}&lt;/p&gt;
&lt;p&gt;p + div#issue-dump {
margin-bottom: 0.5em;
}
&amp;lt;/style&amp;gt;&lt;/p&gt;
&lt;h2&gt;Technique II: Lookup table&lt;/h2&gt;
&lt;p&gt;Unfortunately, it seems that even with those issues fixed, it&apos;s still going to be more efficient to use a lookup table, if you can afford to consume 2KiB of your precious cache. (&lt;a href=&quot;https://zig.godbolt.org/z/rKhEzdnPf&quot;&gt;Godbolt link&lt;/a&gt;)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export fn compress(interleaved_data: @Vector(64, u8), bitstring: u64, dest: [*]u8) void {
    comptime var lookups: [256]@Vector(8, u8) = undefined;
    comptime {
        @setEvalBranchQuota(100000);
        for (&amp;amp;lookups, 0..) |*slot, i| {
            var pos: u8 = 0;
            for (0..8) |j| {
                const bit: u1 = @truncate(i &amp;gt;&amp;gt; j);
                slot[pos] = j / 4 + (j &amp;amp; 3) * 16;
                pos += bit;
            }

            for (pos..8) |j| {
                slot[j] = 255;
            }
        }
    }

    const chunks: [4]@Vector(16, u8) = @bitCast(interleaved_data);

    const prefix_sum_of_popcounts =
        @as(u64, @bitCast(@as(@Vector(8, u8), @popCount(@as(@Vector(8, u8), @bitCast(bitstring))))))
            *% 0x0101010101010101;

    inline for (@as([8]u8, @bitCast(bitstring)), @as([8]u8, @bitCast(prefix_sum_of_popcounts)), 0..)
    |byte, pos, i| {
        dest[pos..][0..8].* = tbl4(
            chunks[0],
            chunks[1],
            chunks[2],
            chunks[3],
            lookups[byte] +| @as(@Vector(8, u8), @splat(2*i))
        );
    }
}

fn tbl4(
    table_part_1: @Vector(16, u8),
    table_part_2: @Vector(16, u8),
    table_part_3: @Vector(16, u8),
    table_part_4: @Vector(16, u8),
    indices: @Vector(8, u8)
) @TypeOf(indices) {
    return struct {
        extern fn @&quot;llvm.aarch64.neon.tbl4&quot;(@TypeOf(table_part_1), @TypeOf(table_part_2), @TypeOf(table_part_3), @TypeOf(table_part_4), @TypeOf(indices)) @TypeOf(indices);
    }.@&quot;llvm.aarch64.neon.tbl4&quot;(table_part_1, table_part_2, table_part_3, table_part_4, indices);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This technique also has to use &lt;code&gt;tbl4&lt;/code&gt; because it is deinterleaving the data at the same time as compressing. In normal space, you would just use &lt;code&gt;tbl1&lt;/code&gt;. But hey, as long as there is no serial dependency, you only eat the latency once.&lt;/p&gt;
&lt;p&gt;Now go compress your interleaved vectors, you glorious vectorizers!&lt;/p&gt;
&lt;p&gt;‒ Validark&lt;/p&gt;
</content:encoded></item><item><title>Use interleaved vectors for parsing on ARM</title><link>https://validark.dev/posts/interleaved-vectors-on-arm/</link><guid isPermaLink="true">https://validark.dev/posts/interleaved-vectors-on-arm/</guid><description>Why using interleaved vectors via LD4 results in efficiency gains</description><pubDate>Tue, 03 Sep 2024 13:15:30 GMT</pubDate><content:encoded>&lt;p&gt;&amp;lt;!-- translate3d(6px, 115px, 0px) scale3d(6.4,6.4,6.4); --&amp;gt;&lt;/p&gt;
&lt;p&gt;When parsing, we can take advantage of data-level parallelism by operating on more than one byte at a time.
Modern CPU architectures provide us with &lt;a href=&quot;https://en.wikipedia.org/wiki/Single_instruction,_multiple_data&quot;&gt;SIMD (single-instruction, multiple-data)&lt;/a&gt; instructions which allow us to do some operation on all bytes in a vector simultaneously.
On the latest and greatest x86-64 hardware (with &lt;a href=&quot;https://en.wikipedia.org/wiki/AVX-512&quot;&gt;AVX-512&lt;/a&gt;), we have hardware support for 64-byte vectors.
This is convenient because we often want to do a &lt;a href=&quot;#movemask&quot;&gt;movemask&lt;/a&gt; operation which reduces the 64-byte vector into a 64-bit bitstring. Each bit in this &quot;mask&quot; corresponds to a byte in the vector, and can tell us some piece of information like &quot;is it a whitespace?&quot;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./bitstring-reduction-dark.svg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Because modern hardware also has nice bitstring-query operations, we can efficiently do, e.g., a &lt;a href=&quot;https://en.wikipedia.org/wiki/Find_first_set&quot;&gt;count-trailing-zeroes&lt;/a&gt; operation in 1 or 2 cycles to answer questions like &quot;how many non-whitespace characters are there at the start of the vector?&quot;&lt;/p&gt;
&lt;h2&gt;Enter ARM Neon&lt;/h2&gt;
&lt;p&gt;On CPU&apos;s sporting the ARM Neon instruction set, like the popular Apple M-series chips, we do not have direct hardware support for 64-byte vectors.
Instead, we have to use 4 vectors of 16 bytes each to emulate 64-byte width.&lt;/p&gt;
&lt;p&gt;This leaves us with a choice. We could load in 4 vectors in normal order, like so: (in &lt;a href=&quot;https://ziglang.org/&quot;&gt;Zig&lt;/a&gt;, we can load in 64 bytes and let the compiler load in 4 vectors for us)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export fn load64Bytes(ptr: [*]u8) @Vector(64, u8) {
    return ptr[0..64].*;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Aarch64 Assembly emit:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ldp     q0, q1, [x0]; loads two vectors from pointer `x0`
ldp     q2, q3, [x0, #32]; loads two vectors from `x0+32`
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or in interleaved order, like so:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export fn load64BytesInterleaved(ptr: [*]u8) @Vector(64, u8) {
    return @bitCast(std.simd.deinterlace(4, ptr[0..64].*));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Aarch64 Assembly emit (we have an instruction for that!):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ld4     { v0.16b, v1.16b, v2.16b, v3.16b }, [x0]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;However, interleaved order, hence the name, does not load data in normal order. It loads every 4th byte, with the first vector starting at byte 0, the second vector starting at byte 1, and so on:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./interleaved-vector-dark.svg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;This strategy is nice for when you have an array of 4 byte structs, where each byte is a separate field.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const rgba = struct { r: u8, g: u8, b: u8, a: u8 };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;E.g. if you have an array of &lt;code&gt;rgba&lt;/code&gt;s, &lt;code&gt;ld4&lt;/code&gt; will return your &lt;code&gt;r&lt;/code&gt;, &lt;code&gt;g&lt;/code&gt;, &lt;code&gt;b&lt;/code&gt;, and &lt;code&gt;a&lt;/code&gt; values in separate vectors.&lt;/p&gt;
&lt;p&gt;However, even when reordering is not what we&apos;re going for, we can still see efficiency gains by using this facility when &lt;a href=&quot;#movemask&quot;&gt;movemasking&lt;/a&gt;, when &lt;a href=&quot;#unmovemask&quot;&gt;unmovemasking&lt;/a&gt;, or when doing &lt;a href=&quot;#elementwise-shifts&quot;&gt;vector element shifts&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Movemask&lt;/h2&gt;
&lt;p&gt;If you want to produce a 64-bit bitstring that tells you where the space characters are in a 64-byte chunk, you might try the following:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export fn checkWhitespace(ptr: [*]u8) u64 {
    return @bitCast(
        @as(@Vector(64, u8), ptr[0..64].*) == @as(@Vector(64, u8), @splat(&apos; &apos;))
    );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At the time of writing, LLVM will give you this emit: (&lt;a href=&quot;https://zig.godbolt.org/z/nP44MrMWx&quot;&gt;Check what it is today&lt;/a&gt;)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.LCPI0_0:
    .byte   1
    .byte   2
    .byte   4
    .byte   8
    .byte   16
    .byte   32
    .byte   64
    .byte   128
    .byte   1
    .byte   2
    .byte   4
    .byte   8
    .byte   16
    .byte   32
    .byte   64
    .byte   128
checkWhitespace:
    ldp     q0, q1, [x0]
    ldp     q2, q3, [x0, #32]
    movi    v4.16b, #32
    cmeq    v3.16b, v3.16b, v4.16b
    adrp    x8, .LCPI0_0
    ldr     q5, [x8, :lo12:.LCPI0_0]
    and     v3.16b, v3.16b, v5.16b
    ext     v6.16b, v3.16b, v3.16b, #8
    zip1    v3.16b, v3.16b, v6.16b
    addv    h3, v3.8h
    fmov    w8, s3
    cmeq    v2.16b, v2.16b, v4.16b
    and     v2.16b, v2.16b, v5.16b
    ext     v3.16b, v2.16b, v2.16b, #8
    zip1    v2.16b, v2.16b, v3.16b
    addv    h2, v2.8h
    fmov    w9, s2
    bfi     w9, w8, #16, #16
    cmeq    v1.16b, v1.16b, v4.16b
    and     v1.16b, v1.16b, v5.16b
    ext     v2.16b, v1.16b, v1.16b, #8
    zip1    v1.16b, v1.16b, v2.16b
    addv    h1, v1.8h
    fmov    w8, s1
    cmeq    v0.16b, v0.16b, v4.16b
    and     v0.16b, v0.16b, v5.16b
    ext     v1.16b, v0.16b, v0.16b, #8
    zip1    v0.16b, v0.16b, v1.16b
    addv    h0, v0.8h
    fmov    w10, s0
    bfi     w10, w8, #16, #16
    orr     x0, x10, x9, lsl #32
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Unfortunately, LLVM, has not yet seen the light-- of interleaved vectors. Here is the x86-64 emit for Zen 4, for reference:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.LCPI0_0:
    .zero   64,32
checkWhitespace:
    vmovdqu64       zmm0, zmmword ptr [rdi]
    vpcmpeqb        k0, zmm0, zmmword ptr [rip + .LCPI0_0]
    kmovq   rax, k0
    vzeroupper
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This paints Arm/Aarch64 in an unnecessarily bad light. With interleaved vectors, and telling the compiler exactly what we want to do, we can do a lot better.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export fn checkWhitespace(ptr: [*]u8) u64 {
    const vec: @Vector(64, u8) = @bitCast(std.simd.deinterlace(4, ptr[0..64].*));
    const spaces = @select(u8, vec == @as(@Vector(64, u8), @splat(&apos; &apos;)),
        @as(@Vector(64, u8), @splat(0xFF)),
        @as(@Vector(64, u8), @splat(0)));
    return vmovmaskq_u8(spaces);
}

fn vmovmaskq_u8(vec: @Vector(64, u8)) u64 {
    const chunks: [4]@Vector(16, u8) = @bitCast(vec);
    const t0 = vsriq_n_u8(chunks[1], chunks[0], 1);
    const t1 = vsriq_n_u8(chunks[3], chunks[2], 1);
    const t2 = vsriq_n_u8(t1, t0, 2);
    const t3 = vsriq_n_u8(t2, t2, 4);
    const t4 = vshrn_n_u16(@bitCast(t3), 4);
    return @bitCast(t4);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This gives us the following assembly (&lt;a href=&quot;https://zig.godbolt.org/z/76zEMee5K&quot;&gt;See full Zig code and assembly here&lt;/a&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;checkWhitespace:
    movi    v0.16b, #32
    ld4     { v1.16b, v2.16b, v3.16b, v4.16b }, [x0]
    cmeq    v5.16b, v3.16b, v0.16b
    cmeq    v6.16b, v4.16b, v0.16b
    cmeq    v7.16b, v1.16b, v0.16b
    cmeq    v0.16b, v2.16b, v0.16b
    sri     v0.16b, v7.16b, #1
    sri     v6.16b, v5.16b, #1
    sri     v6.16b, v0.16b, #2
    sri     v6.16b, v6.16b, #4
    shrn    v0.8b, v6.8h, #4
    fmov    x0, d0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is a lot cleaner! I show and explain how this routine works &lt;a href=&quot;https://www.youtube.com/live/FDiUKafPs0U?t=2245&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;h6&gt;Note: if you check LLVM-mca, you will find that this is expected to run slower than the previous version &lt;a href=&quot;https://zig.godbolt.org/z/K9nYn1e46&quot;&gt;if you only care to do a single movemask&lt;/a&gt;. However, if you want to do several movemasks simultaneously, &lt;a href=&quot;https://zig.godbolt.org/z/69qYnzjo9&quot;&gt;this version will be faster&lt;/a&gt;. LLVM&apos;s cost model is also conservative; the &lt;code&gt;shrn&lt;/code&gt; instruction has 3 cycles of latency on Apple M3&apos;s performance core, but 4 cycles of latency on their efficiency core. LLVM treats it as a 4-cycle operation.&lt;/h6&gt;
&lt;h2&gt;Unmovemask&lt;/h2&gt;
&lt;p&gt;Sometimes, we want to go the other way. This may be because we did a movemask, then did some bit manipulation on the mask, and now we want to turn our mask back into a vector. Here is the routine for normal vectors on ARM64:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export fn unmovemask64(x: u64) @Vector(64, u8) {
    const bit_positions = @as(@Vector(64, u8), @splat(1)) &amp;lt;&amp;lt; @truncate(std.simd.iota(u8, 64));
    const v0 = std.simd.join(@as(@Vector(8, u8), @splat(0)), @as(@Vector(8, u8), @splat(1)));
    const v1 = std.simd.join(@as(@Vector(8, u8), @splat(2)), @as(@Vector(8, u8), @splat(3)));
    const v2 = std.simd.join(@as(@Vector(8, u8), @splat(4)), @as(@Vector(8, u8), @splat(5)));
    const v3 = std.simd.join(@as(@Vector(8, u8), @splat(6)), @as(@Vector(8, u8), @splat(7)));

    const v = std.simd.join(@as(@Vector(8, u8), @bitCast(x)), @as(@Vector(8, u8), @splat(undefined)));

    const final: @Vector(64, u8) = @bitCast([4]@Vector(16, u8){ tbl(v, v0), tbl(v, v1), tbl(v, v2), tbl(v, v3) });

    return @select(u8, (final &amp;amp; bit_positions) == bit_positions,
        @as(@Vector(64, u8), @splat(0xFF)),
        @as(@Vector(64, u8), @splat(0)),
    );
}

fn tbl(table: @Vector(16, u8), indices: anytype) @TypeOf(indices) {
    switch (@TypeOf(indices)) {
        @Vector(8, u8), @Vector(16, u8) =&amp;gt; {},
        else =&amp;gt; @compileError(&quot;[tbl] Invalid type for indices&quot;),
    }
    return struct {
        extern fn @&quot;llvm.aarch64.neon.tbl1&quot;(@TypeOf(table), @TypeOf(indices)) @TypeOf(indices);
    }.@&quot;llvm.aarch64.neon.tbl1&quot;(table, indices);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And the corresponding assembly: (I reordered the instructions and removed C ABI stuff)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.LCPI1_0:
        .byte   0
        .byte   0
        .byte   0
        .byte   0
        .byte   0
        .byte   0
        .byte   0
        .byte   0
        .byte   1
        .byte   1
        .byte   1
        .byte   1
        .byte   1
        .byte   1
        .byte   1
        .byte   1
.LCPI1_1:
        .byte   2
        .byte   2
        .byte   2
        .byte   2
        .byte   2
        .byte   2
        .byte   2
        .byte   2
        .byte   3
        .byte   3
        .byte   3
        .byte   3
        .byte   3
        .byte   3
        .byte   3
        .byte   3
.LCPI1_2:
        .byte   4
        .byte   4
        .byte   4
        .byte   4
        .byte   4
        .byte   4
        .byte   4
        .byte   4
        .byte   5
        .byte   5
        .byte   5
        .byte   5
        .byte   5
        .byte   5
        .byte   5
        .byte   5
.LCPI1_3:
        .byte   6
        .byte   6
        .byte   6
        .byte   6
        .byte   6
        .byte   6
        .byte   6
        .byte   6
        .byte   7
        .byte   7
        .byte   7
        .byte   7
        .byte   7
        .byte   7
        .byte   7
        .byte   7
.LCPI1_4:
        .byte   1
        .byte   2
        .byte   4
        .byte   8
        .byte   16
        .byte   32
        .byte   64
        .byte   128
        .byte   1
        .byte   2
        .byte   4
        .byte   8
        .byte   16
        .byte   32
        .byte   64
        .byte   128
unmovemask64:
        adrp    x9, .LCPI1_0; load the 5 vectors above into registers
        ldr     q1, [x9, :lo12:.LCPI1_0]
        adrp    x9, .LCPI1_1
        ldr     q2, [x9, :lo12:.LCPI1_1]
        adrp    x9, .LCPI1_2
        ldr     q3, [x9, :lo12:.LCPI1_2]
        adrp    x9, .LCPI1_3
        ldr     q4, [x9, :lo12:.LCPI1_3]
        adrp    x9, .LCPI1_4
        ldr     q5, [x9, :lo12:.LCPI1_4]
        fmov    d0, x0; move data from a scalar register to a vector register
        tbl     v1.16b, { v0.16b }, v1.16b; broadcast byte 0 to bytes 0-7, byte 1 to bytes 8-15
        tbl     v2.16b, { v0.16b }, v2.16b; broadcast byte 2 to bytes 0-7, byte 3 to bytes 8-15
        tbl     v3.16b, { v0.16b }, v3.16b; broadcast byte 4 to bytes 0-7, byte 5 to bytes 8-15
        tbl     v4.16b, { v0.16b }, v4.16b; broadcast byte 6 to bytes 0-7, byte 7 to bytes 8-15
        cmtst   v1.16b, v1.16b, v5.16b; turn each unique bit position into a byte of all 0xFF or 0
        cmtst   v2.16b, v2.16b, v5.16b; turn each unique bit position into a byte of all 0xFF or 0
        cmtst   v3.16b, v3.16b, v5.16b; turn each unique bit position into a byte of all 0xFF or 0
        cmtst   v4.16b, v4.16b, v5.16b; turn each unique bit position into a byte of all 0xFF or 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In interleaved space, we can do better:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export fn unmovemask(x: u64) @Vector(64, u8) {
    const vec = @as(@Vector(8, u8), @bitCast(x));
    const interlaced_vec = std.simd.interlace(.{ vec, vec });

    return std.simd.join(
        std.simd.join(cmtst(interlaced_vec, @bitCast(@as(@Vector(8, u16), @splat(@as(u16, @bitCast([2]u8{ 1 &amp;lt;&amp;lt; 0, 1 &amp;lt;&amp;lt; 4 })))))),
                      cmtst(interlaced_vec, @bitCast(@as(@Vector(8, u16), @splat(@as(u16, @bitCast([2]u8{ 1 &amp;lt;&amp;lt; 1, 1 &amp;lt;&amp;lt; 5 }))))))),
        std.simd.join(cmtst(interlaced_vec, @bitCast(@as(@Vector(8, u16), @splat(@as(u16, @bitCast([2]u8{ 1 &amp;lt;&amp;lt; 2, 1 &amp;lt;&amp;lt; 6 })))))),
                      cmtst(interlaced_vec, @bitCast(@as(@Vector(8, u16), @splat(@as(u16, @bitCast([2]u8{ 1 &amp;lt;&amp;lt; 3, 1 &amp;lt;&amp;lt; 7 })))))))
    );
}

fn cmtst(a: anytype, comptime b: @TypeOf(a)) @TypeOf(a) {
    return @select(u8, (a &amp;amp; b) != @as(@TypeOf(a), @splat(0)), @as(@TypeOf(a), @splat(0xff)), @as(@TypeOf(a), @splat(0)));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In assembly: (after &lt;a href=&quot;https://github.com/llvm/llvm-project/issues/107243&quot;&gt;llvm/llvm-project#107243&lt;/a&gt;, reordering, and removing C ABI ceremony)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;unmovemask_interleaved:
        mov     w9, #0x400; load 4 constants into vectors
        dup     v0.8h, w9
        mov     w9, #0x501
        dup     v1.8h, w9
        mov     w9, #0x602
        dup     v2.8h, w9
        mov     w9, #0x703
        dup     v3.8h, w9
        fmov    d4, x0; move data from a scalar register to a vector register
        zip1    v4.16b, v4.16b, v4.16b; interleave input with itself -&amp;gt; 0 0 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8
        cmtst   v0.16b, v0.16b, v4.16b; match bits positions: 0,4,0,4,0,4,0,4,0,4,0,4,0,4,0,4
        cmtst   v1.16b, v1.16b, v4.16b; match bits positions: 1,5,1,5,1,5,1,5,1,5,1,5,1,5,1,5
        cmtst   v2.16b, v2.16b, v4.16b; match bits positions: 2,6,2,6,2,6,2,6,2,6,2,6,2,6,2,6
        cmtst   v3.16b, v3.16b, v4.16b; match bits positions: 3,7,3,7,3,7,3,7,3,7,3,7,3,7,3,7
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this case, using interleaved vectors eliminated the memory accesses and reduced 4 &lt;code&gt;tbl&lt;/code&gt; instructions to a single &lt;code&gt;zip1&lt;/code&gt; instruction!&lt;/p&gt;
&lt;h2&gt;Elementwise Shifts&lt;/h2&gt;
&lt;p&gt;Initially, I believed we could only do algorithms on interleaved vectors where order didn&apos;t change. However, while working on the UTF8 validator for my &lt;a href=&quot;https://github.com/Validark/Accelerated-Zig-Parser&quot;&gt;Accelerated-Zig-Parser&lt;/a&gt;, I realized we can emulate element shifts in interleaved space.&lt;/p&gt;
&lt;p&gt;Let&apos;s say we have a vector where each byte contains its own index. Next, we shift the elements right by one, shifting in &lt;code&gt;-1&lt;/code&gt;. On normal vectors, this looks like so (only showing first 16 bytes of the vector due to space constraints):&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;shift-interleaved-dark.svg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;This aligns the bytes such that each pair of contiguous bytes is now aligned column-wise. This allows us to validate 2-byte UTF8 codepoints efficiently. To properly validate 3 and 4 byte codepoints, we will need to do two more such shifts, shifting in &lt;code&gt;-2&lt;/code&gt;, and &lt;code&gt;-3&lt;/code&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;shift-interleaved-2-dark.svg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;In this article, I will refer to the main vectors as &lt;code&gt;prev0&lt;/code&gt; (the top vector in the previous diagram), and the vectors containing the previous bytes, relative to &lt;code&gt;prev0&lt;/code&gt;, as &lt;code&gt;prev1&lt;/code&gt;, &lt;code&gt;prev2&lt;/code&gt;, and &lt;code&gt;prev3&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;For UTF8 validation, we can match the 4th byte of a 4-byte sequence in &lt;code&gt;prev0&lt;/code&gt;, the 3rd byte in &lt;code&gt;prev1&lt;/code&gt;, the 2nd byte in &lt;code&gt;prev2&lt;/code&gt;, and the 1st byte in &lt;code&gt;prev3&lt;/code&gt; (because the byte-order increases by 1 as we move up a column).&lt;/p&gt;
&lt;p&gt;Now, looking again at the interleaved vectors given by &lt;code&gt;ld4&lt;/code&gt;, think about how we might find the relative &lt;code&gt;prev1&lt;/code&gt;, &lt;code&gt;prev2&lt;/code&gt;, and &lt;code&gt;prev3&lt;/code&gt; of each of the following vectors (each one is a &lt;code&gt;prev0&lt;/code&gt;):&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./interleaved-vector-dark.svg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Here are the &lt;code&gt;prev1&lt;/code&gt; vectors relative to each of the vectors above:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./interleaved-vector-2-dark.svg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Hopefully it is obvious that all we did is subtract one from each index from the perspective of byte-order. As you can see, the lower 3 vectors were already created by &lt;code&gt;ld4&lt;/code&gt;! It&apos;s only the uppermost vector which needs to be computed, by shifting in &lt;code&gt;-1&lt;/code&gt; to the vector that starts with &lt;code&gt;3&lt;/code&gt;, &lt;code&gt;7&lt;/code&gt;, &lt;code&gt;11&lt;/code&gt;, &lt;code&gt;15&lt;/code&gt;, etc.&lt;/p&gt;
&lt;p&gt;That means we can get the semantics of a 64-byte shift by 1 by only shifting a single 16-byte vector!&lt;/p&gt;
&lt;p&gt;Let&apos;s see how this extends to the &lt;code&gt;prev2&lt;/code&gt; vectors:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./interleaved-vectors-4-dark.svg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Same deal as before, the bottom three vectors in this diagram we already had when we computed &lt;code&gt;prev1&lt;/code&gt;. Again, we just need to compute the uppermost vector in this diagram by shifting &lt;code&gt;-2&lt;/code&gt; into the vector that starts with &lt;code&gt;2&lt;/code&gt;, &lt;code&gt;6&lt;/code&gt;, &lt;code&gt;10&lt;/code&gt;, &lt;code&gt;14&lt;/code&gt;, etc.&lt;/p&gt;
&lt;p&gt;We can do the same thing to produce the &lt;code&gt;prev3&lt;/code&gt; vectors. The only additional computation needed is that we need to shift in &lt;code&gt;-3&lt;/code&gt; to the vector that starts with &lt;code&gt;1&lt;/code&gt;, &lt;code&gt;5&lt;/code&gt;, &lt;code&gt;9&lt;/code&gt;, &lt;code&gt;13&lt;/code&gt;, etc. Once we do that, we will have the following vectors:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;interleaved-vectors-5-dark.svg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;In the above diagram, each of the bottom 4 vectors are a &lt;code&gt;prev0&lt;/code&gt; vector. The &lt;code&gt;prev1&lt;/code&gt; vector, relative to each &lt;code&gt;prev0&lt;/code&gt; vector, is the one above it, the &lt;code&gt;prev2&lt;/code&gt; is the one 2 rows above, and the &lt;code&gt;prev3&lt;/code&gt; is 3 rows above, for each &lt;code&gt;prev0&lt;/code&gt; vector.&lt;/p&gt;
&lt;p&gt;With only 3 vector shifts, and a total of 7 vectors in play, we can operate on all the shifted vectors we need for a 64-byte chunk. Compare this to needing to produce a separate &lt;code&gt;prev1&lt;/code&gt;, &lt;code&gt;prev2&lt;/code&gt;, and &lt;code&gt;prev3&lt;/code&gt; for each 16-byte vector, which takes a total of 12 vector shifts for 64 bytes.&lt;/p&gt;
&lt;p&gt;Using this intuition, we can write a function which emulates the semantics of a vector shift by any compile-time known amount on normally-ordered vectors, but for interleaved vectors! This removes the restriction of only using this vector interleaving trick in circumstances where order didn&apos;t change&amp;lt;span id=&quot;shiftInterleavedElementsRight&quot;&amp;gt;.&amp;lt;/span&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fn shiftInterleavedElementsRight(
    vecs: [4]@Vector(16, u8),
    comptime amount: std.simd.VectorCount(@Vector(64, u8)),
    shift_in: std.meta.Child(@Vector(64, u8))
) [4]@Vector(16, u8) {
    var new_vecs = vecs;

    if ((amount &amp;amp; 1) == 1) {
        const n = std.simd.shiftElementsRight(new_vecs[3], 1, shift_in);
        new_vecs[3] = new_vecs[2];
        new_vecs[2] = new_vecs[1];
        new_vecs[1] = new_vecs[0];
        new_vecs[0] = n;
    }

    if ((amount &amp;amp; 2) == 2) {
        const n1 = std.simd.shiftElementsRight(new_vecs[3], 1, shift_in);
        const n0 = std.simd.shiftElementsRight(new_vecs[2], 1, shift_in);
        new_vecs[3] = new_vecs[1];
        new_vecs[2] = new_vecs[0];
        new_vecs[1] = n1;
        new_vecs[0] = n0;
    }

    const leftover_amt = amount &amp;gt;&amp;gt; 2;

    if (leftover_amt &amp;gt; 0) {
        new_vecs = .{
            std.simd.shiftElementsRight(new_vecs[0], leftover_amt, shift_in),
            std.simd.shiftElementsRight(new_vecs[1], leftover_amt, shift_in),
            std.simd.shiftElementsRight(new_vecs[2], leftover_amt, shift_in),
            std.simd.shiftElementsRight(new_vecs[3], leftover_amt, shift_in)
        };
    }

    return new_vecs;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Prefix sums&lt;/h2&gt;
&lt;p&gt;While interleaved vectors are a clear win for reducing the number of vector shifts required for our UTF8 use-case, even emulating a 64-byte vector shift by doing only a single 16-byte shift (!!), it doesn&apos;t always work out favorably.&lt;/p&gt;
&lt;p&gt;The prefix-sum algorithm includes a situation with worse performance for interleaved vectors relative to normal-ordered vectors:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fn prefixSum(vec_: @Vector(64, u8)) @Vector(64, u8) {
    var vec = vec_ + std.simd.shiftElementsRight(vec_, 1, 0);
    vec += std.simd.shiftElementsRight(vec, 2, 0);
    vec += std.simd.shiftElementsRight(vec, 4, 0);
    vec += std.simd.shiftElementsRight(vec, 8, 0);
    vec += std.simd.shiftElementsRight(vec, 16, 0);
    return vec + std.simd.shiftElementsRight(vec, 32, 0);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To compute the last two lines, where we want to shift by 16 and 32 respectively, each of those will require 4 vector shifts and 4 adds to simulate over interleaved vectors. &lt;a href=&quot;https://zig.godbolt.org/z/xWEoKq5cz&quot;&gt;However, with normally-ordered vectors, 0 instructions are necessary to shift by multiples of 16 (the vector length), and only 3 and 2 adds are needed respectively&lt;/a&gt; (because adding a vector of all zeroes is optimized away). See &lt;a href=&quot;https://zig.godbolt.org/z/nq99edxf9&quot;&gt;here&lt;/a&gt; for a playground showing the prefix-sum performed in interleaved space versus normal space.&lt;/p&gt;
&lt;p&gt;If we consider that each line of the &lt;code&gt;prefixSum&lt;/code&gt; function &quot;should&quot; take 4 vector shift (&lt;code&gt;ext&lt;/code&gt;) and 4 add instructions, the interleaved variant saves 5 vector shift instructions on the first two lines, and the normal version saves 8 vector shift instructions and 3 adds on the last two lines. That means the normal version takes 6 fewer instructions per iteration.&lt;/p&gt;
&lt;p&gt;However, using interleaved vectors has instruction-level-parallelism advantages that almost even it out. &lt;a href=&quot;https://zig.godbolt.org/z/f45WE4eTe&quot;&gt;According to LLVM-mca&lt;/a&gt;, the Apple M3 can do a prefix sum in interleaved space in ~14.86 cycles, whereas it can do it in normal (non-interleaved) space in ~12.87 cycles, a difference of ~1.99 cycles, despite the difference of 6 instructions.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;As shown above, the &lt;a href=&quot;../interleaved-vectors-on-arm/#movemask&quot;&gt;movemask&lt;/a&gt; and &lt;a href=&quot;../interleaved-vectors-on-arm/#unmovemask&quot;&gt;unmovemask&lt;/a&gt; routines can not only be emulated in interleaved space, but are more efficicent than the routines for vectors in normal space. &lt;a href=&quot;../interleaved-vectors-on-arm/#elementwise-shifts&quot;&gt;Elementwise-shifts&lt;/a&gt; are also more efficient when shifting only 1-3 slots left or right, but are less efficient when shifting by a multiple of 16.&lt;/p&gt;
&lt;p&gt;So next time you want to parse &lt;a href=&quot;https://github.com/simdutf/simdutf/issues/428&quot;&gt;utf8&lt;/a&gt;, &lt;a href=&quot;https://github.com/simdjson/simdjson&quot;&gt;JSON&lt;/a&gt;, or &lt;a href=&quot;https://github.com/Validark/Accelerated-Zig-Parser&quot;&gt;Zig&lt;/a&gt;, be sure to use interleaved vectors!&lt;/p&gt;
&lt;p&gt;‒ Validark&lt;/p&gt;
</content:encoded></item></channel></rss>