<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Maxim Zaks on Medium]]></title>
        <description><![CDATA[Stories by Maxim Zaks on Medium]]></description>
        <link>https://medium.com/@mzaks?source=rss-3c5928934a5e------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/0*cDlOmXgBwGamXnY9.jpeg</url>
            <title>Stories by Maxim Zaks on Medium</title>
            <link>https://medium.com/@mzaks?source=rss-3c5928934a5e------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Wed, 01 Jul 2026 03:43:04 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@mzaks/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[When “Magic” Becomes Explicit: Mojo’s Take on Metaprogramming]]></title>
            <link>https://mzaks.medium.com/when-magic-becomes-explicit-mojos-take-on-metaprogramming-61bcfafc5145?source=rss-3c5928934a5e------2</link>
            <guid isPermaLink="false">https://medium.com/p/61bcfafc5145</guid>
            <category><![CDATA[mojo]]></category>
            <category><![CDATA[compiler-design]]></category>
            <category><![CDATA[reflections]]></category>
            <category><![CDATA[swift]]></category>
            <category><![CDATA[programming-languages]]></category>
            <dc:creator><![CDATA[Maxim Zaks]]></dc:creator>
            <pubDate>Fri, 06 Feb 2026 11:33:54 GMT</pubDate>
            <atom:updated>2026-02-06T11:33:54.828Z</atom:updated>
            <content:encoded><![CDATA[<h4>What Swift Hides, Mojo Exposes</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*hsfXW9jlhEPrN3wn" /><figcaption>Photo by <a href="https://unsplash.com/@cherstve_pechivo?utm_source=medium&amp;utm_medium=referral">Liana S</a> on <a href="https://unsplash.com?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><p>Mojo is a new programming language by Chris Lattner, the same Chris Lattner who developed Swift, LLVM, MLIR, and tons of other cool stuff.</p><blockquote><em>Mojo means “a magical charm” or “magical powers.”<br></em><a href="https://docs.modular.com/mojo/faq#why-is-it-called-mojo"><em>https://docs.modular.com/mojo/faq#why-is-it-called-mojo</em></a></blockquote><p>Ironically, while Mojo’s name means “magical power,” one of its central design goals is to minimize compiler magic. Recent additions such as compile-time reflection and default trait method implementations reinforce this direction.</p><p>If you’ve worked with Swift, you know that when a type conforms to protocols like Stringable, Sendable, Hashable, or Equatable, the compiler can synthesize the conformance for you. In practice, this means the compiler generates derived implementations based on the conformances of the type’s stored properties. This is powerful, but also entirely compiler-driven and opaque to developers. The behavior is hard-coded for a limited set of standard protocols, and there is no general mechanism for user-defined protocols to opt into the same synthesis model. As a result, authors of custom protocols must require manual conformance implementations from their users.</p><p>In Mojo, this is different. Thanks to compile-time reflection and default trait method implementation, any trait can supply this magical behavior. Example:</p><pre>trait Hashable:<br>    fn __hash__[H: Hasher](self, mut hasher: H):<br>        comptime names = struct_field_names[Self]()<br>        comptime types = struct_field_types[Self]()<br>        @parameter<br>        for i in range(names.size):<br>            comptime T = types[i]<br>            _constrained_field_conforms_to[<br>                conforms_to(T, Hashable),<br>                Parent=Self,<br>                FieldIndex=i,<br>                ParentConformsTo=&quot;Hashable&quot;,<br>            ]()<br>            hasher.update(trait_downcast[Hashable](__struct_field_ref(i, self)))</pre><p>This feature is extremely powerful because it generalizes behavior that was previously restricted to compiler intrinsics and makes it available to all developers. However, to fully realize this model, another upcoming feature is needed: struct extensions. Struct extensions will allow developers to retroactively implement new traits for existing types, including those defined in the standard library. In practice, this enables library authors to introduce new abstractions and have pre-existing types participate in them without requiring modifications to the original type definitions.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=61bcfafc5145" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Was 2025 the year we all became 10x engineers?]]></title>
            <link>https://mzaks.medium.com/was-2025-the-year-we-all-became-10x-engineers-42c9c93ee082?source=rss-3c5928934a5e------2</link>
            <guid isPermaLink="false">https://medium.com/p/42c9c93ee082</guid>
            <category><![CDATA[ai-slop]]></category>
            <category><![CDATA[coding]]></category>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[generative-ai-tools]]></category>
            <dc:creator><![CDATA[Maxim Zaks]]></dc:creator>
            <pubDate>Wed, 17 Dec 2025 15:53:22 GMT</pubDate>
            <atom:updated>2025-12-17T15:53:22.343Z</atom:updated>
            <content:encoded><![CDATA[<h4>Or did we just become sloppy?</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Vq2KA_0yjoSLEHgnTYICgA.png" /></figure><p>This blog post was triggered by a discussion on LinkedIn, a YouTube video, and an audiobook. Well, okay, it is also that time of the year…</p><p>For me personally, 2025 kicked off with a new job in an exciting field: I started working on an AI coding assistant. The field is wild — tons of competition, even more uncertainty, and at the end of the day, as the German saying goes:</p><blockquote>Alle kochen nur mit Wasser</blockquote><p>Where the “Wasser” in this case is a handful of LLMs (services) from a few big companies. I will not go into detail about my journey in this space, I just want to emphasize that I am no stranger to AI-assisted coding. I even have insight into how the sausage is made, and I have used multiple tools throughout 2025.</p><p>As mentioned above, one of the triggers for this post was a YouTube video that resonated with me quite a bit, maybe because the author had a similar experience to mine: doing more with less has its limits.</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2FoOvl_htqhy8%3Ffeature%3Doembed&amp;display_name=YouTube&amp;url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DoOvl_htqhy8&amp;image=https%3A%2F%2Fi.ytimg.com%2Fvi%2FoOvl_htqhy8%2Fhqdefault.jpg&amp;type=text%2Fhtml&amp;schema=youtube" width="854" height="480" frameborder="0" scrolling="no"><a href="https://medium.com/media/0e8999b3a585221e88596fbbacf7407c/href">https://medium.com/media/0e8999b3a585221e88596fbbacf7407c/href</a></iframe><p>Doing more with less is the philosophy behind 10x engineering. And as the author of the video mentioned, after the honeymoon period, reality will hit you like a brick wall. Being sloppy does not pay off in the long run.</p><p>Don’t get me wrong: I do use LLMs for my work. They have become an important tool, but you need to understand that what an LLM does is very similar to a cargo cult. An LLM is trained on tons of code, and when you ask it to build something for you, it simply tries to replicate what it was trained on — without actual understanding the inner workings.</p><p>Here is an example from my current project. I asked ChatGPT about an intricacy of a publicly available specification. Here is an excerpt from its answer:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*z2mhK4wuYLb7U8ab-LC1vA.jpeg" /></figure><p>So far, so good. The model gave me an in-depth analysis and grounded it with a citation from the RFC, including a paragraph number. The problem is that the citation is hallucinated. The paragraph number is correct, the paragraph discusses the SCIM PATCH path specification, but there is no such sentence in it. The model generated a citation using appropriate language, looking absolutely legitimate.</p><p>After I confronted my assistant, it responded as follows:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*rlLp2Ntn_DCmjcVFNoY2Mw.jpeg" /></figure><p>Here we can see how masterful ChatGPT is at consulting, but is it reasonable to generate false citations just because most people interpret the spec differently? If a person did something like this, we would consider it fraudulent. Or is it the equivalent of everybody lying on their dating profile?</p><p>Now let’s go back to the claims of unlocking x times the productivity with AI tools. I see people writing that it takes them one or two days to implement a feature that previously took two to three weeks. And yes, if we think in person-days, they moved from 10–15 days to 1–2 days, which is a 10x speedup. So in a year, they should deliver over 100 features instead of 10.</p><p>How much trust has to be placed in our devoted consultant in order to keep such a pace? What happens if 10% of these features have a bug or undesired behaviour? How fast will you be able to fix a bug in the future that you only spent a couple of hours working on?</p><p>It is a known fact that in order to get good at something, we need to put a certain amount of hours into it. It is also a known fact that doing boring work is where our mind comes up with creative solutions. Going 10x faster than your normal speed means you have to use a 100% reliable tool for a 100% deterministic process, like using a calculator instead of doing math in your head, or digging a grave with an excavator instead of a shovel.</p><p>Speaking of digging a grave, you know what else is very dangerous in the x-times productivity debate? If you have a 10x engineer, how many of them do you need on a team? Do you need a team at all?</p><p>From a profit-oriented perspective, you do not. So it is okay to have only one person hammering out over 100 features a year, replacing a full team. That said, what is your bus factor in this scenario? Right… it is also 100%.</p><p>But fear not: the next person will be able to replace them using the same tool, right? Well, although the context window of an LLM is growing and it is legitimate to explore legacy codebases with the help of an LLM, it is still not enough to understand the why behind the decisions in a codebase. Although, if the previous person just vibe-coded their way to this point, they probably do not understand the why either.</p><p>In conclusion, I think the x-times productivity boost narrative should be considered harmful. Most of it is grounded in anecdotal evidence, novelty, and wishful thinking — sometimes even greed. AI tools are helpful and they are getting better, but we need to understand what sustainable throughput and team size should be based on long-term observations.</p><p>As the navy seal saying goes:</p><blockquote>Slow is smooth and smooth is fast</blockquote><p>Wish you all great holidays and a happy new year!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=42c9c93ee082" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[What do AI Agents and Retained GUI Frameworks Have in Common?]]></title>
            <link>https://medium.com/swlh/what-do-ai-agents-and-retained-gui-frameworks-have-in-common-4bd30498aef0?source=rss-3c5928934a5e------2</link>
            <guid isPermaLink="false">https://medium.com/p/4bd30498aef0</guid>
            <category><![CDATA[ai-agent]]></category>
            <category><![CDATA[agents]]></category>
            <category><![CDATA[mutable-state]]></category>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[llm-applications]]></category>
            <dc:creator><![CDATA[Maxim Zaks]]></dc:creator>
            <pubDate>Sat, 24 May 2025 14:23:01 GMT</pubDate>
            <atom:updated>2025-05-24T14:23:01.331Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*oeINvgPzMiQ6iVXpXOygLw.png" /></figure><p>No, this is not the start of a bad joke. Though it <em>could</em> be:</p><blockquote><em>“An AI agent and a GUI framework walk into a bar. The bartender says, ‘Why the long stack trace?’”</em></blockquote><p>But I digress.</p><p>If you’re not familiar with the term <strong>retained GUI framework</strong>, don’t worry — you’re in good company. Let’s break it down: in a retained-mode GUI, the UI state is <em>stored</em> and <em>managed</em> by the framework. When the data changes, the framework has to go, “Ah! The button needs to be blue now!” and update the UI accordingly. Sounds simple? Sure. Until you realize you’re suddenly debugging why your toggle button has trust issues and won’t commit to being toggled.</p><p>One of the <em>main pain points</em> of retained GUIs is state management. Keeping the UI and your actual data in sync is like trying to get toddlers to nap at the same time — possible in theory, but rarely achieved without chaos.</p><p>You’ve got to:</p><ul><li>Know when your data changes.</li><li>Notify the UI about it.</li><li>Hope the UI updates the right things.</li><li>Pray it didn’t break something else in the process.</li></ul><p>Now here’s the kicker: <strong>AI agents have the same problem.</strong></p><p>Wait… what? Isn’t AI just fancy autocomplete? “You type stuff in, it types stuff out” — that sort of deal?</p><p>Yes, for simple use cases. But AI <strong>agents</strong> are different beasts.</p><p>They’re called <em>agents</em> because they act. They don’t just respond — they <em>do</em>. They’re given tools (check out the MCP protocol if you want to dive deep), and they’re supposed to use them to perform tasks. Sometimes these tools just look up data or do basic computations. But sometimes… they change the world (or at least, your app’s state).</p><p>Now imagine a conversation-based flow. You ask the agent something, it runs a tool, then continues the conversation. Sounds fun — until the agent’s internal world is out of sync with reality.</p><p>Here’s what can happen:</p><ul><li>You asked for “the current temperature” five steps ago.</li><li>The agent called a tool and told you it was 20°C.</li><li>But then the tool <em>updated</em> something, or another agent came along (👀 looking at you, A2A protocol), and the temperature changed.</li><li>Now you’re talking about old data — and the agent doesn’t even know it’s outdated.</li></ul><p>This is where things get weirdly GUI-esque.</p><p>You need <strong>state awareness</strong>. Like in retained-mode GUIs, where widgets have to update when the data changes, agents need a way to know when something they believe is true… isn’t anymore.</p><p>So what do we do? Just… re-check everything all the time like a paranoid squirrel?</p><p>Maybe.</p><p>Or maybe we need to <strong>build in mechanisms for state change notification</strong>, just like GUI frameworks do. Imagine the MCP or A2A protocols being able to say:</p><blockquote><em>“Hey buddy, I just changed the temperature value. Might wanna update your internal map of the world.”</em></blockquote><p>That way, agents can respond intelligently to changes and avoid hallucinating outdated realities. You know, like a properly-behaved UI widget that actually listens when you tell it to change color. (Looking at <em>you</em>, custom toggle button from 2017.)</p><h3>TL;DR</h3><p>Retained GUIs and AI agents both suffer from the same fundamental challenge: <strong>managing and reacting to shared mutable state</strong>.</p><p>So next time you see an agent doing something dumb, remember — it’s not (always) the AI’s fault. Sometimes, it’s just trying to deal with state like the rest of us: in confusion, with outdated facts, and no idea that everything changed two messages ago.</p><p>Welcome to the future. It’s weird, and full of shared mutable state.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=4bd30498aef0" width="1" height="1" alt=""><hr><p><a href="https://medium.com/swlh/what-do-ai-agents-and-retained-gui-frameworks-have-in-common-4bd30498aef0">What do AI Agents and Retained GUI Frameworks Have in Common?</a> was originally published in <a href="https://medium.com/swlh">The Startup</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[ CrazyString]]></title>
            <link>https://mzaks.medium.com/crazystring-c204eee9d3b7?source=rss-3c5928934a5e------2</link>
            <guid isPermaLink="false">https://medium.com/p/c204eee9d3b7</guid>
            <category><![CDATA[utf-8]]></category>
            <category><![CDATA[unicode]]></category>
            <category><![CDATA[datastrucutre]]></category>
            <category><![CDATA[mojo]]></category>
            <category><![CDATA[programming-languages]]></category>
            <dc:creator><![CDATA[Maxim Zaks]]></dc:creator>
            <pubDate>Sun, 25 Aug 2024 07:48:48 GMT</pubDate>
            <atom:updated>2024-08-25T07:48:48.167Z</atom:updated>
            <content:encoded><![CDATA[<h4>An experiment of utilizing small string optimization and efficient Unicode code point indexing</h4><p>For the last year I did a bunch of experiments with the 🔥 Mojo programming language.</p><p>One of the latest experiments is CrazyString. Yesterday I recorded a video, where I explain some of the concepts I employed in this experiment.</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2F31Ka0bUTo2U%3Ffeature%3Doembed&amp;display_name=YouTube&amp;url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D31Ka0bUTo2U&amp;image=https%3A%2F%2Fi.ytimg.com%2Fvi%2F31Ka0bUTo2U%2Fhqdefault.jpg&amp;key=a19fcc184b9711e1b4764040d3dc5c07&amp;type=text%2Fhtml&amp;schema=youtube" width="854" height="480" frameborder="0" scrolling="no"><a href="https://medium.com/media/912ef26f001e8fbc7fa684d9162060b5/href">https://medium.com/media/912ef26f001e8fbc7fa684d9162060b5/href</a></iframe><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=c204eee9d3b7" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Poor persons package management in Mojo]]></title>
            <link>https://mzaks.medium.com/poor-persons-package-management-in-mojo-8671aa6e420a?source=rss-3c5928934a5e------2</link>
            <guid isPermaLink="false">https://medium.com/p/8671aa6e420a</guid>
            <category><![CDATA[package-management]]></category>
            <category><![CDATA[developer-tools]]></category>
            <category><![CDATA[bash-script]]></category>
            <category><![CDATA[mojo]]></category>
            <dc:creator><![CDATA[Maxim Zaks]]></dc:creator>
            <pubDate>Tue, 31 Oct 2023 11:41:41 GMT</pubDate>
            <atom:updated>2023-10-31T11:41:41.357Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*sc5G1iPqCiD9FuBn" /><figcaption>Photo by <a href="https://unsplash.com/@purzlbaum?utm_source=medium&amp;utm_medium=referral">Claudio Schwarz</a> on <a href="https://unsplash.com?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><p>As you might have noticed, if you followed my last few posts, I am currently exploring the new programming language called Mojo.</p><p>I created multiple small repos, where I implement few fundamental elements, like <a href="https://github.com/mzaks/mojo-sort">sorting</a> and <a href="https://github.com/mzaks/mojo-hash">hashing</a> functions, a <a href="https://github.com/mzaks/mojo-hash/tree/main/hashmap">hash map</a> and some <a href="https://github.com/mzaks/mojo-trees">tree</a> data structures and a <a href="https://github.com/mzaks/mojo-csv">CSV builder and parser</a>.</p><p>Specifically the CSV package is quite useful, as I like to collect the results of a benchmark in a tabular form and CSV is the simplest and most portable format for it.</p><p>So it makes sense to reference the CSV package from other projects in order to use it in benchmark scripts. Sadly Mojo does not provide any package management tools yet. This is why I decided to invest a few hours into a simple solution until the official package manager makes its debut.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/a079616a63394a6ef3b41d0006f32181/href">https://medium.com/media/a079616a63394a6ef3b41d0006f32181/href</a></iframe><p>What you see above is a bash script which lets you checkout specific folders from a git repository.</p><pre>check_out_remote_module &quot;https://github.com/mzaks/mojo-csv&quot; &quot;csv&quot;</pre><p>Here we say that we would like to checkout the csv folder which represents a Mojo module from a Github repository. The csv folder will end up in your root project folder, which will make it accessible in your code. The script uses sparse-checkout, which means that only the desired folders are checked out. This is useful as in the case of mojo-csv, the repo also contains large csv files for benchmarking, which we don’t want to check out into our projects.</p><p>After the code is checked out, the script generates a .checkoutinfo file in every folder, which contains a timestamp the source URL and the path to the folder:</p><pre>Sun Oct 29 20:07:58 CET 2023<br>URL: https://github.com/mzaks/mojo-csv<br>Path: csv</pre><p>If a repo contains multiple modules, it is possible to check out multiple folders:</p><pre>check_out_remote_module &quot;https://github.com/mzaks/mojo-trees&quot; &quot;fiby_tree&quot; &quot;left_child_right_sibling=lcrs_tree&quot;</pre><p>As you can see above, I am checking out fiby_tree and left_child_right_sibling from mojo-trees repo. In case of left_child_right_sibling module I also decided to rename the module, so I append =lcrs_tree to the source folder name, which means that I want the target folder to be named lcrs_tree.</p><p>It is also possible to pick a nested module as you can see it in following example:</p><pre>check_out_remote_module &quot;https://github.com/tairov/llama2.mojo&quot; &quot;read/libc/stdio=file_io&quot;</pre><p>And last but not least, it is also very simple to pick a module form a specific branch, or tag as you can see below:</p><pre>check_out_remote_module &quot;-b v0.2.0 https://github.com/gabrieldemarmiesse/mojo-stdlib-extensions&quot; &quot;stdlib_extensions/builtins=list&quot;</pre><p>While I was implementing this solution, I had some ideas for an official Mojo package manager, but this is a topic for another blog post.</p><p>Please let me know, if you find this script useful and if there is a need for another blog post to explain things like how to structure your repo to increase module accessibility.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=8671aa6e420a" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Faster prefix sum computation with SIMD and Mojo]]></title>
            <link>https://mzaks.medium.com/faster-prefix-sum-computation-with-simd-and-mojo-39bdc25e49b3?source=rss-3c5928934a5e------2</link>
            <guid isPermaLink="false">https://medium.com/p/39bdc25e49b3</guid>
            <category><![CDATA[algorithms]]></category>
            <category><![CDATA[simd]]></category>
            <category><![CDATA[mojo]]></category>
            <category><![CDATA[performance-optimization]]></category>
            <dc:creator><![CDATA[Maxim Zaks]]></dc:creator>
            <pubDate>Thu, 19 Oct 2023 11:16:02 GMT</pubDate>
            <atom:updated>2023-10-19T11:16:02.386Z</atom:updated>
            <content:encoded><![CDATA[<p>Since a couple of months I am experimenting with the new programming language called <a href="https://www.modular.com/mojo">Mojo</a>. The language is still in a very early stage, so is the standard library. In order to try things out, I currently concentrate on very basic functionality like <a href="https://github.com/mzaks/mojo-hash">hash functions</a>, <a href="https://github.com/mzaks/mojo-sort">sorting</a> and some <a href="https://github.com/mzaks/mojo-trees">tree data structures</a>.</p><p>While I was working on sorting, I implemented counting and radix sort, which incorporate prefix sum algorithm. A prefix sum algorithm is generally very easy to implement, but I found <a href="https://en.algorithmica.org/hpc/algorithms/prefix/">an article</a>, where the author claims to make it couple of times faster by employing SIMD operations.</p><p>One of the key features of Mojo is first class SIMD support, so I decided to go down the rabbit hole and check if I could implement a faster prefix sum algorithm in Mojo.</p><h4>Lets start from the beginning, what is a <a href="https://en.wikipedia.org/wiki/Prefix_sum">prefix sum</a>?</h4><p>For an in depth understanding I think it is best to follow the Wikipedia link above, but actually the simple implementation of the algorithm in Mojo is self explanatory:</p><pre>var element = array[0]<br>for i in range(1, len(array)):<br>    array[i] += element<br>    element = array[i]</pre><p>So an element at index i in the array is equal to itself plus element at index i — 1</p><p>Given an array: 1, 1, 1, 1, 1, 1, 1, 1</p><p>A prefix sum of this array is: 1, 2, 3, 4, 5, 6, 7, 8</p><p>Which is computed by 7 additions:</p><pre>1, <br>(1 + 1) = 2, <br>(2 + 1) = 3, <br>(3 + 1) = 4, <br>(4 +1) = 5, <br>(5 + 1) = 6, <br>(6 + 1) = 7, <br>(7 + 1 ) = 8</pre><p>For me, it was hard to imagine how one would implement it with SIMD as every iteration is based on the previous one. But there is a way:</p><pre>  1, 1, 1, 1, 1, 1, 1, 1<br>+ 0, 1, 1, 1, 1, 1, 1, 1<br>= 1, 2, 2, 2, 2, 2, 2, 2<br>+ 0, 0, 1, 2, 2, 2, 2, 2<br>= 1, 2, 3, 4, 4, 4, 4, 4<br>+ 0, 0, 0, 0, 1, 2, 3, 4 <br>= 1, 2, 3, 4, 5, 6, 7, 8</pre><p>So it is possible to compute a prefix sum of an 8 element vector in 3 (log2(8)) steps , where we combine a vector shift right with a vector addition operation. In Mojo the code looks as following:</p><pre>var v1 = SIMD[DType.uint8, 8](1, 1, 1, 1, 1, 1, 1, 1)<br>print(v1) # [1, 1, 1, 1, 1, 1, 1, 1]<br>v1 += v1.shift_right[1]()<br>print(v1) # [1, 2, 2, 2, 2, 2, 2, 2]<br>v1 += v1.shift_right[2]()<br>print(v1) # [1, 2, 3, 4, 4, 4, 4, 4]<br>v1 += v1.shift_right[4]()<br>print(v1) # [1, 2, 3, 4, 5, 6, 7, 8]</pre><p>On the first line we define a SIMD vector to be 8 elements wide with numeric values of type uint8. Then we print the vector and proceed with shift right and assigned addition to mutate the vector 3 times and print the (intermediate/final) results along the way.</p><p>You might be surprised by the syntax of the shift_right method call. The number of places we want to shift is passed in square brackets instead of parentheses. This means that the value is passed not at runtime, but at compile time, which also means that the value needs to be known at compile time. For more info on this topic please consult <a href="https://docs.modular.com/mojo/programming-manual.html#parameterization-compile-time-metaprogramming">Mojo Programming manual</a>.</p><h4>How can we compute a prefix sum for a generic array whose size is only known at run time?</h4><p>In order to do this we need to break down the runtime known array into chunks and perform the static compile defined operations on those chunks.</p><p>In order to perform the SIMD prefix sum on chunks we actually need to change the algorithm a bit.</p><p>Say we still have an array: 1, 1, 1, 1, 1, 1, 1, 1</p><p>But now we want to break it down in two 4 element chunks. The computation should look as following:</p><pre>First chunk:<br>  1, 1, 1, 1<br>+ 0, 1, 1, 1<br>= 1, 2, 2, 2<br>+ 0, 0, 1, 2<br>= 1, 2, 3, 4<br>+ 0, 0, 0, 0<br>= 1, 2, 3, 4<br><br>Second chunk:<br>  1, 1, 1, 1<br>+ 0, 1, 1, 1<br>= 1, 2, 2, 2<br>+ 0, 0, 1, 2<br>= 1, 2, 3, 4<br>+ 4, 4, 4, 4<br>= 5, 6, 7, 8</pre><p>Which is reflected in following Mojo code:</p><pre>var v1 = SIMD[DType.uint8, 4](1, 1, 1, 1)<br>var v2 = SIMD[DType.uint8, 4](1, 1, 1, 1)<br>print(v1) # [1, 1, 1, 1]<br>v1 += v1.shift_right[1]()<br>print(v1) # [1, 2, 2, 2]<br>v1 += v1.shift_right[2]()<br>print(v1) # [1, 2, 3, 4]<br>v1 += 0<br>print(v1) # [1, 2, 3, 4]<br>print(v2) # [1, 1, 1, 1]<br>v2 += v2.shift_right[1]()<br>print(v2) # [1, 2, 2, 2]<br>v2 += v2.shift_right[2]()<br>print(v2) # [1, 2, 3, 4]<br>v2 += v1[3]<br>print(v2) # [5, 6, 7, 8]</pre><p>As you can see above, we need to carry over the last value from previous chunk in order to increment all vector values in current chunk by it.</p><p>So given the chunk of size n, we need to perform:</p><pre>result += result.shift_right[1 &lt;&lt; i]()</pre><p>log2(n) times, where i is a number between 0 and log2(n)</p><p>My first instinct was to put the above statement in a for loop:</p><pre>var v1 = SIMD[DType.uint8, 4](1, 1, 1, 1)<br>for i in range(0, 2):<br>    v1 += v1.shift_right[1 &lt;&lt; i]()<br>print(v1)</pre><p>This however will not compile. As I already mentioned, the shift_right method expects a compile time known value, an i in the for loop is a runtime value (although the range is compile time known) But no worries, the Mojo standard library has our backs. It provides a function which allows us to perform loop unrolling in a more functional way:</p><pre>from algorithm import unroll<br><br>fn prefix_sum_on_chunk(inout v1: SIMD[DType.uint8, 4], carry_over: UInt8):<br>    @parameter<br>    fn add[i: Int]():<br>        v1 += v1.shift_right[1 &lt;&lt; i]()<br>    unroll[2, add]()<br>    v1 += carry_over<br><br>var v1 = SIMD[DType.uint8, 4](1, 1, 1, 1)<br>print(v1) # [1, 1, 1, 1]<br>prefix_sum_on_chunk(v1, 0)<br>print(v1) # [1, 2, 3, 4]</pre><p>This way the compiler emits code which is similar to what we wrote above (without runtime branching and condition checks)</p><h4>Next question, how big should the chunks be?</h4><p>This depends on the type of the element in the array (how much bytes one elements occupies) and the hardware we are running the algorithm on. Standard library does provide an <a href="https://docs.modular.com/mojo/stdlib/autotune/autotuning.html">autotune function</a> which should automate this kind of decision, but to be honest with you, I did some manual tuning and come to the conclusion that on my laptop (11th Gen Intel(R) Core(TM) i7–1165G7 @ 2.80GHz) following vector width performs best:</p><ul><li>1 byte elements (int8, uint8), with 256 wide SIMD vector</li><li>2 byte elements (int16, uint16, float16), with 128 wide SIMD vector</li><li>4 byte elements (int32, uint32, float32), with 64 wide SIMD vector</li><li>8 byte elements (int64, uint64, float64), with 32 wide SIMD vectors</li></ul><p>This implies that, if the array is smaller then the preferred vector width, or not an exact multiple of the preferred vector width, we need to compute the rest with a smaller vector width.</p><p>To make it clear: say we have an array of uint64 with 80 elements in it. As I mentioned before we prefer to take chunks of 32 elements for uint64 arrays, which means that we can compute the first 64 elements by taking two chunks with 32 wide SIMD vector and then we would need to reduce the vector size to 16 in order to compute the rest.</p><p>You can find the complete <a href="https://github.com/mzaks/mojo-prefix-sum/blob/main/prefix_sum.mojo">simd_prefix_sum</a> implementation if you follow the link.</p><p>Given my previous explanation you should be able to follow along the code. That said, please don’t hesitate to write a comment if you have questions or suggestions.</p><p>Last but not least I would like to talk about runtime characteristics of the scalar and SIMD prefix sum functions, but first another disclaimer.</p><p>After I implemented the SIMD prefix sum and pushed it to the <a href="https://github.com/mzaks/mojo-prefix-sum">GitHub repository</a>, I announced it on the Mojo Discord server, where a user pointed out, that there is already a <a href="https://docs.modular.com/mojo/stdlib/algorithm/reduction.html#cumsum">prefix sum function</a> in standard library, which I missed. So for the benchmark comparison I included the runtime characteristics of the std function as well.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*V7lz-85V6VjssyZySJ4aSw.png" /><figcaption>Table and chart based on <a href="https://github.com/mzaks/mojo-prefix-sum/blob/main/prefix_sum.csv">benchmark results</a></figcaption></figure><p>The table and chart above shows that the SIMD prefix sum takes from 0.03 to 0.2 nanoseconds per array element to compute dependent on the element and array size, where the scalar prefix sum is very stable at around 0.5 nanoseconds per element. The SIMD speedup is between 2.5x and 15x which is quite great. The results also shows that there is something strange going on with the prefix sum function in the standard library. It is some times comparable with my SIMD implementation, but in some cases, slower than the scalar prefix sum.</p><p>You can find the code I used for benchmarks <a href="https://github.com/mzaks/mojo-prefix-sum/blob/main/std_cumsum_benchmark.mojo">here</a>.</p><p>Thank you for reading and leave a clap or two if you will.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=39bdc25e49b3" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[ FibyTree vs.  Set and SortedSet]]></title>
            <link>https://pub.aimind.so/fibytree-vs-set-and-sortedset-7b4e6b56cac8?source=rss-3c5928934a5e------2</link>
            <guid isPermaLink="false">https://medium.com/p/7b4e6b56cac8</guid>
            <category><![CDATA[data-structures]]></category>
            <category><![CDATA[benchmark]]></category>
            <category><![CDATA[mojo]]></category>
            <category><![CDATA[python]]></category>
            <dc:creator><![CDATA[Maxim Zaks]]></dc:creator>
            <pubDate>Thu, 24 Aug 2023 04:22:36 GMT</pubDate>
            <atom:updated>2023-08-28T15:08:46.387Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*Wvyklb1KgH_YnAOr" /><figcaption>Photo by <a href="https://unsplash.com/@kellysikkema?utm_source=medium&amp;utm_medium=referral">Kelly Sikkema</a> on <a href="https://unsplash.com?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><p>Couple of days ago, I wrote an <a href="https://medium.com/p/bd7f8775d815">article about FibyTree</a>, a data structure I designed and implemented in Mojo. Today I would like to reveal some benchmarks.</p><p>Words of caution. This benchmarks are preliminary. At current point in time Mojo code runs only in a Jupyter Notebook, so I run Mojo and Python code in the Jupyter Notebook. This is also the first performance evaluation I do on FibyTree. I did design the data structure with performance in mind, but I did not have the time to think about and try out optimisations. BTW if you have ideas, how I can improve FibyTree, please let me know!</p><p>For the benchmark I chose seven set operations:</p><ul><li>Insert an element to the set</li><li>Check if an element is in the set</li><li>Delete an element from the set</li><li>Build a <a href="https://en.wikipedia.org/wiki/Set_(mathematics)#/media/File:Venn0111.svg">union</a> of two sets</li><li>Build an <a href="https://en.wikipedia.org/wiki/Set_(mathematics)#/media/File:Venn0001.svg">intersection</a> of two sets</li><li>Build a <a href="https://en.wikipedia.org/wiki/Set_(mathematics)#/media/File:Venn0100.svg">difference</a> of two sets</li><li>Build a <a href="https://en.wikipedia.org/wiki/Set_(mathematics)#/media/File:Venn0110.svg">symmetric difference</a> of two sets</li></ul><p>I chose ten different sizes for the sets (10, 100, 300, 500, 1K, 3K, 9K, 15K, 30K and 50K) in order to understand how the size correlates with performance, specifically when it comes to a tree based data structure this is important.</p><p>The time unit through the benchmarks is nano seconds.</p><h3>Insert an element to the set</h3><p>Here is the Python script I use to populate the Set/SortedSet:</p><pre>def perf_test_random_add_p(size, min=-30000, max=30000, sorted=True):<br>    total = 0<br><br>    s = SortedSet() if sorted else set()<br>    for i in range(size):<br>        v = random.randint(min, max)<br>        tik = time.time_ns()<br>        s.add(v)<br>        tok = time.time_ns()<br>        total += (tok - tik)<br><br>    return total / size<br><br>def perf_test_ordered_add_p(size, sorted=True):<br>    total = 0<br>    s = SortedSet() if sorted else set()<br>    tik = time.time()<br>    for i in range(size):<br>        tik = time.time_ns()<br>        s.add(i)<br>        tok = time.time_ns()<br>        total += (tok - tik)<br>        <br>    return total / size</pre><p>As you can see I am measuring only the time it takes to add an element to a set. I am also considering two possibilities:</p><ul><li>Adding random values to a set</li><li>Adding elements in ascending order</li></ul><p>I did this distinction mainly because adding sorted elements to a binary search tree is the worst case scenario, but I was also surprised to see that Python Set implementation also has quite a preference.</p><p>Here is the corresponding Mojo code:</p><pre>fn perf_test_random_add(size: Int, min: Int = -30000, max: Int = 30000) -&gt; Float64:<br>    var total = 0<br>    <br>    var tik = now()<br>    var tok = now()<br>    var f = fiby()<br>    <br>    for _ in range(size):<br>        let i = random_si64(min, max).to_int()<br>        tik = now()<br>        f.add(i)<br>        tok = now()<br>        total += (tok - tik)<br>    <br>    return total / size<br>    <br>fn perf_test_ordered_add(size: Int) -&gt; Float64:<br>    var total = 0<br>    var tik = now()<br>    var f = fiby()<br>    var tok = now()<br>    total += tok - tik<br>    for i in range(size):<br>        tik = now()<br>        f.add(i)<br>        tok = now()<br>        total += (tok - tik)<br>        <br>    tik = now()<br>    f.balance()<br>    tok = now()<br>    total += (tok - tik)<br>    <br>    return total / size</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*qqDszvabsi9hYhTSfDPatA.png" /></figure><p>As I mentioned before, it was very surprising for me to see that adding elements in ascending order to a Python Set is so much faster then adding random values. Generally adding random elements to a FibyTree seems to have very nice performance characteristics. Through the randomness the balancing does not have to kick in that often and average difference per insert between 100 elements and 50K elements is just 1.5x.</p><p>As expected adding elements in ascending order to FibyTree follows an exponential growth, just because we need to balance the tree so often.</p><p>Adding random elements to FibyTree up to 10K of size seems to be about <strong>10x</strong> more efficient then adding to Python Set or SortedSet.</p><h3>Check if an element is in the set</h3><p>Here is the Python code, where we measure the time to call __contains__ method:</p><pre>def perf_test_contains_p(size, sorted=True):<br>    total = 0<br><br>    s = SortedSet() if sorted else set()<br>    for i in range(size):<br>        s.add(random.randint(-size, size))<br>    <br>    for i in range(size):<br>        tik = time.time_ns()<br>        r = s.__contains__(i)<br>        tok = time.time_ns()<br>        total += (tok - tik)<br><br>    return total / size</pre><p>And it’s Mojo counterpart:</p><pre>fn perf_test_contains(size: Int, inout found: Int) -&gt; Float64:<br>    var f = fiby()<br>    for _ in range(size):<br>        let i = random_si64(-size, size).to_int()<br>        f.add(i)<br>    <br>    var total = 0<br>    <br>    var tik = now()<br>    var tok = now()<br><br>    var res = DynamicVector[Bool](size)<br>    for i in range(size):<br>        tik = now()<br>        let r = f.__contains__(i)<br>        tok = now()<br>        res.push_back(r)<br>        total += (tok - tik)<br>    <br>    var count = 0<br>    for i in  range(len(res)):<br>        if res[i]:<br>            count += 1<br>    found = count<br>    <br>    return total / size</pre><blockquote>There is also perf_test_contains_on_balanced function, where we call f.balance() after all elements are added.</blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*dCfYgyXGj4u3ZLElqaVDCQ.png" /></figure><p>As we can see, the search in FibyTree gets constantly slower with bigger set. That said finding elements in a balanced FibyTree is<strong> ~5x</strong> fastert than SortedSet and about <strong>3x</strong> faster than Set.</p><h3>Delete an element from the set</h3><p>The operation is quite similar to contains, as we first need to find the element and then delete it.</p><p>Bellow you can find corresponding Python and Mojo code:</p><pre>def perf_test_delete_p(size, sorted=True):<br>    total = 0<br><br>    s = SortedSet() if sorted else set()<br>    for i in range(size):<br>        s.add(random.randint(-size, size))<br>    <br>    for i in range(size):<br>        tik = time.time_ns()<br>        r = s.discard(i)<br>        tok = time.time_ns()<br>        total += (tok -tik)<br><br>    return total / size</pre><pre>fn perf_test_delete(size: Int, inout found: Int) -&gt; Float64:<br>    var f = fiby()<br>    for _ in range(size):<br>        let i = random_si64(-size, size).to_int()<br>        f.add(i)<br>    <br>    var total = 0<br>    <br>    var tik = now()<br>    var tok = now()<br><br>    var res = DynamicVector[Bool](size)<br>    for i in range(size):<br>        tik = now()<br>        let r = f.delete(i)<br>        tok = now()<br>        res.push_back(r)<br>        total += (tok - tik)<br>    <br>    var count = 0<br>    for i in  range(len(res)):<br>        if res[i]:<br>            count += 1<br>    found = count<br>    <br>    return total / size</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*5zmyTASJdHLxUspTBpaiBA.png" /></figure><p>Balanced FibyTree is about <strong>4x</strong> faster than Set and whooping <strong>20–30x</strong> faster than SortedSet. It’s also interesting to notice that delete operation in FibyTree is on average faster then contains, which kind of make sense as the tree get smaller through the process. This is however not the case for Python Set.</p><h3>Build a union of two sets</h3><p>For this operation we create two random sets and apply the union operation. The time measured for operation execution is divided by the set size.</p><p>Python code:</p><pre>def perf_test_union_p(size, sorted=True):<br>    total = 0<br><br>    s1 = SortedSet() if sorted else set()<br>    s2 = SortedSet() if sorted else set()<br>    for i in range(size):<br>        s1.add(random.randint(-size, size))<br>        s2.add(random.randint(-size, size))<br>    <br>    tik = time.time_ns()<br>    s3 = s1.union(s2)<br>    tok = time.time_ns()<br><br>    return (tok - tik) / size</pre><p>Mojo code:</p><pre>fn perf_test_union(size: Int) -&gt; Float64:<br>    var f1 = fiby()<br>    var f2 = fiby()<br>    for _ in range(size):<br>        let i = random_si64(-size, size).to_int()<br>        f1.add(i)<br>        f2.add(i)<br>    <br>    let tik = now()<br>    f1.union_inplace(f2)<br>    let tok = now()<br>    <br>    return (tok - tik) / Float64(size)</pre><blockquote>We use union_inplace method in Mojo, which does not create a new instance but mutates the tree in-place, because of a bug I stumbled upon and reported in Mojo compiler. From performance point of view this should not make a big difference though.</blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*eqTDnMT_bLVH5oA6884jyg.png" /></figure><p>The growth of execution time for FibyTree seems to be quite linear based on the set size, where Set and SortedSet have a bit of a degenerating performance. It also shows that Set is <strong>1–7x</strong> slower than FibyTree and SortedSet 6<strong>–12x</strong> slower, with a spike of 26<strong>x</strong> for 10 elements set.</p><h3>Build an intersection of two sets</h3><p>Similar procedure as for union, Python and Mojo code below:</p><pre>def perf_test_intersection_p(size, sorted=True):<br>    total = 0<br><br>    s1 = SortedSet() if sorted else set()<br>    s2 = SortedSet() if sorted else set()<br>    for i in range(size):<br>        s1.add(random.randint(-size, size))<br>        s2.add(random.randint(-size, size))<br>    <br>    tik = time.time_ns()<br>    s3 = s1.intersection(s2)<br>    tok = time.time_ns()<br><br>    return (tok - tik) / size</pre><pre>fn perf_test_intersection(size: Int) -&gt; Float64:<br>    var f1 = fiby()<br>    var f2 = fiby()<br>    for _ in range(size):<br>        let i = random_si64(-size, size).to_int()<br>        f1.add(i)<br>        f2.add(i)<br>    <br>    let tik = now()<br>    f1.intersection_inplace(f2)<br>    let tok = now()<br>    <br>    return (tok - tik) / Float64(size)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*YvpiVqSTjH7jau2T9cqUMg.png" /></figure><p>It seems like Python Set has a very efficient way to compute intersection, at least for smaller sets, the larger sets are still ~2<strong>x</strong> slower than FibyTree. Sorted Set is 3–5x slower, with a spike of <strong>17x</strong> for 10 elements set. Not sure what is happening there but the spike was consistent throughout multiple runs.</p><h3>Build a difference of two sets</h3><p>Similar procedure, Python and Mojo code below:</p><pre>def perf_test_difference_p(size, sorted=True):<br>    total = 0<br><br>    s1 = SortedSet() if sorted else set()<br>    s2 = SortedSet() if sorted else set()<br>    for i in range(size):<br>        s1.add(random.randint(-size, size))<br>        s2.add(random.randint(-size, size))<br>    <br>    tik = time.time_ns()<br>    s3 = s1.difference(s2)<br>    tok = time.time_ns()<br><br>    return (tok - tik) / size</pre><pre>fn perf_test_difference(size: Int) -&gt; Float64:<br>    var f1 = fiby()<br>    var f2 = fiby()<br>    for _ in range(size):<br>        let i = random_si64(-size, size).to_int()<br>        f1.add(i)<br>        f2.add(i)<br>    <br>    let tik = now()<br>    f1.difference_inplace(f2)<br>    let tok = now()<br>    <br>    return (tok - tik) / Float64(size)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Fan2Vn7Y-N8gomEH6W1daw.png" /></figure><p>We observe Set being <strong>1–5x</strong> slower and SortedSet <strong>5–9x</strong> slower with a <strong>19x</strong> spike for 10 elements set.</p><h3>Build a symmetric difference of two sets</h3><p>Similar procedure, Python and Mojo code below:</p><pre>def perf_test_symmetric_difference_p(size, sorted=True):<br>    total = 0<br><br>    s1 = SortedSet() if sorted else set()<br>    s2 = SortedSet() if sorted else set()<br>    for i in range(size):<br>        s1.add(random.randint(-size, size))<br>        s2.add(random.randint(-size, size))<br>    <br>    tik = time.time_ns()<br>    s3 = s1.symmetric_difference(s2)<br>    tok = time.time_ns()<br><br>    return (tok - tik) / size</pre><pre>fn perf_test_symmetric_difference(size: Int) -&gt; Float64:<br>    var f1 = fiby()<br>    var f2 = fiby()<br>    for _ in range(size):<br>        let i = random_si64(-size, size).to_int()<br>        f1.add(i)<br>        f2.add(i)<br>    <br>    let tik = now()<br>    f1.symmetric_difference_inplace(f2)<br>    let tok = now()<br>    <br>    return (tok - tik) / Float64(size)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*p7O7hhnS_x_f4MfvTk9ktA.png" /></figure><p>We observe Set being <strong>1–6x</strong> slower and SortedSet 7<strong>–11x</strong> slower with a <strong>20x</strong> spike for 10 elements set.</p><p>All in all, I think FibyTree is a viable data structure to investigate further. As I mention in the beginning of the article, this benchmarks can be seen as preliminary, but they already establish some baselines we can expect from the data structure. As next steps, I will consider, if there are any vectorisation and parallelisation techniques I could utilise to make the operations run faster and maybe reducing some memory management overhead by utilising stack allocations and direct heap allocations (dropping DynamicVector in favour of direct memory allocation and manual capacity management).</p><h3>Benchmark results summarised</h3><h4><strong>Insert an element to the set:</strong></h4><blockquote>Set: 4–13x</blockquote><blockquote>SortedSet 6–13x</blockquote><h4><strong>Check if an element is in the set:</strong></h4><blockquote>Set: 3–4x, 9x spike for 10 elements set</blockquote><blockquote>SortedSet: 4–6x, 10x spike for 10 elements set</blockquote><h4><strong>Delete an element from the set:</strong></h4><blockquote>Set: 4x</blockquote><blockquote>SortedSet: 14–36x</blockquote><h4><strong>Build a union of two sets:</strong></h4><blockquote>Set: 1–7x</blockquote><blockquote>SortedSet: 6–12x, 26x spike for 10 elements set</blockquote><h4><strong>Build an intersection of two sets:</strong></h4><blockquote>Set: 1–2x</blockquote><blockquote>SortedSet: 3–5x, 17x spike for 10 elements set</blockquote><h4><strong>Build a difference of two sets:</strong></h4><blockquote>Set: 1–5x</blockquote><blockquote>SortedSet: 5–9x, 19x spike for 10 elements set</blockquote><h4><strong>Build a symmetric difference of two sets:</strong></h4><blockquote>Set: 1–6x</blockquote><blockquote>SortedSet: 7–11x, 20x spike for 10 elements set</blockquote><h4>A Message from AI Mind</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/250/0*5Wm7sOfTpe5DEbhg.gif" /></figure><p>Thanks for being a part of our community! Before you go:</p><ul><li>👏 Clap for the story and follow the author 👉</li><li>📰 View more content in the <a href="https://pub.aimind.so/">AI Mind Publication</a></li><li>🧠 Improve your <a href="https://www.aimind.so/prompt-generator?utm_source=pub&amp;utm_medium=message">AI prompts effortlessly and FREE</a></li><li><strong>🧰 Discover </strong><a href="https://www.aimind.so/?utm_source=pub&amp;utm_medium=message"><strong>Intuitive AI Tools</strong></a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=7b4e6b56cac8" width="1" height="1" alt=""><hr><p><a href="https://pub.aimind.so/fibytree-vs-set-and-sortedset-7b4e6b56cac8">🔥 FibyTree vs. 🐍 Set and SortedSet</a> was originally published in <a href="https://pub.aimind.so">AI Mind</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[A high level introduction to FibyTree]]></title>
            <link>https://pub.aimind.so/a-high-level-introduction-to-fibytree-bd7f8775d815?source=rss-3c5928934a5e------2</link>
            <guid isPermaLink="false">https://medium.com/p/bd7f8775d815</guid>
            <category><![CDATA[binary-tree]]></category>
            <category><![CDATA[data-structures]]></category>
            <category><![CDATA[mojo]]></category>
            <dc:creator><![CDATA[Maxim Zaks]]></dc:creator>
            <pubDate>Sat, 19 Aug 2023 18:17:49 GMT</pubDate>
            <atom:updated>2023-08-31T14:44:37.296Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*MHzQl086WXZ32ViI" /><figcaption>Photo by <a href="https://unsplash.com/@toddquackenbush?utm_source=medium&amp;utm_medium=referral">Todd Quackenbush</a> on <a href="https://unsplash.com?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><p>FibyTree is an optionally fully balanced and complete implicit binary search tree, which has the same semantics as a sorted set. I did a reference implementation of this data structure in Mojo, but fear not if you have no idea about Mojo, this article is very high level. So without further ado, lets start from the beginning.</p><h4>What is a tree?</h4><p>A tree is a graph where one node has 0 incoming edges and all the others have only one. The node with 0 incoming edges is called the root of the tree.</p><h4>What is a binary tree?</h4><p>Binary tree is a tree where every node has at most 2 outgoing edges.</p><p>Normally we define a binary search tree as following:</p><pre>class Node:<br>  def __init__(self):<br>    self.left = None<br>    self.right = None</pre><p>Where the two outgoing edges are represented with left and right fields. These fields are either set to None or point to another Node instance.</p><p>This example is a typical reference based tree data structure. As a Node instance stores two pointers, it occupies 16 bytes on 64-bit address space machine. Fields left and right point to a Node instance which can be at arbitrary address and hence this representation does not guarantee data locality and probably will involve CPU cache misses during traversal.</p><p>This leads us to the idea of an implicit, or space efficient data structures. Here is how FibyTree stores it’s outgoing edges:</p><pre>struct FibyTree:<br>  var left: DynamicVector[UInt16]<br>  var right: DynamicVector[UInt16]<br>  <br>  fn __init__(inout self):<br>    self.left = DynamicVector[UInt16]()<br>    self.right = DynamicVector[UInt16]()</pre><p>In FibyTree we represent the edges as indices. We decided to use unsigned 16 bit int as an index, meaning that our tree can have at most 2¹⁶ = 65.536 nodes. This however also mean that our overhead is only 4 bytes per node. When the left and right vectors are empty we have an empty tree. The root node is always placed at index 0. If a node does not have children it stores it’s own index. So for example:</p><pre>left: [0]<br>rigth: [0]</pre><p>Means that we have only one root node in the tree.</p><pre>left: [1, 1]<br>right:[0, 1]</pre><p>Represents a tree which has a tree with a root and only left child</p><pre>left: [0, 1]<br>right:[1, 1]</pre><p>Is the opposite case — a root with only right child</p><pre>left: [1, 1, 2]<br>right: [2, 1, 2]</pre><p>Is a tree where the root node has a left and a right child</p><pre>left: [2, 1, 2]<br>right: [1, 1, 2]</pre><p>Is logically equivalent to previous tree, the only difference is, we flipped the children indices, now left child is at index 2 and right child is at index 1</p><pre>left:[1, 2, 3, 4]<br>right: [0, 1, 2, 3]</pre><p>Is a tree of 4 nodes, where every node has only a left child.</p><p>BTW, representing 4 nodes in this way will take 16 bytes, the same amount of bytes as it takes to represent just one node as reference based tree.</p><p>The way we currently represent nodes is kind of pointless as the nodes do not cary any data, they just encode the tree structure. In order for a node to represent data we would need to associate data with the node:</p><pre>class Node:<br>  def __init__(self, data):<br>    self.data = data<br>    self.left = None<br>    self.right = None</pre><p>In the Node class, data property is another pointer, which points to an arbitrary location in memory and moves the size of a Node instance to 24 bytes. Important to notice, this size is pure overhead cost for associating the data as tree, the size of the data itself is unknown.</p><pre>struct FibyTree[T: AnyType]:<br>  var elements: DynamicVector[T]<br>  var left: DynamicVector[UInt16]<br>  var right: DynamicVector[UInt16]<br>  <br>  fn __init__(inout self):<br>    self.elements = DynamicVector[T]()<br>    self.left = DynamicVector[UInt16]()<br>    self.right = DynamicVector[UInt16]()</pre><p>In order to associate data with a FibyTree, we introduce an elements filed which is a vector of generic type T. This way FibyTree owns the data, the data is stored in contiguous memory and has no overhead per node.</p><h4>Lets talk about binary search trees now</h4><p>A binary search tree is a binary tree, wich stores comparable elements in such way that a left child of the root contains data which is smaller then the root data and the right child data is bigger then the root data. This way by traversing the tree, we can identify which branch to take in order to find the node with desired element. Because of this property, a binary search tree can be seen as a sorted set, where when we add an element, which is not represented by a node yet, a new node will be created as a child of the leaf node, where we unsuccessfully finished our search. I will not go into further details about the properties of the binary search trees as there is enough material online.</p><p>In order to make FibyTree a binary search tree and a semantic equivalent of a sorted set we provide following API:</p><pre>struct FibyTree[T: AnyType, cmp: fn(T, T)-&gt;Int]:<br>  var elements: DynamicVector[T]<br>  var left: DynamicVector[UInt16]<br>  var right: DynamicVector[UInt16]<br>  var deleted: Int<br><br>  fn __init__(inout self):<br>    self.elements = DynamicVector[T]()<br>    self.left = DynamicVector[UInt16]()<br>    self.right = DynamicVector[UInt16]()<br>    self.deleted = 0<br><br>  fn add(inout self, element: T):<br>  fn delete(inout self, element: T) -&gt; Bool:<br>  fn sorted_elements(self) -&gt; UnsafeFixedVector[T]:<br>  fn clear(inout self):<br>  fn union(self, other: Self) -&gt; Self:<br>  fn intersection(self, other: Self) -&gt; Self:<br>  fn difference(self, other: Self) -&gt; Self:<br>  fn symetric_difference(self, other: Self) -&gt; Self:<br>  fn is_subset(self, other: Self) -&gt; Bool:<br>  fn is_superset(self, other: Self) -&gt; Bool:<br>  fn is_disjoint(self, other: Self) -&gt; Bool:<br>  fn __len__(self) -&gt; Int:<br>  fn __contains__(self, element: T) -&gt; Bool:</pre><p>With this API we can add, delete and search for elements in the FibyTree. We can return all elements as a sorted vector, produce a new tree which is a union, intersection, difference or symmetric difference of two FibyTrees. We can check if one tree is a subset, superset, or disjoint from another. We can get a length of the tree and also clear it.</p><h4>Lets talk about last but not least feature of FibyTree. We can turn it in to complete fully balance binary search tree.</h4><p>What does it mean though?</p><p>When we add elements to binary search tree we add new nodes at the bottom, which can lead to tree degradation, worst case scenario we add sorted elements one after another, which will lead to a single branch tree, which is basically a linked list. Balancing the tree means that we strive to minimise the difference between depths of sub trees. A complete binary tree is a binary tree, where each level is fully filled except for the last one. A good example for a complete binary tree is a heap. A Complete fully balanced binary search tree guarantees a ceil(log2(n + 1)) depth for the tree and therefor at most ceil(log2(n + 1)) comparisons for search.</p><p>In order to balance the FibyTree we sort the elements and left / right indices in eytzinger order (BTW if you were wondering this is where FibyTree gets its y from) wich has O(n) runtime complexity. When a FibyTree is fully balanced we don’t even need to lookup left and right element by index, we can use the eytzinger formula:</p><pre>left = (n + 1) * 2 – 1<br>right = (n + 1) * 2</pre><p>to look left / right child.</p><p>This article is only the high level introduction. I will write other articles where I will discuss performance benchmarks and implementation details.</p><p>You can find the very early (not fully optimised and with some copy paste code) but already complete implementation of FibyTree in Mojo bellow.</p><p>Thank you for reading.</p><p>Please leave a 👏 if you liked it.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/dd2dc057e5d3ff6c5dba043dcd7888f0/href">https://medium.com/media/dd2dc057e5d3ff6c5dba043dcd7888f0/href</a></iframe><h4>A Message from AI Mind</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/250/0*5Wm7sOfTpe5DEbhg.gif" /></figure><p>Thanks for being a part of our community! Before you go:</p><ul><li>👏 Clap for the story and follow the author 👉</li><li>📰 View more content in the <a href="https://pub.aimind.so/">AI Mind Publication</a></li><li>🧠 Improve your <a href="https://www.aimind.so/prompt-generator?utm_source=pub&amp;utm_medium=message">AI prompts effortlessly and FREE</a></li><li><strong>🧰 Discover </strong><a href="https://www.aimind.so/?utm_source=pub&amp;utm_medium=message"><strong>Intuitive AI Tools</strong></a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=bd7f8775d815" width="1" height="1" alt=""><hr><p><a href="https://pub.aimind.so/a-high-level-introduction-to-fibytree-bd7f8775d815">A high level introduction to FibyTree</a> was originally published in <a href="https://pub.aimind.so">AI Mind</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Simple CSV parser in Mojo]]></title>
            <link>https://mzaks.medium.com/simple-csv-parser-in-mojo-3555c13fb5c8?source=rss-3c5928934a5e------2</link>
            <guid isPermaLink="false">https://medium.com/p/3555c13fb5c8</guid>
            <category><![CDATA[mojo]]></category>
            <category><![CDATA[csv]]></category>
            <category><![CDATA[parser]]></category>
            <category><![CDATA[simd]]></category>
            <dc:creator><![CDATA[Maxim Zaks]]></dc:creator>
            <pubDate>Sun, 28 May 2023 13:17:10 GMT</pubDate>
            <atom:updated>2023-05-28T13:33:11.118Z</atom:updated>
            <content:encoded><![CDATA[<h4>Parsing 400MB per second with less than 80 lines of 🔥</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*mH3qiNsMtzIhnhcN" /><figcaption>Photo by <a href="https://unsplash.com/@mbaumi?utm_source=medium&amp;utm_medium=referral">Mika Baumeister</a> on <a href="https://unsplash.com?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><p>So what is so hard about parsing CSV, isn’t it just .split(&quot;\n&quot;) and .split(&quot;,&quot;)? It can be, if you don’t have to comply with the <a href="https://www.rfc-editor.org/rfc/rfc4180">RFC 4180</a>.</p><p>RFC 4180 states, that a field can be placed between quotes (&quot;) and then it is legal to have new lines and commas as part of the field. So .split(&quot;\n&quot;) and .split(&quot;,&quot;) will not work anymore.</p><p>Back in 2019 I ported <a href="https://flatbuffers.dev/flexbuffers.html">FlexBuffers</a> to C# (<a href="https://github.com/mzaks/FlexBuffers-CSharp">FlexBuffers-CSharp</a>) as I wanted to use it for a <a href="https://medium.com/@mzaks/packaging-93k-levels-3ffc46b8ddac">project</a> I was working on. And since the initial data came as CSV, I also implemented a <a href="https://github.com/mzaks/FlexBuffers-CSharp/blob/master/FlexBuffers/CsvToFlexBufferConverter.cs">CSV to FlexBuffers converter</a>.</p><p>Nowadays I am exploring a new programming language called <a href="https://www.modular.com/mojo">Mojo</a>. After taking it for a spin by implementing a function to <a href="https://medium.com/@mzaks/counting-chars-with-simd-in-mojo-140ee730bd4d">count chars in UTF-8 string</a>, I decided to go a bit further, brush up my knowledge of CSV parsing and check how good it is going to fly in Mojo.</p><p>First, I ported my converter to Mojo with just some small adaptations:</p><pre>from String import *<br>from Vector import DynamicVector<br><br>struct CsvTable:<br>    var inner_string: String<br>    var starts: DynamicVector[Int]<br>    var ends: DynamicVector[Int]<br>    var column_count: Int<br>    <br>    fn __init__(inout self, owned s: String):<br>        self.inner_string = s<br>        self.starts = DynamicVector[Int](10)<br>        self.ends = DynamicVector[Int](10)<br>        self.column_count = -1<br>        self.parse()<br>    <br>    @always_inline<br>    fn parse(inout self):<br>        let QUOTE = ord(&#39;&quot;&#39;)<br>        let COMMA = ord(&#39;,&#39;)<br>        let LF = ord(&#39;\n&#39;)<br>        let CR = ord(&#39;\r&#39;)<br>        let length = len(self.inner_string.buffer)<br>        var offset = 0<br>        var in_double_quotes = False<br>        self.starts.push_back(offset)<br>        while offset &lt; length:<br>            let c = self.inner_string.buffer[offset]<br>            if c == QUOTE:<br>                in_double_quotes = not in_double_quotes<br>                offset += 1<br>            elif not in_double_quotes and c == COMMA:<br>                self.ends.push_back(offset)<br>                offset += 1<br>                self.starts.push_back(offset)<br>            elif not in_double_quotes and c == LF and not in_double_quotes:<br>                self.ends.push_back(offset)<br>                if self.column_count == -1:<br>                    self.column_count = len(self.ends)<br>                offset += 1<br>                self.starts.push_back(offset)<br>            elif not in_double_quotes and  c == CR and length &gt; offset + 1 and self.inner_string.buffer[offset + 1] == LF:<br>                self.ends.push_back(offset)<br>                if self.column_count == -1:<br>                    self.column_count = len(self.ends)<br>                offset += 2<br>                self.starts.push_back(offset)<br>            else:<br>                offset += 1<br><br>        self.ends.push_back(length)<br>        <br>    fn get(self, row: Int, column: Int) -&gt; String:<br>        if column &gt;= self.column_count:<br>            return &quot;&quot;<br>        let index = self.column_count * row + column<br>        if index &gt;= len(self.ends):<br>            return &quot;&quot;<br>        return self.inner_string[self.starts[index]:self.ends[index]]</pre><p>The code is fairly straightforward. I define a structure called CsvTable, which contains a string holding the CSV text. Upon creating an instance of the structure, it determines the starting and ending positions for each field in the table, as well as the number of columns. The calculation occurs byte by byte within the parse function, where it checks for &quot; , \n, or \r characters as specified in RFC 4180 and responds accordingly. The parse function assumes that the CSV is well-formed, meaning that every row has the same number of columns. Additionally, there is a get function that retrieves a field based on the provided row and column indices. If invalid indices are provided, the function returns an empty string, although some may argue that raising an exception would be more suitable.</p><p>The CsvTable implementation has certain shortcomings and lacks several evident features, including:</p><ul><li>Type inference for fields</li><li>Removal of quotation marks when returning a field using the get function</li><li>Provision of a flag to deviate from RFC-4180 and ignore quotation marks in fields</li><li>Offering the ability to set a different character as the column separator</li></ul><p>While it is possible that I may implement these features in the future, my current focus is on SIMD (Single Instruction, Multiple Data) and the resulting performance enhancements.</p><p>Back in 2019, a paper called “<a href="https://arxiv.org/abs/1902.08318">Parsing Gigabytes of JSON per Second</a>” by Geoff Langdale and Daniel Lemire created quite a buzz. They showed how using SIMD instructions could significantly speed up JSON parsing. The same authors also had <a href="https://github.com/geofflangdale/simdcsv">a solution for parsing CSV files</a>. I got curious and wanted to try implementing their approach using Mojo. Unfortunately, I ran into some issues because the current Mojo standard library didn’t have all the necessary intrinsics exposed. I could have used MLIR inter-op, but instead, I decided to build the parser using only the standard library’s own structs and functions. Despite the limitations, I managed to create a pretty efficient and, in my opinion, simple and elegant parser.</p><p>In the following sections, I will demonstrate my implementation, provide performance benchmarks, and highlight the distinctions between my solution and the one presented by Geoff Langdale and Daniel Lemire.</p><p>So, lets start by looking at the SimdCsvTable:</p><pre>from DType import DType<br>from Functional import vectorize<br>from Intrinsics import compressed_store<br>from Math import iota, any_true, reduce_bit_count<br>from Memory import *<br>from Pointer import DTypePointer<br>from String import String, ord<br>from TargetInfo import dtype_simd_width<br>from Vector import DynamicVector<br><br>alias simd_width_u8 = dtype_simd_width[DType.ui8]()<br><br>struct SimdCsvTable:<br>    var inner_string: String<br>    var starts: DynamicVector[Int]<br>    var ends: DynamicVector[Int]<br>    var column_count: Int<br>    <br>    fn __init__(inout self, owned s: String):<br>        self.inner_string = s<br>        self.starts = DynamicVector[Int](10)<br>        self.ends = DynamicVector[Int](10)<br>        self.column_count = -1<br>        self.parse()<br>    <br>    @always_inline<br>    fn parse(inout self):<br>        let QUOTE = ord(&#39;&quot;&#39;)<br>        let COMMA = ord(&#39;,&#39;)<br>        let LF = ord(&#39;\n&#39;)<br>        let CR = ord(&#39;\r&#39;)<br>        let p = DTypePointer[DType.si8](self.inner_string.buffer.data)<br>        let string_byte_length = len(self.inner_string)<br>        var in_quotes = False<br>        self.starts.push_back(0)<br>        <br>        @always_inline<br>        @parameter<br>        fn find_indexies[simd_width: Int](offset: Int):<br>            let chars = p.simd_load[simd_width](offset)<br>            let quotes = chars == QUOTE<br>            let commas = chars == COMMA<br>            let lfs = chars == LF<br>            let all_bits = quotes | commas | lfs<br>            <br>            let offsets = iota[simd_width, DType.ui8]()<br>            let sp: DTypePointer[DType.ui8] = stack_allocation[simd_width, UI8, simd_width]()<br>            compressed_store(offsets, sp,  all_bits)<br>            let all_len = reduce_bit_count(all_bits)<br>            <br>            let crs_ui8 = (chars == CR).cast[DType.ui8]()<br>            let lfs_ui8 = lfs.cast[DType.ui8]()<br><br>            for i in range(all_len):<br>                let index = sp.load(i).to_int()<br>                if quotes[index]:<br>                    in_quotes = not in_quotes<br>                    continue<br>                if in_quotes:<br>                    continue<br>                let current_offset = index + offset<br>                self.ends.push_back(current_offset - (lfs_ui8[index] * crs_ui8[index - 1]).to_int())<br>                self.starts.push_back(current_offset + 1)<br>                if self.column_count == -1 and lfs[index]:<br>                    self.column_count = len(self.ends)<br>            <br>        vectorize[simd_width_u8, find_indexies](string_byte_length)<br>        self.ends.push_back(string_byte_length)<br>    <br>    fn get(self, row: Int, column: Int) -&gt; String:<br>        if column &gt;= self.column_count:<br>            return &quot;&quot;<br>        let index = self.column_count * row + column<br>        if index &gt;= len(self.ends):<br>            return &quot;&quot;<br>        return self.inner_string[self.starts[index]:self.ends[index]]</pre><p>The structure of the struct closely resembles CsvTable, with the main distinction being the implementation of the parse function. To access the underlying string byte buffer with SIMD, we need to represent it as a DTypePointer[DType.si8]. This allows us to use the simd_load method, enabling SIMD operations on vectors of bytes. The find_indices function plays a crucial role in our parsing process. With the help of the vectorize function, we can apply the find_indices function to chunks of the inner_string, effectively converting them into SIMD vectors. Subsequently, we create three bit-sets that indicate whether an element in the vector is equal to &quot;, , or \n, respectively:</p><pre>let chars = p.simd_load[simd_width](offset)<br>let quotes = chars == QUOTE<br>let commas = chars == COMMA<br>let lfs = chars == LF<br>let all_bits = quotes | commas | lfs</pre><p>The all_bits bit-set (a SIMD vector of bools) represents the combination of the markers.</p><p>Now we need to transform a SIMD vector of bools into a list of offsets. Which we can do with following four lines:</p><pre>let offsets = iota[simd_width, DType.ui8]()<br>let sp: DTypePointer[DType.ui8] = stack_allocation[simd_width, UI8, simd_width]()<br>compressed_store(offsets, sp,  all_bits)<br>let all_len = reduce_bit_count(all_bits)</pre><p>First we use the iota function to produce a vector of increasing ints starting from zero. Side note: AFAIK the iota function has it’s roots in <a href="https://en.wikipedia.org/wiki/APL_(programming_language)">APL</a> and is also called an <a href="https://aplwiki.com/wiki/Index_Generator">index generator</a>.</p><p>Moving forward, our objective is to “<em>compress</em>” the offsets using the all_bits vector. This involves removing all instances in the offsets where the corresponding values in all_bits are False. The resulting list’s size will correspond to the number of True values in the all_bits vector. We allocate memory on the stack for the compressed_store function’s result and determine its length by invoking the reduce_bit_count(all_bits) function.</p><p>Given the length and the compressed list, we can iterate over the special characters in current text chunk and push start and end offsets if they are not in quotes.</p><p>Now it is time to do some benchmarking. I picked the same CSV file which were provided in the <a href="https://github.com/geofflangdale/simdcsv/tree/master/examples">simdcsv repo</a>.</p><p>For the nfl.csv, I got following result:</p><pre>SIMD min parse time in nanoseconds:<br>3135722.0<br>Non SIMD min parse time in nanoseconds:<br>4992006.0<br>Difference<br>1.5919797737171855<br>SIMD bytes parsed per nanosecond<br>0.43519738038002093<br>Non SIMD bytes parsed per nanosecond<br>0.27336866181651226</pre><p>And for EDW.TEST_CAL_DT.csv the number are a bit lower:</p><pre>SIMD min time in nanoseconds:<br>2007207.0<br>Non SIMD min time in nanoseconds:<br>2446398.0<br>Difference<br>1.2188070288714616<br>SIMD byte per nanosecond<br>0.25521333873387247<br>Non SIMD byte per nanosecond<br>0.20939601814586178</pre><p>It’s worth noting that at present, Mojo code can only be run through a cloud-based Jupyter Notebook. This means that the absolute performance numbers may not carry as much weight, but the relative difference between the SIMD and non-SIMD parsers should still hold true. In our analysis, we found that the SIMD parser was around 60% faster for the nfl dataset, while for the second dataset, it showed a modest improvement of about 20%.</p><p>To be frank, the performance boost achieved was not as substantial as I had anticipated. However, it still represents a notable improvement, which I consider a win.</p><p>Now, let’s discuss why I had to deviate from the solution proposed by Geoff Langdale and Daniel Lemire. In Mojo, when we compare a SIMD vector of integers with an integer, we obtain a SIMD vector of booleans, which is indeed a useful feature. However, I encountered a challenge when trying to perform a shift operation on the boolean vector. In contrast, performing a shift on a bit-set represented as a 64-bit unsigned integer is a straightforward operation. This limitation hindered my ability to replicate certain aspects of the original solution.</p><p>Another obstacle I encountered was the need to check if a separator is within quotation marks, which requires the use of carry-less multiplication. Geoff Langdale wrote a <a href="https://branchfree.org/2019/03/06/code-fragment-finding-quote-pairs-with-carry-less-multiply-pclmulqdq/">blog post</a> explaining the advantages of this approach. Unfortunately, the Mojo standard library does not currently provide access to this rather specialized operation. While it may be possible to expose carry-less multiplication through MLIR inter-op, I opted not to pursue that route for now.</p><p>Therefore, due to these limitations in the Mojo standard library, I had to find alternative approaches to address these specific requirements in my implementation, which introduced some branching in the code.</p><p>That’s all from my side. Thank you for reading until the end. Feel free to leave a comment. Take care, and until next time!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=3555c13fb5c8" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Counting chars with SIMD in Mojo]]></title>
            <link>https://mzaks.medium.com/counting-chars-with-simd-in-mojo-140ee730bd4d?source=rss-3c5928934a5e------2</link>
            <guid isPermaLink="false">https://medium.com/p/140ee730bd4d</guid>
            <category><![CDATA[mojo]]></category>
            <category><![CDATA[programming-languages]]></category>
            <category><![CDATA[simd]]></category>
            <category><![CDATA[utf-8]]></category>
            <dc:creator><![CDATA[Maxim Zaks]]></dc:creator>
            <pubDate>Thu, 18 May 2023 14:05:05 GMT</pubDate>
            <atom:updated>2023-05-19T15:09:39.625Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*435wbD8QcFr8u-Y5" /><figcaption>Photo by <a href="https://unsplash.com/@nshuman1291?utm_source=medium&amp;utm_medium=referral">Nathaniel Shuman</a> on <a href="https://unsplash.com?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><p>Mojo is a very young (actually a work in progress) programming language designed and developed by a new company called Modular. Here is a blurb from their <a href="https://www.modular.com/mojo">website</a>:</p><blockquote>Mojo combines the usability of Python with the performance of C, unlocking unparalleled programmability of AI hardware and extensibility of AI models.</blockquote><p>So, the language is a Python superset, which should have full Python inter-op and allows developers, with proper know-how, develop efficient modules. This sounds a little bit like <a href="https://cython.readthedocs.io/en/latest/src/quickstart/overview.html">Cython</a>, however Mojo has much more ambitious goals. If you are interested in more details, browse through the <a href="https://docs.modular.com/mojo/faq.html">FAQ</a> compiled by the Modular team.</p><p>To enable low-level programming, Mojo introduces extra keywords and concepts to Python. For instance, it introduces a struct keyword for defining efficient value types and an fn keyword for defining efficient functions. If you want to delve into more specific information, I recommend referring to the <a href="https://docs.modular.com/mojo/programming-manual.html">Mojo programming manual</a>.</p><p>At some point in the future, Mojo is planned to be released as an open-source project. However, currently, external developers can only try out the language by requesting access to a Jupyter Notebook provided by the Modular team.</p><p>Yours truly was granted an access a couple of days ago, and after spending some time studying the documentation, I became curious about the low-level implementation of Strings in Mojo.</p><p>Mojo has three types to express a string:</p><ol><li>StringLiteral a built in type</li><li>StringRef also a built in type</li><li>String a type defined in the standard library</li></ol><p>As the name implies StringLiteral represents a string we write in the code:</p><pre>let s = &quot;hello&quot; # s is of type StringLiteral</pre><p>In Mojo, a StringRef can be created from a StringLiteral and it appears to primarily serve as a type for ABI (Application Binary Interface) inter-operation.</p><pre>let s: StringRef = &quot;hello&quot; <br># StringLiteral &quot;hello&quot; is converted to StringRef and assigned to s</pre><p>A StringLiteral has a data method, which lets us get raw pointer to the underlying data. However, the type of the pointer, pointer&lt;scalar&lt;si8&gt;&gt;, is quite mysterious since it’s not explained in the docs at all. At first, I thought it could be related to MLIR, because Mojo allows referencing MLIR types directly (see <a href="https://docs.modular.com/mojo/notebooks/BoolMLIR.html">Low-level IR in Mojo</a>), but even after searching, I couldn’t find any information about it on the MLIR side either. It’s definitely an intriguing aspect of Mojo’s implementation!</p><p>However, despite the mystery surrounding the pointer&lt;scalar&lt;si8&gt;&gt; type, it’s worth noting that the Mojo standard library does include a module called Pointer. This module defines a Pointer struct that can be initialized with a pointer&lt;type&gt;. Therefore, after some experimentation and tinkering, I was able to write the following code:</p><pre>let s = &quot;hello&quot;<br><br>let p = Pointer(s.data())<br><br>for i in range(len(s)):<br>    print(p[i])</pre><p>Which returned the internal byte stream of the string:</p><pre>104<br>101<br>108<br>108<br>111</pre><p><strong>And what do we learn from this?</strong> Mojo uses UTF-8 encoding to store strings!</p><p>Ok, but what about the actual String type from the standard library?</p><p>If we want to use the String struct from the standard library, we can simply write:</p><pre>from String import String<br><br>let s: String = &quot;hello&quot;</pre><p>In order to output the chars from a string, I figured, I should be able to write following:</p><pre>from String import String<br><br>let s: String = &quot;hello&quot;<br><br>for c in s:<br>    print(c)</pre><p>But this end up in an error: <strong>‘String’ does not implement the ‘__iter__’ method</strong>. So lets try something else:</p><pre>from String import String<br><br>let s: String = &quot;hello&quot;<br><br>for i in range(len(s)):<br>    print(s[i])</pre><p>And we get the characters printed:</p><pre>h<br>e<br>l<br>l<br>o</pre><p>That is great, but what about a string with characters, which are longer then one byte?</p><pre>from String import String<br><br>let s: String = &quot;hello 🔥&quot;<br><br>for i in range(len(s)):<br>    print(s[i])</pre><p>While this code compiles successfully, it doesn’t produce any output in the Jupyter Notebook. It appears to result in a runtime error. Even attempting to print a specific character, such as print(s[6]), doesn&#39;t work as expected. To troubleshoot the issue, let&#39;s examine the underlying byte stream:</p><pre>let s = &quot;hello 🔥&quot;<br>let p = Pointer(s.data())<br><br>for i in range(len(s)):<br>    print(p[i])<br><br># returns <br># 104<br># 101<br># 108<br># 108<br># 111<br># 32<br># -16<br># -97<br># -108<br># -91</pre><p>This printed output highlights one small oddity, which caught my eye earlier. The byte stream is typed as si8 which is a signed 1-byte integer. I think it is more logical to type sequence of bytes as an unsigned byte integer ui8. But 🤷, that is not super important. What is important, we see that the byte sequence is 10 bytes long, if we execute print(len(s)) we also get 10 as a result. If we execute print(s[6:10]) we get 🔥 as the result.</p><p><strong>What did we learn?</strong> In current implementation, the String struct is a light weight wrapper around the UTF-8 byte sequence. The length corresponds to the number of bytes, not number of characters and if we try to access a multi byte character with an incorrect range, we get a runtime error.</p><p>To hone our Mojo skills, let’s create our own function that takes a StringLiteral as input and returns the number of characters. However, before we proceed, it’s essential to grasp how UTF-8 encoding works and how we can identify when multiple bytes form a single character. Fortunately, all the necessary information about UTF-8 is available in this <a href="https://en.wikipedia.org/wiki/UTF-8">Wikipedia article</a>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/642/1*0CwCHXWuNfPXwZkQVGe6vA.png" /></figure><p>Looking at the table I copied from the <a href="https://en.wikipedia.org/wiki/UTF-8">Wikipedia article</a>, we can observe that every Unicode character can be encoded in UTF-8 using 1 to 4 bytes. The first byte in the sequence representing a character has a special trailing bits pattern that indicates the length of the sequence. Any byte that isn’t in the first position will always have a 10 trailing bits pattern. To determine whether a byte represents the start of a character, we can use the following check on each byte:</p><pre>(byte &gt;&gt; 6) != 0b10</pre><p>As described above, in Mojo, we have easy access to the underlying bytes of the string literal, so we can simply loop over each byte and apply the check. If the condition (byte &gt;&gt; 6) != 0b10 is true, we increment the character count. While this is a straightforward solution, we can further optimize it in Mojo.</p><p>Mojo offers excellent support for SIMD (Single Instruction Multiple Data) operations, which allow us to execute a single operation on a vector of values. In our case, we need to perform a right shift and an equality comparison on a sequence of 1-byte values. With SIMD, we can group these values into SIMD vectors and carry out both operations as following:</p><pre>let p = DTypePointer[DType.si8](string_literal.data()).bitcast[DType.ui8]()<br>(p.simd_load[64]() &gt;&gt; 6) != 0b10</pre><p>On the first line, we extract the data from the string literal and encapsulate it within a DTypePointer. A DTypePointer represents a pointer to DType values, which is necessary for invoking the simd_load method. This method, in turn, generates a SIMD type that enables us to execute vectorized operations.</p><p>You might be curious about the .bitcast[DType.ui8]() operation. This is required in Mojo, because the string literal data is initially typed as si8. By applying the .bitcast[DType.ui8]() operation, we rectify this issue, ensuring compatibility with the &gt;&gt; operator that we intend to use.</p><p>On the second line, we load 64 bytes from the pointer as a SIMD vector and perform shift to the right on all elements and after that, we compare all elements with 0b10. The result of this comparison will be a SIMD vector of booleans, which we can cast to a SIMD vector of ui8</p><pre>.cast[DType.ui8]()</pre><p>Now we can sum all the element of the SIMD vector to get the number of chars.</p><pre>.reduce_add().to_int()</pre><p>Below is my first implementation of a function that calculates the number of characters in a string literal:</p><pre>fn chars_len(s: StringLiteral) -&gt; Int:<br>    let p = DTypePointer[DType.si8](s.data()).bitcast[DType.ui8]()<br>    let l = len(s)<br>    var offset = 0<br>    var result = 0<br>    while l - offset &gt;= 64:<br>        result += ((p.simd_load[64](offset) &gt;&gt; 6) != 0b10).cast[DType.ui8]().reduce_add().to_int()<br>        offset += 64<br>    while l - offset &gt;= 32:<br>        result += ((p.simd_load[32](offset) &gt;&gt; 6) != 0b10).cast[DType.ui8]().reduce_add().to_int()<br>        offset += 32<br>    while l - offset &gt;= 16:<br>        result += ((p.simd_load[16](offset) &gt;&gt; 6) != 0b10).cast[DType.ui8]().reduce_add().to_int()<br>        offset += 16<br>    while l - offset &gt;= 8:<br>        result += ((p.simd_load[8](offset) &gt;&gt; 6) != 0b10).cast[DType.ui8]().reduce_add().to_int()<br>        offset += 8<br>    while l - offset &gt;= 4:<br>        result += ((p.simd_load[4](offset) &gt;&gt; 6) != 0b10).cast[DType.ui8]().reduce_add().to_int()<br>        offset += 4<br>    while l - offset &gt;= 2:<br>        result += ((p.simd_load[2](offset) &gt;&gt; 6) != 0b10).cast[DType.ui8]().reduce_add().to_int()<br>        offset += 2<br>    while l - offset &gt;= 1:<br>        result += ((p.simd_load[1](offset) &gt;&gt; 6) != 0b10).cast[DType.ui8]().reduce_add().to_int()<br>        offset += 1<br>        <br>    return result</pre><p>Oh boy, this looks like a lot of copy pasta! How comes?</p><p>Well, the length of the string literal is only known at runtime. The more bytes we can consume in a large SIMD vector, the better, so we “fall through” the different sizes of SIMD vectors.</p><h4>Is there a better way?</h4><p>After the first implementation, I come up with another one, which is better in the sense of less conditions, but has a memcpy:</p><pre>from Bit import *<br>from Memory import *<br>from Pointer import *<br><br>fn chars_len[simd_width: Int](s: StringLiteral) -&gt; Int:<br>    let p = DTypePointer[DType.si8](s.data()).bitcast[DType.ui8]()<br>    let l = len(s)<br>    var offset = 0<br>    var result = 0<br>    while l - offset &gt;= simd_width:<br>        result += ((p.simd_load[simd_width](offset) &gt;&gt; 6) != 0b10).cast[DType.ui8]().reduce_add().to_int()<br>        offset += simd_width<br>    <br>    if offset &lt; l:<br>        let rest_p: DTypePointer[DType.ui8] = stack_allocation[simd_width, UI8, 1]()<br>        memset_zero(rest_p, simd_width)<br>        memcpy(rest_p, p.offset(offset), l - offset)<br>        result += ((rest_p.simd_load[simd_width]() &gt;&gt; 6) != 0b10).cast[DType.ui8]().reduce_add().to_int()<br>        result -= simd_width - (l - offset)<br>    <br>    return result</pre><p>In this implementation, we let the user provide the SIMD vector size, although they probably should use the <a href="https://docs.modular.com/mojo/programming-manual.html#autotuning-adaptive-compilation">autotune feature</a>, but if they know that the string is short, they could pass a better vector size value.</p><p>We run the algorithm, explained above, on as many bytes as possible for the given vector size. Then we check, if we consumed all the bytes through the vector transformation. If not, we allocate memory on stack for another go and copy the unprocessed bytes, from string literals underlying bytes sequence, to the newly allocated stack region. As the rest_p memory region has 0 bytes, which do not belong to the string, and 0 byte values will result in a positive char count, we need to subtract them from the result:</p><pre>result -= simd_width - (l - offset)</pre><p>And that concludes this blog post. However, I’m contemplating writing another one where we not only count the number of characters but also the number of graphemes in a string. Additionally, I plan to provide a function for safely truncating strings based on the number of bytes. If you find these topics interesting, please let me know in the comments section. Your feedback and suggestions are greatly appreciated!</p><h4>Update 19th of May 2023</h4><p>Every new technology needs a great community behind it and Mojo already has it!</p><p>Thanks to the community feedback, I was able to produce a third version of the code, which is very elegant, without sacrificing the performance:</p><pre>from DType import DType<br>from Functional import vectorize<br>from Pointer import DTypePointer<br>from TargetInfo import dtype_simd_width<br><br>alias simd_width_u8 = dtype_simd_width[DType.ui8]()<br><br>fn chars_count(s: StringLiteral) -&gt; Int:<br>    let p = DTypePointer[DType.si8](s.data()).bitcast[DType.ui8]()<br>    let string_byte_length = len(s)<br>    var result = 0<br>    <br>    @parameter<br>    fn count[simd_width: Int](offset: Int):<br>        result += ((p.simd_load[simd_width](offset) &gt;&gt; 6) != 0b10).cast[DType.ui8]().reduce_add().to_int()<br>    <br>    vectorize[simd_width_u8, count](string_byte_length)<br>    return result</pre><p>There are a few concepts in this solution which I did not touch in my blog post before, so let me give a brief explanation.</p><pre>alias simd_width_u8 = dtype_simd_width[DType.ui8]()</pre><p>With the alias keyword we identify that the value of simd_width_u8 is a constant, which will be computed at compile time. This value identifies how many entries a SIMD vector can have for the ui8 type. This can be computed at compile time, based on the architecture and what kind of SIMD support it has. In the Notebook, this value evaluates to 64 as the system, which runs the Notebook, has AVX512 support.</p><pre>    @parameter<br>    fn count[simd_width: Int](offset: Int):<br>        result += ((p.simd_load[simd_width](offset) &gt;&gt; 6) != 0b10).cast[DType.ui8]().reduce_add().to_int()</pre><p>This is an inner function, which will be called with an offset to perform the count. The @parameter decorator is needed, because we are capturing the result variable. For more details please read the <a href="https://docs.modular.com/mojo/programming-manual.html#parameter-decorator">documentation</a>.</p><pre>vectorize[simd_width_u8, count](string_byte_length)</pre><p>By calling the vectorize function, I avoid the copy and paste frenzy, I introduced in the first solution. This function executes the count function for us, based on the compile time parameters (the SIMD vector width, the function for the loop body) and arguments (the total loop count) we provide. My guess is, it does something similar to what I did manually in solution one, but this is not our burden to read and write anymore. Which is great!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=140ee730bd4d" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>