﻿<?xml version="1.0" encoding="utf-8"?><rss version="2.0"><channel><title>Ayende @ Rahien</title><link>http://ayende.com</link><description>Ayende @ Rahien</description><copyright>Copyright (C) Ayende Rahien  2004 - 2021 (c) 2026</copyright><ttl>60</ttl><item><title>Challenge: Giving file system developer ulcer</title><description>&lt;p style="text-align:left;"&gt;I&amp;rsquo;m trying to reason about the behavior of this code, and I can&amp;rsquo;t decide if this is a stroke of genius or if I&amp;rsquo;m suffering from a stroke. Take a look at the code, and then I&amp;rsquo;ll discuss what I&amp;rsquo;m trying to do below:&lt;/p&gt;&lt;p style="text-align:left;"&gt;&lt;hr/&gt;&lt;pre class='line-numbers language-bash'&gt;&lt;code class='line-numbers language-bash'&gt;HANDLE hFile &lt;span class="token operator"&gt;=&lt;/span&gt; CreateFileA&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token string"&gt;"R:/original_file.bin"&lt;/span&gt;, 
GENERIC_READ &lt;span class="token operator"&gt;|&lt;/span&gt; GENERIC_WRITE, 
FILE_SHARE_READ &lt;span class="token operator"&gt;|&lt;/span&gt; FILE_SHARE_WRITE, 
NULL, 
OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, 
NULL&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
&lt;span class="token keyword"&gt;if&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;hFile &lt;span class="token operator"&gt;==&lt;/span&gt; INVALID_HANDLE_VALUE&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token punctuation"&gt;{&lt;/span&gt;
    printf&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token string"&gt;"Error creating file: %d&lt;span class="token entity" title="\n"&gt;\n&lt;/span&gt;"&lt;/span&gt;, GetLastError&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;))&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
    exit&lt;span class="token punctuation"&gt;(&lt;/span&gt;__LINE__&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
&lt;span class="token punctuation"&gt;}&lt;/span&gt;
&lt;p&gt;HANDLE hMapFile &lt;span class="token operator"&gt;=&lt;/span&gt; CreateFileMapping&lt;span class="token punctuation"&gt;(&lt;/span&gt;hFile, NULL,&lt;br /&gt;
PAGE_READWRITE, &lt;span class="token number"&gt;0&lt;/span&gt;, &lt;span class="token number"&gt;0&lt;/span&gt;, NULL&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;&lt;br /&gt;
&lt;span class="token keyword"&gt;if&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;hMapFile &lt;span class="token operator"&gt;==&lt;/span&gt; NULL&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token punctuation"&gt;{&lt;/span&gt;&lt;br /&gt;
fprintf&lt;span class="token punctuation"&gt;(&lt;/span&gt;stderr, &lt;span class="token string"&gt;&amp;quot;Could not create file mapping object: %x&lt;span class="token entity" title="\n"&gt;\n&lt;/span&gt;&amp;quot;&lt;/span&gt;, GetLastError&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;))&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;&lt;br /&gt;
exit&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;strong&gt;LINE&lt;/strong&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;&lt;br /&gt;
&lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;char* lpMapAddress &lt;span class="token operator"&gt;=&lt;/span&gt; MapViewOfFile&lt;span class="token punctuation"&gt;(&lt;/span&gt;hMapFile, FILE_MAP_WRITE, &lt;span class="token number"&gt;0&lt;/span&gt;, &lt;span class="token number"&gt;0&lt;/span&gt;, &lt;span class="token number"&gt;0&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;&lt;br /&gt;
&lt;span class="token keyword"&gt;if&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;lpMapAddress &lt;span class="token operator"&gt;==&lt;/span&gt; NULL&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token punctuation"&gt;{&lt;/span&gt;&lt;br /&gt;
fprintf&lt;span class="token punctuation"&gt;(&lt;/span&gt;stderr, &lt;span class="token string"&gt;&amp;quot;Could not map view of file: %x&lt;span class="token entity" title="\n"&gt;\n&lt;/span&gt;&amp;quot;&lt;/span&gt;, GetLastError&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;))&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;&lt;br /&gt;
exit&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;strong&gt;LINE&lt;/strong&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;&lt;br /&gt;
&lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span class="token keyword"&gt;for&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;size_t i &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token number"&gt;2&lt;/span&gt; * MB&lt;span class="token punctuation"&gt;;&lt;/span&gt; i &lt;span class="token operator"&gt;&amp;lt;&lt;/span&gt; &lt;span class="token number"&gt;4&lt;/span&gt; * MB&lt;span class="token punctuation"&gt;;&lt;/span&gt; i++&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;br /&gt;
&lt;span class="token punctuation"&gt;{&lt;/span&gt;&lt;br /&gt;
lpMapAddress&lt;span class="token punctuation"&gt;[&lt;/span&gt;i&lt;span class="token punctuation"&gt;]&lt;/span&gt;++&lt;span class="token punctuation"&gt;;&lt;/span&gt;&lt;br /&gt;
&lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;HANDLE hDirect &lt;span class="token operator"&gt;=&lt;/span&gt; CreateFileA&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token string"&gt;&amp;quot;R:/original_file.bin&amp;quot;&lt;/span&gt;,&lt;br /&gt;
GENERIC_READ &lt;span class="token operator"&gt;|&lt;/span&gt; GENERIC_WRITE,&lt;br /&gt;
FILE_SHARE_READ &lt;span class="token operator"&gt;|&lt;/span&gt; FILE_SHARE_WRITE,&lt;br /&gt;
NULL,&lt;br /&gt;
OPEN_ALWAYS,&lt;br /&gt;
FILE_ATTRIBUTE_NORMAL,&lt;br /&gt;
NULL&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;SetFilePointerEx&lt;span class="token punctuation"&gt;(&lt;/span&gt;hDirect, &lt;span class="token punctuation"&gt;(&lt;/span&gt;LARGE_INTEGER&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token punctuation"&gt;{&lt;/span&gt; &lt;span class="token number"&gt;6&lt;/span&gt; * MB &lt;span class="token punctuation"&gt;}&lt;/span&gt;, &lt;span class="token operator"&gt;&amp;amp;&lt;/span&gt; fileSize, FILE_BEGIN&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;&lt;br /&gt;
&lt;span class="token keyword"&gt;for&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;i &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token number"&gt;6&lt;/span&gt; &lt;span class="token punctuation"&gt;;&lt;/span&gt; i &lt;span class="token operator"&gt;&amp;lt;&lt;/span&gt; &lt;span class="token number"&gt;10&lt;/span&gt; &lt;span class="token punctuation"&gt;;&lt;/span&gt; i++&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token punctuation"&gt;{&lt;/span&gt;&lt;br /&gt;
&lt;span class="token keyword"&gt;if&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token operator"&gt;!&lt;/span&gt;WriteFile&lt;span class="token punctuation"&gt;(&lt;/span&gt;hDirect, lpMapAddress + i * MB, MB, &lt;span class="token operator"&gt;&amp;amp;&lt;/span&gt;bytesWritten, NULL&lt;span class="token punctuation"&gt;))&lt;/span&gt; &lt;span class="token punctuation"&gt;{&lt;/span&gt;&lt;br /&gt;
fprintf&lt;span class="token punctuation"&gt;(&lt;/span&gt;stderr, &lt;span class="token string"&gt;&amp;quot;WriteFile direct failed on iteration %d: %x&lt;span class="token entity" title="\n"&gt;\n&lt;/span&gt;&amp;quot;&lt;/span&gt;, i, GetLastError&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;))&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;&lt;br /&gt;
exit&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;strong&gt;LINE&lt;/strong&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;&lt;br /&gt;
&lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;br /&gt;
&lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;hr/&gt;&lt;/p&gt;&lt;p style="text-align:left;"&gt;The idea is pretty simple, I’m opening the same file &lt;em&gt;twice&lt;/em&gt;. Once in buffered mode and mapping that memory for both reads &amp;amp; writes. The problem is that to flush the data to disk, I have to either wait for the OS, or call FlushViewOfFile() and FlushFileBuffers() to actually flush it to disk explicitly.&lt;/p&gt;&lt;p style="text-align:left;"&gt;The problem with this approach is that FlushFileBuffers() has &lt;span style="text-decoration:underline;"&gt;&lt;a style="color:inherit;" href="https://github.com/dotnet/runtime/issues/111201"&gt;undesirable side effects&lt;/a&gt;&lt;/span&gt;. So I’m opening the file &lt;em&gt;again&lt;/em&gt;, this time for &lt;em&gt;unbuffered&lt;/em&gt; I/O. I’m writing to the &lt;em&gt;memory&lt;/em&gt; map and then using the &lt;em&gt;same&lt;/em&gt; mapping to write to the file itself. On Windows, that goes through a separate path (and may lose coherence with the memory map). &lt;/p&gt;&lt;p style="text-align:left;"&gt;The idea here is that since I’m writing from the same location, I &lt;em&gt;can’t&lt;/em&gt; lose coherence. I either get the value from the file or from the memory map, and they are both the same. At least, that is what I &lt;em&gt;hope&lt;/em&gt; will happen.&lt;/p&gt;&lt;p style="text-align:left;"&gt;For the purpose of discussion, I can ensure that there is no one else writing to this file while I’m abusing the system in this manner. What do you &lt;em&gt;think&lt;/em&gt; Windows will do in this case?&lt;/p&gt;&lt;p style="text-align:left;"&gt;I believe that when I’m writing using unbuffered I/O in this manner, I’m forcing the OS to drop the mapping and refresh from the disk. That is likely the reason why it may lose coherence, because there may be already reads that aren’t served from main memory, or something like that.&lt;/p&gt;&lt;p style="text-align:left;"&gt;This isn’t an approach that I would actually take for production usage, but it is a &lt;em&gt;damn&lt;/em&gt; interesting thing to speculate on. If you have any idea what will actually happen, I would love to have your input.&lt;/p&gt;&lt;/p&gt;
&lt;link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/9000.0.1/themes/prism.min.css" integrity="sha512-/mZ1FHPkg6EKcxo0fKXF51ak6Cr2ocgDi5ytaTBjsQZIH/RNs6GF6+oId/vPe3eJB836T36nXwVh/WBl/cWT4w==" crossorigin="anonymous" referrerpolicy="no-referrer" /&gt;
</description><link>http://ayende.com/201989-a/challenge-giving-file-system-developer-ulcer?Key=43f24790-760e-4bd6-bd27-e458228ddada</link><guid>http://ayende.com/201989-a/challenge-giving-file-system-developer-ulcer?Key=43f24790-760e-4bd6-bd27-e458228ddada</guid><pubDate>Mon, 03 Feb 2025 12:00:00 GMT</pubDate></item><item><title>Challenge: Efficient snapshotable state</title><description>&lt;p style="text-align:left;"&gt;At the heart of RavenDB, there is a data structure that we call the Page Translation Table. It is one of &lt;em&gt;the&lt;/em&gt;&amp;nbsp;most important pieces inside RavenDB.&lt;/p&gt;&lt;p style="text-align:left;"&gt;The page translation table is basically a Dictionary&amp;lt;long, Page&amp;gt;, mapping between a page number and the actual page. The critical aspect of this data structure is that it is both concurrent and multi-version. That is, at a single point, there may be multiple versions of the table, representing different versions of the table at given points in time.&lt;/p&gt;&lt;p style="text-align:left;"&gt;The way it works, a transaction in RavenDB generates a page translation table as part of its execution and publishes the table on commit. However, each subsequent table builds upon the previous one, so things become more complex. Here is a usage example (in Python pseudo-code):&lt;/p&gt;&lt;p style="text-align:left;"&gt;&lt;hr/&gt;&lt;pre class='line-numbers language-bash'&gt;&lt;code class='line-numbers language-bash'&gt;table &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token punctuation"&gt;{&lt;/span&gt;&lt;span class="token punctuation"&gt;}&lt;/span&gt;


with wtx1 &lt;span class="token operator"&gt;=&lt;/span&gt; write_tx&lt;span class="token punctuation"&gt;(&lt;/span&gt;table&lt;span class="token punctuation"&gt;)&lt;/span&gt;:
  wtx1.put&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token number"&gt;2&lt;/span&gt;, &lt;span class="token string"&gt;'v1'&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;
  wtx1.put&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token number"&gt;3&lt;/span&gt;, &lt;span class="token string"&gt;'v1'&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;
  wtx1.publish&lt;span class="token punctuation"&gt;(&lt;/span&gt;table&lt;span class="token punctuation"&gt;)&lt;/span&gt;


&lt;span class="token comment"&gt;# table has (2 =&gt; v1, 3 =&gt; v1)&lt;/span&gt;


with wtx2 &lt;span class="token operator"&gt;=&lt;/span&gt; write_tx&lt;span class="token punctuation"&gt;(&lt;/span&gt;table&lt;span class="token punctuation"&gt;)&lt;/span&gt;:
  wtx2.put&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token number"&gt;2&lt;/span&gt;, &lt;span class="token string"&gt;'v2'&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;
  wtx2.put&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token number"&gt;4&lt;/span&gt;, &lt;span class="token string"&gt;'v2'&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;
  wtx2.publish&lt;span class="token punctuation"&gt;(&lt;/span&gt;table&lt;span class="token punctuation"&gt;)&lt;/span&gt;


&lt;span class="token comment"&gt;# table has (2 =&gt; v2, 3 =&gt; v1, 4 =&gt; v2)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;hr/&gt;&lt;/p&gt;&lt;p style="text-align:left;"&gt;This is pretty easy to follow, I think. The table is a simple hash table at this point in time.&lt;/p&gt;&lt;p style="text-align:left;"&gt;The catch is when we mix read transactions as well, like so:&lt;/p&gt;&lt;p style="text-align:left;"&gt;&lt;hr/&gt;&lt;pre class='line-numbers language-bash'&gt;&lt;code class='line-numbers language-bash'&gt;&lt;span class="token comment"&gt;# table has (2 =&gt; v2, 3 =&gt; v1, 4 =&gt; v2)&lt;/span&gt;


with rtx1 &lt;span class="token operator"&gt;=&lt;/span&gt; read_tx&lt;span class="token punctuation"&gt;(&lt;/span&gt;table&lt;span class="token punctuation"&gt;)&lt;/span&gt;:


        with wtx3 &lt;span class="token operator"&gt;=&lt;/span&gt; write_tx&lt;span class="token punctuation"&gt;(&lt;/span&gt;table&lt;span class="token punctuation"&gt;)&lt;/span&gt;:
                wtx3.put&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token number"&gt;2&lt;/span&gt;, &lt;span class="token string"&gt;'v3'&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;
                wtx3.put&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token number"&gt;3&lt;/span&gt;, &lt;span class="token string"&gt;'v3'&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;
                wtx3.put&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token number"&gt;5&lt;/span&gt;, &lt;span class="token string"&gt;'v3'&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;


                with rtx2 &lt;span class="token operator"&gt;=&lt;/span&gt; read_tx&lt;span class="token punctuation"&gt;(&lt;/span&gt;table&lt;span class="token punctuation"&gt;)&lt;/span&gt;:
                        rtx2.read&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token number"&gt;2&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token comment"&gt;# =&gt; gives, v2&lt;/span&gt;
                        rtx2.read&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token number"&gt;3&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token comment"&gt;# =&gt; gives, v1&lt;/span&gt;
                        rtx2.read&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token number"&gt;5&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token comment"&gt;# =&gt; gives, None&lt;/span&gt;


                wtx3.publish&lt;span class="token punctuation"&gt;(&lt;/span&gt;table&lt;span class="token punctuation"&gt;)&lt;/span&gt;


&lt;span class="token comment"&gt;# table has (2 =&gt; v3, 3 =&gt; v3, 4 =&gt; v2, 5 =&gt; v3)&lt;/span&gt;
&lt;span class="token comment"&gt;# but rtx2 still observe the value as they were when&lt;/span&gt;
&lt;span class="token comment"&gt;# rtx2 was created&lt;/span&gt;


        rtx2.read&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token number"&gt;2&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token comment"&gt;# =&gt; gives, v2&lt;/span&gt;
        rtx2.read&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token number"&gt;3&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token comment"&gt;# =&gt; gives, v1&lt;/span&gt;
        rtx2.read&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token number"&gt;5&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token comment"&gt;# =&gt; gives, None&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;hr/&gt;&lt;/p&gt;&lt;p style="text-align:left;"&gt;In other words, until we publish a transaction, its changes don&amp;rsquo;t take effect. And any &lt;em&gt;read&lt;/em&gt;&amp;nbsp;translation that was already started isn&amp;rsquo;t impacted. We also need this to be concurrent, so we can use the table in multiple threads (a single write transaction at a time, but potentially &lt;em&gt;many&lt;/em&gt;&amp;nbsp;read transactions). Each transaction may modify hundreds or thousands of pages, and we&amp;rsquo;ll only clear the table of old values once in a while (so it isn&amp;rsquo;t &lt;em&gt;infinite&lt;/em&gt;&amp;nbsp;growth, but may certainly reach respectable numbers of items).&lt;/p&gt;&lt;p style="text-align:left;"&gt;The implementation we have inside of RavenDB for this is &lt;em&gt;complex&lt;/em&gt;! I tried drawing that on the whiteboard to explain what was going on, and I needed both the third and fourth dimensions to illustrate the concept. &lt;/p&gt;&lt;p style="text-align:left;"&gt;Given these requirements, how would you implement this sort of data structure? &lt;/p&gt;
&lt;link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/9000.0.1/themes/prism.min.css" integrity="sha512-/mZ1FHPkg6EKcxo0fKXF51ak6Cr2ocgDi5ytaTBjsQZIH/RNs6GF6+oId/vPe3eJB836T36nXwVh/WBl/cWT4w==" crossorigin="anonymous" referrerpolicy="no-referrer" /&gt;</description><link>http://ayende.com/201281-b/challenge-efficient-snapshotable-state?Key=352251b0-97cd-4fbd-a04a-3e274ffdc2f0</link><guid>http://ayende.com/201281-b/challenge-efficient-snapshotable-state?Key=352251b0-97cd-4fbd-a04a-3e274ffdc2f0</guid><pubDate>Mon, 01 Jul 2024 12:00:00 GMT</pubDate></item><item><title>Challenge: Spot the bug</title><description>&lt;p&gt;The following bug cost me a bunch of time, can you see what I&amp;rsquo;m doing wrong?&lt;/p&gt;
&lt;blockquote&gt;
&lt;script src="https://gist.github.com/ayende/cd3d6a7efae64b9725a3a9083625d4c1.js"&gt;&lt;/script&gt;
&lt;/blockquote&gt;
&lt;p&gt;For fun, it&amp;rsquo;s so nasty because usually, it will accidentally work.&lt;/p&gt;
</description><link>http://ayende.com/200161-c/challenge-spot-the-bug?Key=277eeb3d-ea55-4969-a179-95bd86ebece9</link><guid>http://ayende.com/200161-c/challenge-spot-the-bug?Key=277eeb3d-ea55-4969-a179-95bd86ebece9</guid><pubDate>Tue, 19 Sep 2023 12:00:00 GMT</pubDate></item><item><title>Filtering negative numbers, fast: Beating memcpy()</title><description>&lt;p&gt;In the &lt;a href="https://ayende.com/blog/200102-C/filtering-negative-numbers-fast-avx"&gt;previous post&lt;/a&gt;, I was able to utilize AVX to get some nice speedups. In general, I was able to save up to 57%(!) of the runtime in processing arrays of 1M items. That is really amazing, if you think about it. But my best effort only gave me a 4% improvement when using 32M items.&lt;/p&gt;
&lt;p&gt;I decided to investigate what is going on in more depth, and I came up with the following benchmark. Given that I want to filter negative numbers, what would happen if the only negative number in the array was the first one?&lt;/p&gt;
&lt;p&gt;In other words, let&amp;rsquo;s see what happens when we could write this algorithm as the following line of code:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;array[1..].CopyTo(array);&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The idea here is that we should measure the speed of raw memory copy and see how that compares to our code.&lt;/p&gt;
&lt;p&gt;Before we dive into the results, I want to make a few things explicit. We are dealing here with arrays of long, when I&amp;rsquo;m talking about an array with 1M items, I&amp;rsquo;m actually talking about an 8MB buffer, and for the 32M items, we are talking about 256MB of memory.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m running these benchmarks on the following machine:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; AMD Ryzen 9 5950X 16-Core Processor&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; Base speed:&amp;nbsp;&amp;nbsp;&amp;nbsp; 3.40 GHz&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; L1 cache:&amp;nbsp;&amp;nbsp;&amp;nbsp; 1.0 MB&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; L2 cache:&amp;nbsp;&amp;nbsp;&amp;nbsp; 8.0 MB&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; L3 cache:&amp;nbsp;&amp;nbsp;&amp;nbsp; 64.0 MB&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; Utilization&amp;nbsp;&amp;nbsp;&amp;nbsp; 9%&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; Speed&amp;nbsp;&amp;nbsp;&amp;nbsp; 4.59 GHz&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In other words, when we look at this, the 1M items (8MB) can fit into L2 (barely, but certainly backed by the L3 cache. For the 32M items (256MB), we are far beyond what can fit in the cache, so we are probably dealing with memory bandwidth issues.&lt;/p&gt;
&lt;p&gt;I wrote the following functions to test it out:&lt;/p&gt;
&lt;blockquote&gt;
&lt;script src="https://gist.github.com/ayende/ab0deb8f900f3005f439802a10f367cb.js"&gt;&lt;/script&gt;
&lt;/blockquote&gt;
&lt;p&gt;Let&amp;rsquo;s look at what I&amp;rsquo;m actually testing here.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CopyTo() &amp;ndash; using the span native copying is the most ergonomic way to do things, I think.&lt;/li&gt;
&lt;li&gt;MemoryCopy() &amp;ndash; uses a built-in unsafe API in the framework. That eventually boils down to a &lt;a href="https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Buffer.cs#L134"&gt;heavily optimized routine&lt;/a&gt;, which&amp;hellip; calls to Memove() if the buffer overlaps (as they do in this case).&lt;/li&gt;
&lt;li&gt;MoveMemory() &amp;ndash; uses a pinvoke to call to the Windows API to do the moving of memory for us.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here are the results for the 1M case (8MB):&lt;/p&gt;
&lt;table class="table table-striped table-bordered"&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;N&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Mean&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Error&lt;/th&gt;
&lt;th style="text-align: right;"&gt;StdDev&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Ratio&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp&lt;/td&gt;
&lt;td&gt;1048599&lt;/td&gt;
&lt;td style="text-align: right;"&gt;441.4 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.78 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.58 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp_Avx&lt;/td&gt;
&lt;td&gt;1048599&lt;/td&gt;
&lt;td style="text-align: right;"&gt;141.1 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;2.70 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;2.65 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.32&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CopyTo&lt;/td&gt;
&lt;td&gt;1048599&lt;/td&gt;
&lt;td style="text-align: right;"&gt;872.8 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;11.27 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;10.54 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.98&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MemoryCopy&lt;/td&gt;
&lt;td&gt;1048599&lt;/td&gt;
&lt;td style="text-align: right;"&gt;869.7 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;7.29 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;6.46 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.97&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MoveMemory&lt;/td&gt;
&lt;td&gt;1048599&lt;/td&gt;
&lt;td style="text-align: right;"&gt;126.9 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.28 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.25 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.29&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;We can see some real surprises here. I&amp;rsquo;m using the FilterCmp (the very basic implementation) that I wrote.&lt;/p&gt;
&lt;p&gt;I cannot explain why CopyTo() and MemoryMove() are so slow.&lt;/p&gt;
&lt;p&gt;What is really impressive is that the FilterCmp_Avx() and MoveMemory() are so close in performance, and &lt;em&gt;so much faster. &lt;/em&gt;To put it in another way, we are already at a stage where we are within shouting distance from the MoveMemory() performance. That is.. really impressive.&lt;/p&gt;
&lt;p&gt;That said, what happens with 32M (256MB) ?&lt;/p&gt;
&lt;table class="table table-striped table-bordered"&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;N&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Mean&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Error&lt;/th&gt;
&lt;th style="text-align: right;"&gt;StdDev&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Ratio&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp&lt;/td&gt;
&lt;td&gt;33554455&lt;/td&gt;
&lt;td style="text-align: right;"&gt;22,763.6 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;157.23 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;147.07 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp_Avx&lt;/td&gt;
&lt;td&gt;33554455&lt;/td&gt;
&lt;td style="text-align: right;"&gt;20,122.3 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;214.10 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;200.27 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.88&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CopyTo&lt;/td&gt;
&lt;td&gt;33554455&lt;/td&gt;
&lt;td style="text-align: right;"&gt;27,660.1 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;91.41 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;76.33 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.22&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MemoryCopy&lt;/td&gt;
&lt;td&gt;33554455&lt;/td&gt;
&lt;td style="text-align: right;"&gt;27,618.4 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;136.16 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;127.36 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.21&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MoveMemory&lt;/td&gt;
&lt;td&gt;33554455&lt;/td&gt;
&lt;td style="text-align: right;"&gt;20,152.0 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;166.66 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;155.89 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.89&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Now we are &lt;em&gt;faster&lt;/em&gt; in the FilterCmp_Avx than MoveMemory. That is&amp;hellip; a pretty big wow, and a really nice close for this blog post series, right? Except that we won&amp;rsquo;t be stopping here.&lt;/p&gt;
&lt;p&gt;The way the task I set out works, we are actually filtering just the first item out, and then we are basically copying the memory. Let&amp;rsquo;s do some math: 256MB in 20.1ms means 12.4 GB/sec!&lt;/p&gt;
&lt;p&gt;On this system, I have the following memory setup:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 64.0 GB&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; Speed:&amp;nbsp;&amp;nbsp;&amp;nbsp; 2133 MHz&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; Slots used:&amp;nbsp;&amp;nbsp;&amp;nbsp; 4 of 4&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; Form factor:&amp;nbsp;&amp;nbsp;&amp;nbsp; DIMM&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; Hardware reserved:&amp;nbsp;&amp;nbsp;&amp;nbsp; 55.2 MB&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I&amp;rsquo;m using DDR4 memory, so &lt;a href="https://www.softwareok.com/?seite=faq-This-and-That-or-Other&amp;amp;faq=74"&gt;I can expect a maximum speed of 17GB/sec.&lt;/a&gt; In theory, I might be able to get more if the memory is located on different DIMMs, but for the size in question, that is not likely.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m going to skip the training montage of VTune, understanding memory architecture and figuring out what is actually going on.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s drop everything and look at what we have with just AVX vs. MoveMemory:&lt;/p&gt;
&lt;table class="table table-striped table-bordered"&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;N&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Mean&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Error&lt;/th&gt;
&lt;th style="text-align: right;"&gt;StdDev&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Median&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Ratio&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp_Avx&lt;/td&gt;
&lt;td&gt;1048599&lt;/td&gt;
&lt;td style="text-align: right;"&gt;141.6 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;2.28 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;2.02 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;141.8 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MoveMemory&lt;/td&gt;
&lt;td&gt;1048599&lt;/td&gt;
&lt;td style="text-align: right;"&gt;126.8 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.25 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.19 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;126.8 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp_Avx&lt;/td&gt;
&lt;td&gt;33554455&lt;/td&gt;
&lt;td style="text-align: right;"&gt;21,105.5 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;408.65 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;963.25 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;20,770.4 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.08&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MoveMemory&lt;/td&gt;
&lt;td&gt;33554455&lt;/td&gt;
&lt;td style="text-align: right;"&gt;20,142.5 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;245.08 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;204.66 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;20,108.2 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.00&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The new baseline is MoveMemory, and in this run, we can see that the AVX code is slightly &lt;em&gt;slower&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s sadly not uncommon to see numbers shift by those ranges when we are testing such micro-optimizations, mostly because we are subject to so many variables that can affect performance. In this case, I dropped all the other benchmarks, which may have changed things.&lt;/p&gt;
&lt;p&gt;At any rate, using those numbers, we have 12.4GB/sec for MoveMemory() and 11.8GB/sec for the AVX version. The &lt;em&gt;hardware &lt;/em&gt;maximum speed is 17GB/sec. So we are quite close to what can be done.&lt;/p&gt;
&lt;p&gt;For that matter, I would like to point out that the trivial code completed the task in 11GB/sec, so that is &lt;em&gt;quite&lt;/em&gt; respectable and shows that the issue here is literally getting the memory fast enough to the CPU.&lt;/p&gt;
&lt;p&gt;Can we do something about that? I made a &lt;a href="https://gist.github.com/ayende/ad5ae9c3a4519b116db920f2b9ea10cd"&gt;pretty small change&lt;/a&gt; to the AVX version, like so:&lt;/p&gt;
&lt;blockquote&gt;
&lt;script src="https://gist.github.com/ayende/bdb8612f1c05dd32810a4164cd04f6a5.js"&gt;&lt;/script&gt;
&lt;/blockquote&gt;
&lt;p&gt;What are we actually doing here? Instead of loading the value and immediately using it, we are loading the &lt;em&gt;next&lt;/em&gt; value, then we are executing the loop and when we iterate again, we will start loading the next value and process the current one. The idea is to parallelize load and compute at the instruction level.&lt;/p&gt;
&lt;p&gt;Sadly, that didn&amp;rsquo;t seem to do the trick. I saw a 19% additional cost for that version compared to the vanilla AVX one on the 8MB run and a 2% additional cost on the 256MB run.&lt;/p&gt;
&lt;p&gt;I then realized that there was one really important test that I had to also make, and wrote the following:&lt;/p&gt;
&lt;blockquote&gt;
&lt;script src="https://gist.github.com/ayende/61cda608199a25705d0139adbcaeb1f1.js"&gt;&lt;/script&gt;
&lt;/blockquote&gt;
&lt;p&gt;In other words, let's test the speed of moving memory and filling memory as fast as we possibly can. Here are the results:&lt;/p&gt;
&lt;table class="table table-striped table-bordered"&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;N&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Mean&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Error&lt;/th&gt;
&lt;th style="text-align: right;"&gt;StdDev&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Ratio&lt;/th&gt;
&lt;th style="text-align: right;"&gt;RatioSD&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Code Size&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;MoveMemory&lt;/td&gt;
&lt;td&gt;1048599&lt;/td&gt;
&lt;td style="text-align: right;"&gt;126.8 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.36 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.33 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.25&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;270 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FillMemory&lt;/td&gt;
&lt;td&gt;1048599&lt;/td&gt;
&lt;td style="text-align: right;"&gt;513.5 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;10.05 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;10.32 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;351 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MoveMemory&lt;/td&gt;
&lt;td&gt;33554455&lt;/td&gt;
&lt;td style="text-align: right;"&gt;20,022.5 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;395.35 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;500.00 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.26&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.02&lt;/td&gt;
&lt;td style="text-align: right;"&gt;270 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FillMemory&lt;/td&gt;
&lt;td&gt;33554455&lt;/td&gt;
&lt;td style="text-align: right;"&gt;15,822.4 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;19.85 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;17.60 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;351 B&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;This is really interesting, for a small buffer (8MB), MoveMemory is somehow faster. I don&amp;rsquo;t have a way to explain that, but it has been a pretty consistent result in my tests.&lt;/p&gt;
&lt;p&gt;For the large buffer (256MB), we are seeing results that make more sense to me.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;MoveMemory &amp;ndash; 12.5 GB / sec&lt;/li&gt;
&lt;li&gt;FIllMemory &amp;ndash; 15.8 GB / sec&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In other words, for MoveMemory, we are both reading and writing, so we are paying for memory bandwidth in both directions. For filling the memory, we are only writing, so we can get better performance (no need for reads).&lt;/p&gt;
&lt;p&gt;In other words, we are talking about hitting the real physical limits of what the hardware can do. There are all sorts of tricks that one can pull, but when we are this close to the limit, they are almost always context-sensitive and dependent on many factors.&lt;/p&gt;
&lt;p&gt;To conclude, here are my final results:&lt;/p&gt;
&lt;table class="table table-striped table-bordered"&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;N&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Mean&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Error&lt;/th&gt;
&lt;th style="text-align: right;"&gt;StdDev&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Ratio&lt;/th&gt;
&lt;th style="text-align: right;"&gt;RatioSD&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Code Size&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp_Avx&lt;/td&gt;
&lt;td&gt;1048599&lt;/td&gt;
&lt;td style="text-align: right;"&gt;307.9 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;6.15 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;12.84 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.99&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.05&lt;/td&gt;
&lt;td style="text-align: right;"&gt;270 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp_Avx_Next&lt;/td&gt;
&lt;td&gt;1048599&lt;/td&gt;
&lt;td style="text-align: right;"&gt;308.4 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;6.07 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;9.26 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.99&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.03&lt;/td&gt;
&lt;td style="text-align: right;"&gt;270 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CopyTo&lt;/td&gt;
&lt;td&gt;1048599&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1,043.7 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;15.96 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;14.93 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;3.37&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.11&lt;/td&gt;
&lt;td style="text-align: right;"&gt;452 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ArrayCopy&lt;/td&gt;
&lt;td&gt;1048599&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1,046.7 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;15.92 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;14.89 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;3.38&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.14&lt;/td&gt;
&lt;td style="text-align: right;"&gt;266 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UnsafeCopy&lt;/td&gt;
&lt;td&gt;1048599&lt;/td&gt;
&lt;td style="text-align: right;"&gt;309.5 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;6.15 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;8.83 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.04&lt;/td&gt;
&lt;td style="text-align: right;"&gt;133 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MoveMemory&lt;/td&gt;
&lt;td&gt;1048599&lt;/td&gt;
&lt;td style="text-align: right;"&gt;310.8 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;6.17 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;9.43 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;270 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp_Avx&lt;/td&gt;
&lt;td&gt;33554455&lt;/td&gt;
&lt;td style="text-align: right;"&gt;24,013.1 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;451.09 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;443.03 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.98&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.02&lt;/td&gt;
&lt;td style="text-align: right;"&gt;270 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp_Avx_Next&lt;/td&gt;
&lt;td&gt;33554455&lt;/td&gt;
&lt;td style="text-align: right;"&gt;24,437.8 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;179.88 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;168.26 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.01&lt;/td&gt;
&lt;td style="text-align: right;"&gt;270 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CopyTo&lt;/td&gt;
&lt;td&gt;33554455&lt;/td&gt;
&lt;td style="text-align: right;"&gt;32,931.6 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;416.57 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;389.66 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.35&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.02&lt;/td&gt;
&lt;td style="text-align: right;"&gt;452 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ArrayCopy&lt;/td&gt;
&lt;td&gt;33554455&lt;/td&gt;
&lt;td style="text-align: right;"&gt;32,538.0 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;463.00 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;433.09 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.33&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.02&lt;/td&gt;
&lt;td style="text-align: right;"&gt;266 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UnsafeCopy&lt;/td&gt;
&lt;td&gt;33554455&lt;/td&gt;
&lt;td style="text-align: right;"&gt;24,386.9 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;209.98 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;196.42 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.01&lt;/td&gt;
&lt;td style="text-align: right;"&gt;133 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MoveMemory&lt;/td&gt;
&lt;td&gt;33554455&lt;/td&gt;
&lt;td style="text-align: right;"&gt;24,427.8 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;293.75 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;274.78 us&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;270 B&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;As you can see, just the AVX version is comparable or (slightly) beating the MoveMemory function.&lt;/p&gt;
&lt;p&gt;I tried things like prefetching memory, both the next item, the next cache item and from the next page, using non-temporal load and stores and many other things, but this is a pretty tough challenge.&lt;/p&gt;
&lt;p&gt;What is really interesting is that the original, very simple and obvious implementation, clocked at 11 GB/sec. After pulling pretty much all the stops and tricks, I was able to hit 12.5 GB / sec.&lt;/p&gt;
&lt;p&gt;I don&amp;rsquo;t think anyone can look / update / understand the resulting code in any way without going through deep meditation. That is &lt;em&gt;not&lt;/em&gt; a bad result at all. And along the way, I learned quite a bit about how the lowest level of the machine architecture&amp;nbsp;is working.&lt;/p&gt;
</description><link>http://ayende.com/200129-a/filtering-negative-numbers-fast-beating-memcpy?Key=150a4bae-c691-4f65-a2b6-cbabae9f8832</link><guid>http://ayende.com/200129-a/filtering-negative-numbers-fast-beating-memcpy?Key=150a4bae-c691-4f65-a2b6-cbabae9f8832</guid><pubDate>Fri, 15 Sep 2023 12:00:00 GMT</pubDate></item><item><title>Filtering negative numbers, fast: AVX</title><description>&lt;p&gt;&lt;a href="https://ayende.com/blog/200101-C/filtering-negative-numbers-fast-unroll?key=617ce0ba58e7446092686f584c90c4cd"&gt;In the previous post&lt;/a&gt; I discussed how we can optimize the filtering of negative numbers by unrolling the loop, looked into branchless code and in general was able to improve performance by up to 15% from the initial version we started with. We pushed as much as we could on what can be done using scalar code. Now it is the time to open a whole new world and see what we can do when we implement this challenge using vector instructions.&lt;/p&gt;
&lt;p&gt;The key problem with such tasks is that SIMD, AVX and their friends were designed by&amp;hellip; an interesting process using a perspective that makes sense if you can see in a couple of additional dimensions. I assume that at least some of that is implementation constraints, but the key issue is that when you start using SIMD, you realize that you don&amp;rsquo;t have general-purpose instructions. Instead, you have a lot of dedicated instructions that are doing one thing, hopefully well, and it is your role to compose them into something that would make sense. Oftentimes, you need to turn the solution on its head in order to successfully solve it using SIMD. The benefit, of course, is that you can get quite an amazing boost in speed when you do this.&lt;/p&gt;
&lt;p&gt;The algorithm we use is basically to scan the list of entries and copy to the start of the list only those items that are positive. How can we do that using SIMD? The whole point here is that we want to be able to operate on multiple data, but this particular task isn&amp;rsquo;t trivial. I&amp;rsquo;m going to show the code first, then discuss what it does in detail:&lt;/p&gt;
&lt;blockquote&gt;
&lt;script src="https://gist.github.com/ayende/3982fadce72c64a99f5c3a5a1ea8d2c2.js"&gt;&lt;/script&gt;
&lt;/blockquote&gt;
&lt;p&gt;We start with the usual check (if you&amp;rsquo;ll recall, that ensures that the JIT knows to elide some range checks, then we load the PremuteTable. For now, just assume that this is magic (and it is). The first interesting thing happens when we start iterating over the loop. Unlike before, now we do that in chunks of 4 int64 elements at a time. Inside the loop, we start by loading a vector of int64 and then we do the first odd thing. We call &lt;em&gt;ExtractMostSignificantBits&lt;/em&gt;(), since the sign bit is used to mark whether&amp;nbsp;a number if negative or not. That means that I can use a single instruction to get an integer with the bits set for all the negative numbers. That is particularly juicy for what we need, since there is no need for comparisons, etc.&lt;/p&gt;
&lt;p&gt;If the mask we got is all zeroes, it means that all the numbers we loaded to the vector are positives, so we can write them as-is to the output and move to the next part. Things get interesting when that isn&amp;rsquo;t the case.&lt;/p&gt;
&lt;p&gt;We load a permute value using some shenanigans (we&amp;rsquo;ll touch on that shortly) and call the &lt;em&gt;PermuteVar8x32&lt;/em&gt;() method. The idea here is that we pack all the non-negative numbers to the start of the vector, then we write the vector to the output. The key here is that when we do that, we increment the output index only by the number of valid values.&amp;nbsp; The rest of this method just handles the remainder that does not fit into a vector.&lt;/p&gt;
&lt;p&gt;The hard part in this implementation was to figure out how to handle the scenario where we loaded some negative numbers. We need a way to filter them, after all. But there is no SIMD instruction that allows us to do so. Luckily, we have the &lt;a href="https://learn.microsoft.com/en-us/dotnet/api/system.runtime.intrinsics.x86.avx2.permutevar8x32?view=net-7.0"&gt;Avx2.PermuteVar8x32&lt;/a&gt;() method to help here. To confuse things, we don&amp;rsquo;t actually want to deal with 8x32 values. We want to deal with 4x64 values. There &lt;em&gt;is&lt;/em&gt; &lt;a href="https://learn.microsoft.com/en-us/dotnet/api/system.runtime.intrinsics.x86.avx2.permute4x64?view=net-7.0#system-runtime-intrinsics-x86-avx2-permute4x64(system-runtime-intrinsics-vector256((system-int64))-system-byte)"&gt;Avx2.Permute4x64&lt;/a&gt;() method, and it will work quite nicely, with a single caveat. This method assumes that you are going to pass it a constant value. We don&amp;rsquo;t have such a constant, we need to be able to provide that based on whatever the masked bits will give us.&lt;/p&gt;
&lt;p&gt;So how do we deal with this issue of filtering with SIMD? We need to move all the values we care about to the front of the vector. We have the method to do that, &lt;em&gt;PermuteVar8x32&lt;/em&gt;() method, and we just need to figure out how to actually make use of this. &lt;em&gt;PermuteVar8x32&lt;/em&gt;() accepts an input vector as well as a vector of the premutation you want to make. In this case, we are basing this on the 4 top bits of the 4 elements vector of int64. As such, there are a total of 16 options available to us. We have to deal with 32bits values rather than 64bits, but that isn&amp;rsquo;t that much of a problem.&lt;/p&gt;
&lt;p&gt;Here is the premutation table that we&amp;rsquo;ll be using:&lt;/p&gt;
&lt;blockquote&gt;
&lt;script src="https://gist.github.com/ayende/9b1ace0341874c50403342346928299c.js"&gt;&lt;/script&gt;
&lt;/blockquote&gt;
&lt;p&gt;What you can see here is that when we have a 1 in the bits (shown in comments) we&amp;rsquo;ll not copy that to the vector. Let&amp;rsquo;s take a look at the entry of &lt;em&gt;0101&lt;/em&gt;, which may be caused by the following values [1,-2,3,-4].&lt;/p&gt;
&lt;p&gt;When we look at the right entry at index #5 in the table: 2,3,6,7,0,0,0,0&lt;/p&gt;
&lt;p&gt;What does this mean? It means that we want to put the 2nd int64 element in the source vector and move it as the first element of the destination vector, take the 3rd element from the source as the second element in the destination and discard the rest (marked as 0,0,0,0 in the table).&lt;/p&gt;
&lt;p&gt;This is a bit hard to follow because we have to compose the value out of the individual 32 bits words, but it works quite well. Or, at least, it would work, but not as efficiently. This is because we would need to load the PermuteTableInts into a variable and access it, but there are better ways to deal with it. We can also ask the JIT to embed the value directly. The problem is that the pattern that the JIT recognizes is limited to ReadOnlySpan&amp;lt;byte&amp;gt;, which means that the already non-trivial int32 table got turned into this:&lt;/p&gt;
&lt;blockquote&gt;
&lt;script src="https://gist.github.com/ayende/e45b5ddce0d41879912a7dbed1958345.js"&gt;&lt;/script&gt;
&lt;/blockquote&gt;
&lt;p&gt;This is the exact same data as before, but using ReadOnlySpan&amp;lt;byte&amp;gt; means that the JIT can package that inside the data section and treat it as a constant value.&lt;/p&gt;
&lt;p&gt;The code was heavily optimized, to the point where I noticed &lt;a href="https://github.com/dotnet/runtime/issues/91824"&gt;a JIT bug&lt;/a&gt; where these two versions of the code give different assembly output:&lt;/p&gt;
&lt;blockquote&gt;
&lt;script src="https://gist.github.com/ayende/ea4aefe4e0c956692558ad2a4c880d05.js"&gt;&lt;/script&gt;
&lt;/blockquote&gt;
&lt;p&gt;Here is what we get out:&lt;/p&gt;
&lt;blockquote&gt;
&lt;script src="https://gist.github.com/ayende/5fe1d8ae986dcfacf758bbaa301d1b27.js"&gt;&lt;/script&gt;
&lt;/blockquote&gt;
&lt;p&gt;This looks like an unintended consequence of Roslyn and the JIT each doing their (separate jobs), but not reaching the end goal. Constant folding looks like it is done mostly by Roslyn, but it does a scan like that from the left, so it wouldn&amp;rsquo;t convert $A * 4 * 8 to $A * 32. That is because it stopped evaluating the constants when it found a variable. When we add parenthesis, we isolate the value and now understand that we can fold it.&lt;/p&gt;
&lt;p&gt;Speaking of assembly, here is the annotated assembly version of the code:&lt;/p&gt;
&lt;blockquote&gt;
&lt;script src="https://gist.github.com/ayende/7e144aacf9385ee7ec4ebd7d1428a5ac.js"&gt;&lt;/script&gt;
&lt;/blockquote&gt;
&lt;p&gt;And after all of this work, where are we standing?&lt;/p&gt;
&lt;table class="table table-striped table-bordered"&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;N&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Mean&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Error&lt;/th&gt;
&lt;th style="text-align: right;"&gt;StdDev&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Ratio&lt;/th&gt;
&lt;th style="text-align: right;"&gt;RatioSD&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Code Size&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp&lt;/td&gt;
&lt;td&gt;23&lt;/td&gt;
&lt;td style="text-align: right;"&gt;285.7 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;3.84 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;3.59 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;411 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp_NoRangeCheck&lt;/td&gt;
&lt;td&gt;23&lt;/td&gt;
&lt;td style="text-align: right;"&gt;272.6 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;3.98 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;3.53 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.95&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.01&lt;/td&gt;
&lt;td style="text-align: right;"&gt;397 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp_Unroll_8&lt;/td&gt;
&lt;td&gt;23&lt;/td&gt;
&lt;td style="text-align: right;"&gt;261.4 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.27 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.18 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.91&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.01&lt;/td&gt;
&lt;td style="text-align: right;"&gt;672 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp_Avx&lt;/td&gt;
&lt;td&gt;23&lt;/td&gt;
&lt;td style="text-align: right;"&gt;261.6 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.37 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.28 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.92&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.01&lt;/td&gt;
&lt;td style="text-align: right;"&gt;521 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp&lt;/td&gt;
&lt;td&gt;1047&lt;/td&gt;
&lt;td style="text-align: right;"&gt;758.7 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.51 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.42 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;411 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp_NoRangeCheck&lt;/td&gt;
&lt;td&gt;1047&lt;/td&gt;
&lt;td style="text-align: right;"&gt;756.8 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.83 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.53 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;397 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp_Unroll_8&lt;/td&gt;
&lt;td&gt;1047&lt;/td&gt;
&lt;td style="text-align: right;"&gt;640.4 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.94 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.82 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.84&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;672 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp_Avx&lt;/td&gt;
&lt;td&gt;1047&lt;/td&gt;
&lt;td style="text-align: right;"&gt;426.0 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.62 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.52 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.56&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;521 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp&lt;/td&gt;
&lt;td&gt;1048599&lt;/td&gt;
&lt;td style="text-align: right;"&gt;502,681.4 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;3,732.37 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;3,491.26 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;411 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp_NoRangeCheck&lt;/td&gt;
&lt;td&gt;1048599&lt;/td&gt;
&lt;td style="text-align: right;"&gt;499,472.7 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;6,082.44 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;5,689.52 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.99&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.01&lt;/td&gt;
&lt;td style="text-align: right;"&gt;397 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp_Unroll_8&lt;/td&gt;
&lt;td&gt;1048599&lt;/td&gt;
&lt;td style="text-align: right;"&gt;425,800.3 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;352.45 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;312.44 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.85&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.01&lt;/td&gt;
&lt;td style="text-align: right;"&gt;672 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp_Avx&lt;/td&gt;
&lt;td&gt;1048599&lt;/td&gt;
&lt;td style="text-align: right;"&gt;218,075.1 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;212.40 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;188.29 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.43&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;521 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp&lt;/td&gt;
&lt;td&gt;33554455&lt;/td&gt;
&lt;td style="text-align: right;"&gt;29,820,978.8 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;73,461.68 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;61,343.83 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;411 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp_NoRangeCheck&lt;/td&gt;
&lt;td&gt;33554455&lt;/td&gt;
&lt;td style="text-align: right;"&gt;29,471,229.2 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;73,805.56 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;69,037.77 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.99&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;397 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp_Unroll_8&lt;/td&gt;
&lt;td&gt;33554455&lt;/td&gt;
&lt;td style="text-align: right;"&gt;29,234,413.8 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;67,597.45 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;63,230.70 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.98&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;672 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp_Avx&lt;/td&gt;
&lt;td&gt;33554455&lt;/td&gt;
&lt;td style="text-align: right;"&gt;28,498,115.4 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;71,661.94 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;67,032.62 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.96&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;521 B&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;So it seems that the idea of using SIMD instruction has a lot of merit. Moving from the original code to the final version, we see that we can complete the same task in up to half the time.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m not quite sure why we aren&amp;rsquo;t seeing the same sort of performance on the 32M, but I suspect that this is likely because we far exceed the CPU cache and we have to fetch it all from memory, so that is as fast as it &lt;em&gt;can&lt;/em&gt; go.&lt;/p&gt;
&lt;p&gt;If you are interested in learning more, &lt;a href="https://lemire.me/blog/2022/06/23/filtering-numbers-quickly-with-sve-on-amazon-graviton-3-processors/"&gt;Lemire solves the same problem in SVE (SIMD for ARM)&lt;/a&gt; and &lt;a href="https://quickwit.io/blog/simd-range"&gt;Paul has a similar approach in Rust&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If you can think of further optimizations, I would love to hear your ideas.&lt;/p&gt;
</description><link>http://ayende.com/200102-c/filtering-negative-numbers-fast-avx?Key=ecacbd27-fe9d-4a7c-8824-411229bbec91</link><guid>http://ayende.com/200102-c/filtering-negative-numbers-fast-avx?Key=ecacbd27-fe9d-4a7c-8824-411229bbec91</guid><pubDate>Wed, 13 Sep 2023 12:00:00 GMT</pubDate></item><item><title>Filtering negative numbers, fast: Unroll</title><description>&lt;p&gt;&lt;a href="https://ayende.com/blog/200100-C/filtering-negative-numbers-fast-scalar?key=457d45e9014e4352ac3dab2aa2bd4e04"&gt;In the previous post,&lt;/a&gt; we looked into what it would take to reduce the cost of filtering negative numbers. We got into the assembly and analyzed exactly what was going on. In terms of this directly, I don&amp;rsquo;t think that even hand-optimized assembly would take us further. Let&amp;rsquo;s see if there are other options that are available for us to get better speed.&lt;/p&gt;
&lt;p&gt;The first thing that pops to mind here is to do a loop unrolling. After all, we have a very tight loop, if we can do more work per loop iteration, we might get better performance, no? Here is my first version:&lt;/p&gt;
&lt;blockquote&gt;
&lt;script src="https://gist.github.com/ayende/ce56c4517491bc8c08961c9d147213f1.js"&gt;&lt;/script&gt;
&lt;/blockquote&gt;
&lt;p&gt;And here are the benchmark results:&lt;/p&gt;
&lt;table class="table table-striped table-bordered"&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;N&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Mean&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Error&lt;/th&gt;
&lt;th style="text-align: right;"&gt;StdDev&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Ratio&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Code Size&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp&lt;/td&gt;
&lt;td&gt;23&lt;/td&gt;
&lt;td style="text-align: right;"&gt;274.6 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.40 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.35 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;411 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp_Unroll&lt;/td&gt;
&lt;td&gt;23&lt;/td&gt;
&lt;td style="text-align: right;"&gt;257.5 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.94 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.83 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.94&lt;/td&gt;
&lt;td style="text-align: right;"&gt;606 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp&lt;/td&gt;
&lt;td&gt;1047&lt;/td&gt;
&lt;td style="text-align: right;"&gt;748.1 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;2.91 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;2.58 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;411 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp_Unroll&lt;/td&gt;
&lt;td&gt;1047&lt;/td&gt;
&lt;td style="text-align: right;"&gt;702.5 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;5.23 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;4.89 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.94&lt;/td&gt;
&lt;td style="text-align: right;"&gt;606 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp&lt;/td&gt;
&lt;td&gt;1048599&lt;/td&gt;
&lt;td style="text-align: right;"&gt;501,545.2 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;4,985.42 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;4,419.45 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;411 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp_Unroll&lt;/td&gt;
&lt;td&gt;1048599&lt;/td&gt;
&lt;td style="text-align: right;"&gt;446,311.1 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;3,131.42 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;2,929.14 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.89&lt;/td&gt;
&lt;td style="text-align: right;"&gt;606 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp&lt;/td&gt;
&lt;td&gt;33554455&lt;/td&gt;
&lt;td style="text-align: right;"&gt;29,637,052.2 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;184,796.17 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;163,817.00 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;411 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp_Unroll&lt;/td&gt;
&lt;td&gt;33554455&lt;/td&gt;
&lt;td style="text-align: right;"&gt;29,275,060.6 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;145,756.53 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;121,713.31 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.99&lt;/td&gt;
&lt;td style="text-align: right;"&gt;606 B&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;That is quite a jump, 6% &amp;ndash; 11% savings is no joke. Let&amp;rsquo;s look at what is actually going on at the assembly level and see if we can optimize this further.&lt;/p&gt;
&lt;p&gt;As expected, the code size is bigger, 264 bytes versus the 55 we previously got. But more importantly, we got the range check back, and a lot of them:&lt;/p&gt;
&lt;blockquote&gt;
&lt;script src="https://gist.github.com/ayende/d2265156014143ece0ac3901a430428e.js"&gt;&lt;/script&gt;
&lt;/blockquote&gt;
&lt;p&gt;The JIT isn&amp;rsquo;t able to reason about our first for loop and see that all our accesses are within bounds, which leads to doing a lot of range checks, and likely slows us down. Even with that, we are still showing significant improvements here.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s see what we can do with this:&lt;/p&gt;
&lt;blockquote&gt;
&lt;script src="https://gist.github.com/ayende/48719ff7e902113847be504a56473a06.js"&gt;&lt;/script&gt;
&lt;/blockquote&gt;
&lt;p&gt;With that, we expect to have no range checks and still be able to benefit from the unrolling.&lt;/p&gt;
&lt;table class="table table-striped table-bordered"&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;N&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Mean&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Error&lt;/th&gt;
&lt;th style="text-align: right;"&gt;StdDev&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Ratio&lt;/th&gt;
&lt;th style="text-align: right;"&gt;RatioSD&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Code Size&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp&lt;/td&gt;
&lt;td&gt;23&lt;/td&gt;
&lt;td style="text-align: right;"&gt;275.4 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;2.31 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;2.05 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;411 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp_Unroll&lt;/td&gt;
&lt;td&gt;23&lt;/td&gt;
&lt;td style="text-align: right;"&gt;253.6 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;2.59 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;2.42 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.92&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.01&lt;/td&gt;
&lt;td style="text-align: right;"&gt;563 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp&lt;/td&gt;
&lt;td&gt;1047&lt;/td&gt;
&lt;td style="text-align: right;"&gt;741.6 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;5.95 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;5.28 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;411 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp_Unroll&lt;/td&gt;
&lt;td&gt;1047&lt;/td&gt;
&lt;td style="text-align: right;"&gt;665.5 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;2.38 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;2.22 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.90&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.01&lt;/td&gt;
&lt;td style="text-align: right;"&gt;563 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp&lt;/td&gt;
&lt;td&gt;1048599&lt;/td&gt;
&lt;td style="text-align: right;"&gt;497,624.9 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;3,904.39 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;3,652.17 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;411 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp_Unroll&lt;/td&gt;
&lt;td&gt;1048599&lt;/td&gt;
&lt;td style="text-align: right;"&gt;444,489.0 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;2,524.45 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;2,361.38 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.89&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.01&lt;/td&gt;
&lt;td style="text-align: right;"&gt;563 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp&lt;/td&gt;
&lt;td&gt;33554455&lt;/td&gt;
&lt;td style="text-align: right;"&gt;29,781,164.3 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;361,625.63 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;320,571.70 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;411 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp_Unroll&lt;/td&gt;
&lt;td&gt;33554455&lt;/td&gt;
&lt;td style="text-align: right;"&gt;29,954,093.9 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;588,614.32 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;916,401.59 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.01&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.04&lt;/td&gt;
&lt;td style="text-align: right;"&gt;563 B&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;That helped, by quite a lot, it seems, for most cases, the 32M items case, however, was slightly slower, which is quite a surprise.&lt;/p&gt;
&lt;p&gt;Looking at the assembly, I can see that we still have branches, like so:&lt;/p&gt;
&lt;blockquote&gt;
&lt;script src="https://gist.github.com/ayende/5ffa2b6ad85b5039c9145996843e1608.js"&gt;&lt;/script&gt;
&lt;/blockquote&gt;
&lt;p&gt;And here is why this is the case:&lt;/p&gt;
&lt;blockquote&gt;
&lt;script src="https://gist.github.com/ayende/77b7f29167e7369f80454ee31a26def9.js"&gt;&lt;/script&gt;
&lt;/blockquote&gt;
&lt;p&gt;Now, can we do better here? It turns out that we can, by using a branchless version of the operation. Here is another way to write the same thing:&lt;/p&gt;
&lt;blockquote&gt;
&lt;script src="https://gist.github.com/ayende/f2547983bed2c383991d0cb468c53f65.js"&gt;&lt;/script&gt;
&lt;/blockquote&gt;
&lt;p&gt;What happens here is that we are unconditionally setting the value in the array, but only increment if the value is greater than or equal to zero. That saves us in branches and will likely result in less code. In fact, let&amp;rsquo;s see what sort of assembly the JIT will output:&lt;/p&gt;
&lt;blockquote&gt;
&lt;script src="https://gist.github.com/ayende/205e96a945978d829b6abb936760edb1.js"&gt;&lt;/script&gt;
&lt;/blockquote&gt;
&lt;p&gt;What about the performance? I decided to pit the two versions (normal and branchless) head to head and see what this will give us:&lt;/p&gt;
&lt;table class="table table-striped table-bordered"&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;N&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Mean&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Error&lt;/th&gt;
&lt;th style="text-align: right;"&gt;StdDev&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Ratio&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Code Size&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp_Unroll&lt;/td&gt;
&lt;td&gt;23&lt;/td&gt;
&lt;td style="text-align: right;"&gt;276.3 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;4.13 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;3.86 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;411 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp_Unroll_Branchleses&lt;/td&gt;
&lt;td&gt;23&lt;/td&gt;
&lt;td style="text-align: right;"&gt;263.6 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.95 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.84 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.96&lt;/td&gt;
&lt;td style="text-align: right;"&gt;547 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp_Unroll&lt;/td&gt;
&lt;td&gt;1047&lt;/td&gt;
&lt;td style="text-align: right;"&gt;743.7 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;9.41 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;8.80 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;411 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp_Unroll_Branchleses&lt;/td&gt;
&lt;td&gt;1047&lt;/td&gt;
&lt;td style="text-align: right;"&gt;733.3 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;3.54 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;3.31 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.99&lt;/td&gt;
&lt;td style="text-align: right;"&gt;547 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp_Unroll&lt;/td&gt;
&lt;td&gt;1048599&lt;/td&gt;
&lt;td style="text-align: right;"&gt;502,631.1 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;3,641.47 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;3,406.23 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;411 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp_Unroll_Branchleses&lt;/td&gt;
&lt;td&gt;1048599&lt;/td&gt;
&lt;td style="text-align: right;"&gt;495,590.9 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;335.33 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;297.26 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.99&lt;/td&gt;
&lt;td style="text-align: right;"&gt;547 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp_Unroll&lt;/td&gt;
&lt;td&gt;33554455&lt;/td&gt;
&lt;td style="text-align: right;"&gt;29,356,331.7 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;207,133.86 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;172,966.15 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;411 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp_Unroll_Branchleses&lt;/td&gt;
&lt;td&gt;33554455&lt;/td&gt;
&lt;td style="text-align: right;"&gt;29,709,835.1 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;86,129.58 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;71,922.10 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.01&lt;/td&gt;
&lt;td style="text-align: right;"&gt;547 B&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Surprisingly enough, it looks like the branchless version is very slightly &lt;em&gt;slower&lt;/em&gt;. That is a surprise, since I would expect reducing the branches to be more efficient.&lt;/p&gt;
&lt;p&gt;Looking at the assembly of those two, the branchless version is slightly bigger (10 bytes, not that meaningful). I think that the key here is that there is a 0.5% chance of actually hitting the branch, which is pretty low. That means that the branch predictor can likely do a really good job and we aren&amp;rsquo;t going to see any big benefits from the branchless version.&lt;/p&gt;
&lt;p&gt;That said&amp;hellip; what would happen if we tested that with 5% negatives? That difference in behavior may cause us to see a different result. I tried that, and the results were quite surprising. In the case of the 1K and 32M items, we see a slightl&amp;nbsp;&lt;em&gt;cost &lt;/em&gt;for the branchless version (additional 1% &amp;ndash; 4%) while for the 1M entries there is an 18% &lt;em&gt;reduction&lt;/em&gt; in latency for the branchless version.&lt;/p&gt;
&lt;p&gt;I ran the tests again with a 15% change of negative, to see what would happen. In that case, we get:&lt;/p&gt;
&lt;table class="table table-striped table-bordered"&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;N&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Mean&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Error&lt;/th&gt;
&lt;th style="text-align: right;"&gt;StdDev&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Ratio&lt;/th&gt;
&lt;th style="text-align: right;"&gt;RatioSD&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Code Size&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp_Unroll&lt;/td&gt;
&lt;td&gt;23&lt;/td&gt;
&lt;td style="text-align: right;"&gt;273.5 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;3.66 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;3.42 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;537 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp_Unroll_Branchleses&lt;/td&gt;
&lt;td&gt;23&lt;/td&gt;
&lt;td style="text-align: right;"&gt;280.2 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;4.85 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;4.30 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.03&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.02&lt;/td&gt;
&lt;td style="text-align: right;"&gt;547 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp_Unroll&lt;/td&gt;
&lt;td&gt;1047&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1,675.7 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;29.55 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;27.64 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;537 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp_Unroll_Branchleses&lt;/td&gt;
&lt;td&gt;1047&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1,676.3 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;16.97 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;14.17 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.02&lt;/td&gt;
&lt;td style="text-align: right;"&gt;547 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp_Unroll&lt;/td&gt;
&lt;td&gt;1048599&lt;/td&gt;
&lt;td style="text-align: right;"&gt;2,206,354.4 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;6,141.19 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;5,444.01 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;537 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp_Unroll_Branchleses&lt;/td&gt;
&lt;td&gt;1048599&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1,688,677.3 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;11,584.00 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;10,835.68 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.77&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.01&lt;/td&gt;
&lt;td style="text-align: right;"&gt;547 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp_Unroll&lt;/td&gt;
&lt;td&gt;33554455&lt;/td&gt;
&lt;td style="text-align: right;"&gt;205,320,736.1 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;2,757,108.01 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;2,152,568.58 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;537 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp_Unroll_Branchleses&lt;/td&gt;
&lt;td&gt;33554455&lt;/td&gt;
&lt;td style="text-align: right;"&gt;199,520,169.4 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;2,097,285.87 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1,637,422.86 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.97&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.01&lt;/td&gt;
&lt;td style="text-align: right;"&gt;547 B&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;As you can see, we have basically the same cost under 15% negatives for small values, a &lt;em&gt;big&lt;/em&gt; improvement on the 1M scenario and not much improvement on the 32M scenario.&lt;/p&gt;
&lt;p&gt;All in all, that is very interesting information. Digging into the exact why and how of that means pulling a CPU instruction profiler and starting to look at where we have stalls, which is a bit further that I want to invest in this scenario.&lt;/p&gt;
&lt;p&gt;What if we&amp;rsquo;ll try to rearrange the code a little bit. The code looks like this (load the value and AddToOutput() immediately):&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;AddToOutput(ref itemsRef, Unsafe.Add(ref itemsRef, i + 0));&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;What if we split it a little bit, so the code will look like this:&lt;/p&gt;
&lt;blockquote&gt;
&lt;script src="https://gist.github.com/ayende/eda4fa6716cdc2dafebc912b23fa56f7.js"&gt;&lt;/script&gt;
&lt;/blockquote&gt;
&lt;p&gt;The idea here is that we are trying to get the JIT / CPU to fetch the items before they are actually needed, so there would be more time for the memory to arrive.&lt;/p&gt;
&lt;p&gt;Remember that for the 1M scenario, we are dealing with 8MB of memory and for the 32M scenario, we have 256MB. Here is what happens when we look at the loop prolog, we can see that it is indeed first fetching all the items from memory, then doing the work:&lt;/p&gt;
&lt;script src="https://gist.github.com/ayende/0260946c003df0ad02ae18cdde2b80dc.js"&gt;&lt;/script&gt;
&lt;p&gt;In terms of performance, that gives us a small win (1% &amp;ndash; 2% range) for the 1M and 32M entries scenario.&lt;/p&gt;
&lt;p&gt;The one last thing that I wanted to test is if we&amp;rsquo;ll unroll the loop even further, what would happen if we did 8 items per loop, instead of 4.&lt;/p&gt;
&lt;blockquote&gt;
&lt;script src="https://gist.github.com/ayende/3416319e177d00eec4f97bfeab6052c8.js"&gt;&lt;/script&gt;
&lt;/blockquote&gt;
&lt;p&gt;There is &lt;em&gt;some&lt;/em&gt; improvement, (4% in the 1K scenario, 1% in the 32M scenario) but also slowdowns&amp;nbsp; (2% in the 1M scenario).&lt;/p&gt;
&lt;p&gt;I think that this is probably roughly the end of the line as far as we can get for scalar code.&lt;/p&gt;
&lt;p&gt;We already made quite a few strides in trying to parallelize the work the CPU is doing by just laying out the code as we would like it to be. We tried to control the manner in which it touches memory and in general, those are pretty advanced techniques.&lt;/p&gt;
&lt;p&gt;To close this post, I would like to take a look at the gains we got. I&amp;rsquo;m comparing the first version of the code, the last version we had on the previous post and the unrolled version for both branchy and branchless with 8 operations at once and memory prefetching.&lt;/p&gt;
&lt;table class="table table-striped table-bordered"&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;N&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Mean&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Error&lt;/th&gt;
&lt;th style="text-align: right;"&gt;StdDev&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Ratio&lt;/th&gt;
&lt;th style="text-align: right;"&gt;RatioSD&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Code Size&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp&lt;/td&gt;
&lt;td&gt;23&lt;/td&gt;
&lt;td style="text-align: right;"&gt;277.3 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.69 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.64 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;411 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp_NoRangeCheck&lt;/td&gt;
&lt;td&gt;23&lt;/td&gt;
&lt;td style="text-align: right;"&gt;270.7 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.42 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.38 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.98&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;397 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp_Unroll_8&lt;/td&gt;
&lt;td&gt;23&lt;/td&gt;
&lt;td style="text-align: right;"&gt;257.6 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.45 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.21 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.93&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;672 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp_Unroll_8_Branchless&lt;/td&gt;
&lt;td&gt;23&lt;/td&gt;
&lt;td style="text-align: right;"&gt;259.9 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.96 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.84 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.94&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.01&lt;/td&gt;
&lt;td style="text-align: right;"&gt;682 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp&lt;/td&gt;
&lt;td&gt;1047&lt;/td&gt;
&lt;td style="text-align: right;"&gt;754.3 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.38 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.22 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;411 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp_NoRangeCheck&lt;/td&gt;
&lt;td&gt;1047&lt;/td&gt;
&lt;td style="text-align: right;"&gt;749.0 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.81 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.69 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.99&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;397 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp_Unroll_8&lt;/td&gt;
&lt;td&gt;1047&lt;/td&gt;
&lt;td style="text-align: right;"&gt;647.2 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;2.23 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;2.09 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.86&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;672 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp_Unroll_8_Branchless&lt;/td&gt;
&lt;td&gt;1047&lt;/td&gt;
&lt;td style="text-align: right;"&gt;721.2 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.23 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.09 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.96&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;682 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp&lt;/td&gt;
&lt;td&gt;1048599&lt;/td&gt;
&lt;td style="text-align: right;"&gt;499,675.6 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;2,639.97 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;2,469.43 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;411 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp_NoRangeCheck&lt;/td&gt;
&lt;td&gt;1048599&lt;/td&gt;
&lt;td style="text-align: right;"&gt;494,388.4 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;600.46 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;532.29 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.99&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.01&lt;/td&gt;
&lt;td style="text-align: right;"&gt;397 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp_Unroll_8&lt;/td&gt;
&lt;td&gt;1048599&lt;/td&gt;
&lt;td style="text-align: right;"&gt;426,940.7 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1,858.57 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1,551.99 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.85&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.01&lt;/td&gt;
&lt;td style="text-align: right;"&gt;672 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp_Unroll_8_Branchless&lt;/td&gt;
&lt;td&gt;1048599&lt;/td&gt;
&lt;td style="text-align: right;"&gt;483,940.8 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;517.14 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;458.43 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.97&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;682 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp&lt;/td&gt;
&lt;td&gt;33554455&lt;/td&gt;
&lt;td style="text-align: right;"&gt;30,282,334.8 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;599,306.15 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;531,269.30 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;411 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp_NoRangeCheck&lt;/td&gt;
&lt;td&gt;33554455&lt;/td&gt;
&lt;td style="text-align: right;"&gt;29,410,612.5 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;29,583.56 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;24,703.61 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.97&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.02&lt;/td&gt;
&lt;td style="text-align: right;"&gt;397 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp_Unroll_8&lt;/td&gt;
&lt;td&gt;33554455&lt;/td&gt;
&lt;td style="text-align: right;"&gt;29,102,708.3 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;42,824.78 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;40,058.32 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.96&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.02&lt;/td&gt;
&lt;td style="text-align: right;"&gt;672 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp_Unroll_8_Branchless&lt;/td&gt;
&lt;td&gt;33554455&lt;/td&gt;
&lt;td style="text-align: right;"&gt;29,761,841.1 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;48,108.03 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;42,646.51 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.98&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.02&lt;/td&gt;
&lt;td style="text-align: right;"&gt;682 B&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The unrolled 8 version is the winner by far, in this scenario (0.5% negatives). Since that is the scenario we have in the real code, that is what I&amp;rsquo;m focusing on.&lt;/p&gt;
&lt;p&gt;Is there anything left to do here?&lt;/p&gt;
&lt;p&gt;My next step is to explore whether&amp;nbsp;using vector instructions will be a good option for us.&lt;/p&gt;
</description><link>http://ayende.com/200101-c/filtering-negative-numbers-fast-unroll?Key=617ce0ba-58e7-4460-9268-6f584c90c4cd</link><guid>http://ayende.com/200101-c/filtering-negative-numbers-fast-unroll?Key=617ce0ba-58e7-4460-9268-6f584c90c4cd</guid><pubDate>Tue, 12 Sep 2023 12:00:00 GMT</pubDate></item><item><title>Filtering negative numbers, fast: Scalar</title><description>&lt;p&gt;While working deep on the guts of RavenDB, I found myself with a seemingly simple task. Given a list of longs, I need to filter out all negative numbers as quickly as possible.&lt;/p&gt;
&lt;p&gt;The actual scenario is that we run a speculative algorithm, given a potentially large list of items, we check if we can fulfill the request in an optimal fashion. However, if that isn&amp;rsquo;t possible, we need to switch to a slower code path that does more work.&lt;/p&gt;
&lt;p&gt;Conceptually, this looks something like this:&lt;/p&gt;
&lt;blockquote&gt;
&lt;script src="https://gist.github.com/ayende/9e0e2ceaa091b0039eb70bf4d79c4f70.js"&gt;&lt;/script&gt;
&lt;/blockquote&gt;
&lt;p&gt;That is the setup for this story. The problem we have now is that we now need to &lt;em&gt;filter&lt;/em&gt; the results we pass to the &lt;em&gt;RunManually&lt;/em&gt;() method.&lt;/p&gt;
&lt;p&gt;There is a problem here, however. We marked the entries that we already used in the list by negating them. The issue is that &lt;em&gt;RunManually&lt;/em&gt;() does not allow negative values, and its internal implementation is &lt;em&gt;not&lt;/em&gt; friendly to ignoring those values.&lt;/p&gt;
&lt;p&gt;In other words, given a Span&amp;lt;long&amp;gt;, I need to write the code that would filter out all the negative numbers. Everything else about the list of numbers should remain the same (the order of elements, etc).&lt;/p&gt;
&lt;p&gt;From a coding perspective, this is as simple as:&lt;/p&gt;
&lt;blockquote&gt;
&lt;script src="https://gist.github.com/ayende/f24e8a3b056f72f68b7ee9af265c21ed.js"&gt;&lt;/script&gt;
&lt;/blockquote&gt;
&lt;p&gt;Please note, just &lt;em&gt;looking&lt;/em&gt; at this code makes me cringe a lot. This does the work, but it has an absolutely horrible performance profile. It allocates multiple arrays, uses a lambda, etc.&lt;/p&gt;
&lt;p&gt;We don&amp;rsquo;t actually &lt;em&gt;care&lt;/em&gt; about the &lt;em&gt;entries &lt;/em&gt;here, so we are free to modify them without allocating a new value. As such, let&amp;rsquo;s see what kind of code we can write to do this work in an efficient manner. Here is what I came up with:&lt;/p&gt;
&lt;blockquote&gt;
&lt;script src="https://gist.github.com/ayende/b952dba7f28b538ec8ba4f0513d1bb70.js"&gt;&lt;/script&gt;
&lt;/blockquote&gt;
&lt;p&gt;The way this works is that we scan through the list, skipping writing the negative lists, so we effectively &amp;ldquo;move down&amp;rdquo; all the non-negative lists on top of the negative ones. This has a cost of O(N) and will modify the entire array, the final output is the number of valid items that we have there.&lt;/p&gt;
&lt;p&gt;In order to test the performance, I wrote the following harness:&lt;/p&gt;
&lt;blockquote&gt;
&lt;script src="https://gist.github.com/ayende/a0006f32dba0e5b26f15ac0021b14b94.js"&gt;&lt;/script&gt;
&lt;/blockquote&gt;
&lt;p&gt;We compare 1K, 1M and 32M elements arrays, each of which has about 0.5% negative, randomly spread across the range. Because we modify the values directly, we need to sprinkle the negatives across the array on each call. In this case, I&amp;rsquo;m testing two options for this task, one that uses a direct comparison (shown above) and one that uses bitwise or, like so:&lt;/p&gt;
&lt;blockquote&gt;
&lt;script src="https://gist.github.com/ayende/809308fbbf24975197d4133a6771bbfe.js"&gt;&lt;/script&gt;
&lt;/blockquote&gt;
&lt;p&gt;I&amp;rsquo;m testing the cost of sprinkling negatives as well, since that has to be done before each benchmark call (since we modify the array during the call, we need to &amp;ldquo;reset&amp;rdquo; its state for the next one).&lt;/p&gt;
&lt;p&gt;Given the two options, before we discuss the results, what would you expect to be the faster option? How would the size of the array matter here?&lt;/p&gt;
&lt;p&gt;I really like this example, because it is &lt;em&gt;simple, &lt;/em&gt;there isn&amp;rsquo;t any real complexity in what we are trying to do. And there is a very straightforward implementation that we can use as our baseline. That also means that I get to analyze what is going on at a very deep level. You might have noticed the disassembler attribute on the benchmark code, we are going to dive &lt;em&gt;deep&lt;/em&gt; into that. For the same reason, we aren&amp;rsquo;t using &lt;em&gt;exactly&lt;/em&gt; 1K, 1M, or 32M arrays, but slightly higher than that, so we&amp;rsquo;ll have to deal with remainders later on.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s first look at what the JIT actually did here. Here is the annotated assembly for the FilterCmp function:&lt;/p&gt;
&lt;blockquote&gt;
&lt;script src="https://gist.github.com/ayende/3c08fffcd2baa9e91ae844f3114f93b5.js"&gt;&lt;/script&gt;
&lt;/blockquote&gt;
&lt;p&gt;For the FilterOr, the code is pretty much the same, except that the key part is:&lt;/p&gt;
&lt;blockquote&gt;
&lt;script src="https://gist.github.com/ayende/45dfa8af2e19a5c9b2927dca660af698.js"&gt;&lt;/script&gt;
&lt;/blockquote&gt;
&lt;p&gt;As you can see, the cmp option is slightly smaller, in terms of code size. In terms of performance, we have:&lt;/p&gt;
&lt;table class="table table-striped table-bordered"&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;N&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Mean&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;FilterOr&lt;/td&gt;
&lt;td&gt;1047&lt;/td&gt;
&lt;td style="text-align: right;"&gt;745.6 ns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp&lt;/td&gt;
&lt;td&gt;1047&lt;/td&gt;
&lt;td style="text-align: right;"&gt;745.8 ns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;mdash;&lt;/td&gt;
&lt;td&gt;&amp;ndash;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;ndash;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FilterOr&lt;/td&gt;
&lt;td&gt;1048599&lt;/td&gt;
&lt;td style="text-align: right;"&gt;497,463.6 ns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp&lt;/td&gt;
&lt;td&gt;1048599&lt;/td&gt;
&lt;td style="text-align: right;"&gt;498,784.8 ns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;mdash;&lt;/td&gt;
&lt;td&gt;&amp;ndash;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;ndash;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FilterOr&lt;/td&gt;
&lt;td&gt;33554455&lt;/td&gt;
&lt;td style="text-align: right;"&gt;31,427,660.7 ns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp&lt;/td&gt;
&lt;td&gt;33554455&lt;/td&gt;
&lt;td style="text-align: right;"&gt;30,024,102.9 ns&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The costs are very close to one another, with Or being very slightly faster on low numbers, and Cmp being slightly faster on the larger sizes. Note that the difference level between them is basically noise. They have the same performance.&lt;/p&gt;
&lt;p&gt;The question is, can we do better here?&lt;/p&gt;
&lt;p&gt;Looking at the assembly, there is an extra range check in the main loop that the JIT couldn&amp;rsquo;t elide (the call to &lt;em&gt;items[output++])&lt;/em&gt;. Can &lt;em&gt;we&lt;/em&gt; do something about it, and would it make any difference in performance? Here is how I can remove the range check:&lt;/p&gt;
&lt;blockquote&gt;
&lt;script src="https://gist.github.com/ayende/451cf7bb3594ad7a860058fe51e18e9c.js"&gt;&lt;/script&gt;
&lt;/blockquote&gt;
&lt;p&gt;Here I&amp;rsquo;m telling the JIT: &amp;ldquo;I know what I&amp;rsquo;m doing&amp;rdquo;, and it shows.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s look at the assembly changes between those two methods, first the prolog:&lt;/p&gt;
&lt;blockquote&gt;
&lt;script src="https://gist.github.com/ayende/c397a944faddbaeb0be20a3d8b8587b5.js"&gt;&lt;/script&gt;
&lt;/blockquote&gt;
&lt;p&gt;Here you can see what we are actually doing here. Note the last 4 instructions,&amp;nbsp;we have a range check for the items, and then we have another check for the loop. The first will get you an exception, the second will just skip the loop. In both cases, we test the exact same thing. The JIT had a chance to actually optimize that, but didn&amp;rsquo;t.&lt;/p&gt;
&lt;p&gt;Here is a funny scenario where &lt;em&gt;adding&lt;/em&gt; code may reduce the amount of code generated. Let&amp;rsquo;s do another version of this method:&lt;/p&gt;
&lt;blockquote&gt;
&lt;script src="https://gist.github.com/ayende/e15931fa4dbdc4258ea2676bb0fcc0e6.js"&gt;&lt;/script&gt;
&lt;/blockquote&gt;
&lt;p&gt;In this case, I added a check to handle the scenario of items being empty. What can the JIT do with this now? It turns out, quite a lot. We dropped 10 bytes from the method, which is a nice result of our diet.&amp;nbsp; Here is the annotated version of the assembly:&lt;/p&gt;
&lt;blockquote&gt;
&lt;script src="https://gist.github.com/ayende/6c710aceedc59a8160cabe081c25ede5.js"&gt;&lt;/script&gt;
&lt;/blockquote&gt;
&lt;p&gt;A lot of the space savings in this case come from just not having to do a range check, but you&amp;rsquo;ll note that we still do an extra check there (lines 12..13), even though we already checked that. I think that the JIT knows that the value is not zero at this point, but has to consider that the value may be negative.&lt;/p&gt;
&lt;p&gt;If we&amp;rsquo;ll change the initial guard clause to: items.Length &amp;lt;= 0, what do you think will happen? At this point, the JIT is smart enough to just elide everything, we are at 55 bytes of code and it is a super clean assembly (not a sentence I ever thought I would use). I&amp;rsquo;ll spare you going through &lt;em&gt;more &lt;/em&gt;assembly listing, but you can find &lt;a href="https://gist.github.com/ayende/ef55effdec3a68e9e0c4ca5b1884bc9a"&gt;the output here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;And after all of that, where are we at?&lt;/p&gt;
&lt;table class="table table-striped table-bordered"&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;N&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Mean&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Error&lt;/th&gt;
&lt;th style="text-align: right;"&gt;StdDev&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Ratio&lt;/th&gt;
&lt;th style="text-align: right;"&gt;RatioSD&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Code Size&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp&lt;/td&gt;
&lt;td&gt;23&lt;/td&gt;
&lt;td style="text-align: right;"&gt;274.5 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.91 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.70 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;411 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp_NoRangeCheck&lt;/td&gt;
&lt;td&gt;23&lt;/td&gt;
&lt;td style="text-align: right;"&gt;269.7 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.33 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.24 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.98&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.01&lt;/td&gt;
&lt;td style="text-align: right;"&gt;397 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp&lt;/td&gt;
&lt;td&gt;1047&lt;/td&gt;
&lt;td style="text-align: right;"&gt;744.5 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;4.88 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;4.33 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;411 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp_NoRangeCheck&lt;/td&gt;
&lt;td&gt;1047&lt;/td&gt;
&lt;td style="text-align: right;"&gt;745.8 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;3.44 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;3.22 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;397 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp&lt;/td&gt;
&lt;td&gt;1048599&lt;/td&gt;
&lt;td style="text-align: right;"&gt;502,608.6 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;3,890.38 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;3,639.06 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;411 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp_NoRangeCheck&lt;/td&gt;
&lt;td&gt;1048599&lt;/td&gt;
&lt;td style="text-align: right;"&gt;490,669.1 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1,793.52 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1,589.91 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.98&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.01&lt;/td&gt;
&lt;td style="text-align: right;"&gt;397 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp&lt;/td&gt;
&lt;td&gt;33554455&lt;/td&gt;
&lt;td style="text-align: right;"&gt;30,495,286.6 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;602,907.86 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;717,718.92 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;411 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FilterCmp_NoRangeCheck&lt;/td&gt;
&lt;td&gt;33554455&lt;/td&gt;
&lt;td style="text-align: right;"&gt;29,952,221.2 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;442,176.37 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;391,977.84 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.99&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.02&lt;/td&gt;
&lt;td style="text-align: right;"&gt;397 B&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;There is a very slight benefit to the NoRangeCheck, but even when we talk about 32M items, we aren&amp;rsquo;t talking about a lot of time.&lt;/p&gt;
&lt;p&gt;The question what can we do better here?&lt;/p&gt;
</description><link>http://ayende.com/200100-c/filtering-negative-numbers-fast-scalar?Key=457d45e9-014e-4352-ac3d-ab2aa2bd4e04</link><guid>http://ayende.com/200100-c/filtering-negative-numbers-fast-scalar?Key=457d45e9-014e-4352-ac3d-ab2aa2bd4e04</guid><pubDate>Mon, 11 Sep 2023 12:00:00 GMT</pubDate></item><item><title>Not all O(1) operations are considered equal</title><description>&lt;p&gt;At some point in any performance optimization sprint, you are going to run into a super annoying problem: The dictionary.&lt;/p&gt;
&lt;p&gt;The reasoning is quite simple. One of the most powerful optimization techniques is to use a cache, which is usually implemented as a dictionary. Today&amp;rsquo;s tale is about a dictionary, but surprisingly enough, not about a cache.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s set up the background, I&amp;rsquo;m looking at optimizing a big indexing batch deep inside RavenDB, and here is my current focus:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://ayende.com/blog/Images/Open-Live-Writer/Not-all-O1-operations-are-considered-equ_9504/image_2.png"&gt;&lt;img style="margin: 0px; border: 0px currentcolor; display: inline; background-image: none;" title="image" src="https://ayende.com/blog/Images/Open-Live-Writer/Not-all-O1-operations-are-considered-equ_9504/image_thumb.png" alt="image" width="1110" height="107" border="0" /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;You can see that the &lt;em&gt;RecordTermsForEntries&lt;/em&gt; take 4% of the &lt;em&gt;overall&lt;/em&gt; indexing time. That is&amp;hellip; a lot, as you can imagine.&lt;/p&gt;
&lt;p&gt;What is more interesting here is &lt;em&gt;why&lt;/em&gt;. The simplified version of the code looks like this:&lt;/p&gt;
&lt;blockquote&gt;
&lt;script src="https://gist.github.com/ayende/3f9eccc935a86a391e7dcd88c876b104.js"&gt;&lt;/script&gt;
&lt;/blockquote&gt;
&lt;p&gt;Basically, we are registering, for each entry, all the terms that belong to it. This is complicated by the fact that we are doing the process in stages:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create the entries&lt;/li&gt;
&lt;li&gt;Process the terms for the entries&lt;/li&gt;
&lt;li&gt;Write the terms to persistent storage (giving them the recorded term id)&lt;/li&gt;
&lt;li&gt;Update the entries to record the &lt;em&gt;term ids&lt;/em&gt; that they belong to&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The part of the code that we are looking at now is the last one, where we already wrote the terms to persistent storage and we need to update the entries. This is needed so when we read them, we&amp;rsquo;ll be able to find the relevant terms.&lt;/p&gt;
&lt;p&gt;At any rate, you can see that this method cost is absolutely dominated by the dictionary call. In fact, we are actually using an optimized method here to avoid doing a TryGetValue() and then Add() in case the value is not already in the dictionary.&lt;/p&gt;
&lt;p&gt;If we actually look at the metrics, this is actually kind of awesome. We are calling the dictionary almost 400 &lt;em&gt;million&lt;/em&gt;&amp;nbsp;times and it is able to do the work in under 200 &lt;em&gt;nanoseconds&lt;/em&gt; per call.&lt;/p&gt;
&lt;p&gt;That is pretty awesome, but that still means that we have over 2% of our &lt;em&gt;total indexing time&lt;/em&gt; spent doing lookups. Can we do better?&lt;/p&gt;
&lt;p&gt;In this case, absolutely. Here is how this works, instead of doing a dictionary lookup, we are going to store a list. And the entry will record the &lt;em&gt;index&lt;/em&gt; of the item in the list. Here is what this looks like:&lt;/p&gt;
&lt;blockquote&gt;
&lt;script src="https://gist.github.com/ayende/258325003839af3bc2a0c8951ff070d1.js"&gt;&lt;/script&gt;
&lt;/blockquote&gt;
&lt;p&gt;There isn&amp;rsquo;t much to this process, I admit. I was lucky that in this case, we were able to reorder things in such a way that skipping the dictionary lookup is a viable method.&lt;/p&gt;
&lt;p&gt;In other cases, we would need to record the index at the creation of the entry (effectively reserving the position) and then use that later.&lt;/p&gt;
&lt;p&gt;And the result is&amp;hellip;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://ayende.com/blog/Images/Open-Live-Writer/Not-all-O1-operations-are-considered-equ_9504/image_4.png"&gt;&lt;img style="margin: 0px; border: 0px currentcolor; display: inline; background-image: none;" title="image" src="https://ayende.com/blog/Images/Open-Live-Writer/Not-all-O1-operations-are-considered-equ_9504/image_thumb_1.png" alt="image" width="1088" height="97" border="0" /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;That is &lt;em&gt;pretty&lt;/em&gt; good, even if I say so myself. The cost went down from 3.6 microseconds per call to 1.3 microseconds. That is almost 3 folds improvement.&lt;/p&gt;
</description><link>http://ayende.com/200033-b/not-all-o-1-operations-are-considered-equal?Key=9b63efc3-8651-43b1-a301-6051e87361da</link><guid>http://ayende.com/200033-b/not-all-o-1-operations-are-considered-equal?Key=9b63efc3-8651-43b1-a301-6051e87361da</guid><pubDate>Wed, 30 Aug 2023 12:00:00 GMT</pubDate></item><item><title>A twisted tale of memory optimization</title><description>&lt;p&gt;I was looking into reducing the allocation in a particular part of our code, and I ran into what was basically the following code (boiled down to the essentials):&lt;/p&gt;
&lt;blockquote&gt;
&lt;script src="https://gist.github.com/ayende/5212d11a369922d2b4408d08890e831c.js"&gt;&lt;/script&gt;
&lt;/blockquote&gt;
&lt;p&gt;As you can see, this does a &lt;em&gt;lot&lt;/em&gt; of allocations. The actual method in question was a pretty good size, and all those operations happened in different locations and weren&amp;rsquo;t as obvious.&lt;/p&gt;
&lt;p&gt;Take a moment to look at the code, how many allocations can you spot here?&lt;/p&gt;
&lt;p&gt;The first one, obviously, is the string allocation, but there is another one, inside the call to GetBytes(), let&amp;rsquo;s fix that first by allocating the buffer once (I&amp;rsquo;m leaving aside the allocation of the reusable buffer, you can assume it is big enough to cover all our needs):&lt;/p&gt;
&lt;blockquote&gt;
&lt;script src="https://gist.github.com/ayende/c4e0decb2442767e9bce88da7c5be89e.js"&gt;&lt;/script&gt;
&lt;/blockquote&gt;
&lt;p&gt;For that matter, we can also easily fix the second problem, by avoiding the string allocation:&lt;/p&gt;
&lt;blockquote&gt;
&lt;script src="https://gist.github.com/ayende/30fa55843832bfff0e630557fa75247e.js"&gt;&lt;/script&gt;
&lt;/blockquote&gt;
&lt;p&gt;That is a few minutes of work, and we are good to go. This method is called &lt;em&gt;a lot&lt;/em&gt;, so we can expect a huge reduction in the amount of memory that we allocated.&lt;/p&gt;
&lt;p&gt;Except&amp;hellip; that didn&amp;rsquo;t happen. In fact, the amount of memory that we allocate remained pretty much the same. Digging into the details, we allocate roughly the same number of byte arrays (how!) and instead of allocating a lot of strings, we now allocate a lot of character arrays.&lt;/p&gt;
&lt;p&gt;I broke the code apart into multiple lines, which made things a lot clearer. (&lt;a href="https://sharplab.io/#v2:EYLgxg9gTgpgtADwGwBYA+ABATARgLABQGADAAQY4B0AKjAgC4DchJ5VAkgPLMEsDM5LKQDKYGADsAhlACWEUgG9CpFSPqxJAW1IB9AM7qYWnqtLAAnvRgBtALq7YAVz2TgAGxgAhRwDMfMKBNVDAEMFFIAdVkrAAowAAtpOzNffygAGlIZcXpSD3EASkVlU1US0tIAN2kzSxg9UgBeUgBRcUgAE2yAc0oAVWoAMQAOSgBxGHpPOr0Y4FSA60pKfNtMnScXdy8FqAKgiv1DLUoomViNmGdXD28/ReWLKz1bffLSAF9CD6A=="&gt;In fact, I threw that into SharpLab, to be honest&lt;/a&gt;). Take a look:&lt;/p&gt;
&lt;blockquote&gt;
&lt;script src="https://gist.github.com/ayende/942a494f64f10df43d3af3886b475189.js"&gt;&lt;/script&gt;
&lt;/blockquote&gt;
&lt;p&gt;This code: buffer[..len] is actually translated to:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;char[] charBuffer= RuntimeHelpers.GetSubArray(buffer, Range.EndAt(len));&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That will, of course, allocate. I had to change the code to be very explicit about the types that I wanted to use:&lt;/p&gt;
&lt;blockquote&gt;
&lt;script src="https://gist.github.com/ayende/12049752c22b3574fe5d432039d6f481.js"&gt;&lt;/script&gt;
&lt;/blockquote&gt;
&lt;p&gt;This will not allocate, but if you note the changes in the code, you can see that the use of &lt;em&gt;var&lt;/em&gt; in this case really tripped me up. Because of the number of overloads and automatic coercion of types that &lt;em&gt;didn&amp;rsquo;t&lt;/em&gt; happen.&lt;/p&gt;
&lt;p&gt;For that matter, note that &lt;em&gt;any&lt;/em&gt; slicing on arrays will generate a &lt;em&gt;new&lt;/em&gt; array, including this code:&lt;/p&gt;
&lt;blockquote&gt;
&lt;script src="https://gist.github.com/ayende/339bacdd5907b08534ea76f399ff89d4.js"&gt;&lt;/script&gt;
&lt;/blockquote&gt;
&lt;p&gt;This makes perfect sense when you realize what is going on and can still be a big surprise, I looked at the code &lt;em&gt;a lot&lt;/em&gt; before I figured out what was going on, and that was with a profiler output that pinpointed the fault.&lt;/p&gt;
</description><link>http://ayende.com/199969-a/a-twisted-tale-of-memory-optimization?Key=cb2fc684-6549-466c-9c4f-0bae8b5ef566</link><guid>http://ayende.com/199969-a/a-twisted-tale-of-memory-optimization?Key=cb2fc684-6549-466c-9c4f-0bae8b5ef566</guid><pubDate>Thu, 24 Aug 2023 12:00:00 GMT</pubDate></item><item><title>On Moq &amp; SponsorLink: Some thoughts</title><description>&lt;p&gt;Today I ran into &lt;a href="https://www.reddit.com/r/dotnet/comments/15ljdcc/does_moq_in_its_latest_version_extract_and_send/"&gt;this Reddit post&lt;/a&gt;, detailing how Moq is now using &lt;a href="https://www.cazzulino.com/sponsorlink.html"&gt;SponsorLink&lt;/a&gt; to encourage users to sponsor the project.&lt;/p&gt;
&lt;p&gt;The idea is that if you are using the project, you&amp;rsquo;ll sponsor it for some amount, which funds the project. You&amp;rsquo;ll also get something like this:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.cazzulino.com/img/sponsorlink-thanks.png" alt="lots of thanks from ThisAssembly" /&gt;&lt;/p&gt;
&lt;p&gt;This has been rolled out for some projects for quite some time, it seems. But Moq is a far more popular project and it got quite &lt;a href="https://github.com/moq/moq/issues/1372"&gt;a bit of attention&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It is an interesting scenario, and I gave some thought to what this means.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I&amp;rsquo;m not a user of Moq, just to note.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I absolutely understand the desire to be paid for Open Source work. It takes a &lt;em&gt;lot&lt;/em&gt; of time and effort and looking at the amount of usage people get out of your code compared to the compensation is sometimes ridiculous.&lt;/p&gt;
&lt;p&gt;For myself, I can tell you that &lt;a href="https://ayende.com/blog/4176/ave-duci-novo-similis-duci-seneci"&gt;I made 800 USD&lt;/a&gt; out of Rhino.Mocks directly when it was one of the most popular mocking frameworks in the .NET world. That isn&amp;rsquo;t &lt;em&gt;a &lt;/em&gt;sale, that is the total amount of compensation that I got for it directly.&lt;/p&gt;
&lt;p&gt;I literally cannot total the number of hours that I spent on it. &lt;a href="https://openhub.net/p/rhino_mocks"&gt;But OpenHub estimates it as 245 man-years&lt;/a&gt;. I&amp;hellip; disagree with that estimate, but I certainly put a &lt;em&gt;lot&lt;/em&gt; of time there.&lt;/p&gt;
&lt;p&gt;From a commercial perspective, I think that this direction is a mistake. Primarily because of the economies of software purchases. You can read about the implementation of &lt;a href="https://www.cazzulino.com/sponsorlink.html"&gt;SponsorLink here&lt;/a&gt;. The model basically says that it will check whether the &lt;em&gt;individual user &lt;/em&gt;has sponsored the project.&lt;/p&gt;
&lt;p&gt;That is&amp;hellip; not really how it works. Let&amp;rsquo;s say that a new developer is starting to work on an existing project. It is using a SponsorLink project. What happens then? That new developer is being asked to sponsor the project?&lt;/p&gt;
&lt;p&gt;If this is a commercial project, I certainly support the notion that there should be some payment. But it should not be on the individual developer, it should be on the &lt;em&gt;company&lt;/em&gt; that pays for the project.&lt;/p&gt;
&lt;p&gt;That leaves aside all the scenarios where this is being used for an open source project, etc. Let&amp;rsquo;s ignore those for now.&lt;/p&gt;
&lt;p&gt;The problem is that this isn&amp;rsquo;t how you actually get paid for software. If you are targeting commercial usage, you should be targeting companies, not individual users. More to the point, let&amp;rsquo;s say that a developer&amp;nbsp;&lt;em&gt;wants&lt;/em&gt; to pay, and their company will compensate them for that.&lt;/p&gt;
&lt;p&gt;The process for actually doing that is &lt;em&gt;atrocious&lt;/em&gt; beyond belief. There are tax implications (if they sponsor with 5$ / month and their employer gives them a 5$ raise, that would be taxed, for example), so you need to submit a receipt for expenses, etc.&lt;/p&gt;
&lt;p&gt;A far better model would be to have a way to get the company to pay for that, maybe on a per project basis. Then you can detect if the project is sponsored, for example, by looking at the repository URL (and accounting for forks)&lt;em&gt;. &lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Note that at this point, we are talking about the actual process of getting money, nothing else about this issue.&lt;/p&gt;
&lt;p&gt;Now, let&amp;rsquo;s get to the reason that this caused much angst for people. The way SponsorLink works is that it fetches your email from the git configuration file and check wether:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You are registered as a SponsorLink sponsor&lt;/li&gt;
&lt;li&gt;You are sponsoring this particular project&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It does both checks using what appears to be: &lt;em&gt;base62(sha256(email));&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;If you are already a SponsorLink sponsor, you have explicitly agreed to sharing your email, so not a problem there. So the second request is perfectly fine.&lt;/p&gt;
&lt;p&gt;The real problem is the first check, when you check if you are a SponsorLink sponsor in the first place. Let&amp;rsquo;s assume that you aren&amp;rsquo;t, what happens then.&lt;/p&gt;
&lt;p&gt;Well, there is a request made that looks something like this:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;HEAD /azure-blob-storage/path/app/3uVutV7zDlwv2rwBwfOmm2RXngIwJLPeTO0qHPZQuxyS&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The server will return a 404 if you are &lt;em&gt;not&lt;/em&gt; a sponsor at this point.&lt;/p&gt;
&lt;p&gt;The email hash above is my own, by the way. As I mentioned, I&amp;rsquo;m not a sponsor, so I assume this will return 404. The question is what sort of information is being provided to the server in this case?&lt;/p&gt;
&lt;p&gt;Well, there is the hashed email, right? Is that a privacy concern?&lt;/p&gt;
&lt;p&gt;It is indeed. While reversing SHA256 in general is not possible, for something like emails, that is pretty trivial. It took me a few minutes to find &lt;a href="https://hashes.com/en/emails/sha256"&gt;an online tool that does just that&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The cost is around 0.00045 USD / email, just to give some context. So the end result is that using SponsorLink will provide the email of the user (without their express or implied consent) to the server. It takes a little bit of extra work, but it most certainly does.&lt;/p&gt;
&lt;p&gt;Note that practically speaking, this looks like it hits Azure Blob Storage, not a dedicated endpoint. That means that you can probably define logging to check for the requests and then gather the information from there.&amp;nbsp; Not sure what you would &lt;em&gt;do&lt;/em&gt; with this information, but it certainly looks like this falls under PII definition on the GDPR.&lt;/p&gt;
&lt;p&gt;There are a few ways to resolve this problem. The first would be to not use email at all, but instead the project repository URL. That may require a bit more work to resolve forks, but would alleviate most of the concerns regarding privacy. A &lt;em&gt;better&lt;/em&gt; option would be to just check for an included file in the repository, to be honest. Something like: .sponsored.projects file.&lt;/p&gt;
&lt;p&gt;That would include the ids of the projects that were sponsored by this project, and then you can run a check to see that they are actually sponsored. There is no issue with consent here, since adding that file to the repository will explicitly consent for the process.&lt;/p&gt;
&lt;p&gt;Assuming that you want / need to use the emails still, the problem is much more complex. You cannot use the same process as &lt;a href="https://www.troyhunt.com/understanding-have-i-been-pwneds-use-of-sha-1-and-k-anonymity/"&gt;k-Anonymity as you can use for passwords&lt;/a&gt;. The problem is that a SHA256 of an email is as good as the email itself.&lt;/p&gt;
&lt;p&gt;I &lt;em&gt;think&lt;/em&gt; that something like this would work, however. Given the SHA256 of the email, you send to the server the following details:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;prefix = SHA256(email)[0 .. 6]&lt;/li&gt;
&lt;li&gt;key = read(&amp;ldquo;/dev/urandom&amp;rdquo;, 32bytes)&lt;/li&gt;
&lt;li&gt;hash = HMAC-SHA256(key, SHA256(email)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The prefix is the first 6 letters of the SHA256 hash. The key has cryptography strength of 32 random bytes.&lt;/p&gt;
&lt;p&gt;The hash is taking the SHA256 and hashing it again usung HMAC with the provided key.&lt;/p&gt;
&lt;p&gt;The idea is that on the server side, you can load all the hashes that you stored that match the provided prefix. Then you compute the keyed HMAC for all of those values and attempt to check if there is a match.&lt;/p&gt;
&lt;p&gt;We are trying to protect against a &lt;em&gt;malicious server&lt;/em&gt; here, remember. So the idea is that if there is a match, we pinged the server with an email that it knows about. If we ping the server with an email that it does &lt;em&gt;not&lt;/em&gt; know about, on the other hand, it cannot tell you anything about the value.&lt;/p&gt;
&lt;p&gt;The first 6 characters of the SHA256 will tell you nothing about the value, after all. And the fact that we use a random key to sending the actual hash to the server means that there is no point trying to figure it out.&amp;nbsp; Unlike trying to guess an email, guessing a &lt;em&gt;hash&lt;/em&gt; of an email is likely far harder, to the point that it is not feasible.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Note, I&amp;rsquo;m &lt;em&gt;not&lt;/em&gt; a cryptography expert, and I wouldn&amp;rsquo;t actually implement such a thing without consulting with one. I&amp;rsquo;m just writing a blog post with my ideas.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That would at least alleviate the privacy concern. But there are other issues.&lt;/p&gt;
&lt;p&gt;The SponsorLink is provided as a closed-source &lt;em&gt;obfuscated&lt;/em&gt; library. People have taken the time to de-obfuscate it, and so far it appears to be matching the documented behavior. But the mere fact that it is actually obfuscated and closed-source inclusion in an open-source project raises a lot of red flags.&lt;/p&gt;
&lt;p&gt;Finally, there is the actual behavior when it detects that you are not sponsoring this project. Here is what the blog post states will happen:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://raw.githubusercontent.com/devlooped/SponsorLink/main/assets/img/VS-SL02.png" alt="A diagnostics warning in VS suggesting you install SponsorLink" /&gt;&lt;/p&gt;
&lt;p&gt;It will delay the build (locally, on your machine, not on CI).&lt;/p&gt;
&lt;p&gt;That&amp;hellip; is really bad. I assume that this happens on every build (not sure, though). If that is the case, that means that the feedback cycle of "write a test, run it, write code, run a test", is going to hit significant slowdowns.&lt;/p&gt;
&lt;p&gt;I would consider this to be a breaking point even excluding everything else.&lt;/p&gt;
&lt;p&gt;As I previously stated, I&amp;rsquo;m all for paying for Open Source software. But this is not the way to do that, there is a whole bunch of friction and not much that can indicate a positive outcome for the project.&lt;/p&gt;
&lt;p&gt;Monetization strategies for Open Source projects are &lt;em&gt;complex&lt;/em&gt;. Open core, for example, likely would not work for this scenario. Nor would you be likely to get support contracts. The critical aspect is that beyond just the technical details, any such strategy requires a whole bunch of &lt;em&gt;infrastructure&lt;/em&gt; around it. Marketing, sales, contract negotiation, etc. There is no easy solution here, I&amp;rsquo;m afraid.&lt;/p&gt;
</description><link>http://ayende.com/199905-c/on-moq-sponsorlink-some-thoughts?Key=c8cdbd73-9a61-4c49-87dd-7352604ec047</link><guid>http://ayende.com/199905-c/on-moq-sponsorlink-some-thoughts?Key=c8cdbd73-9a61-4c49-87dd-7352604ec047</guid><pubDate>Thu, 10 Aug 2023 12:00:00 GMT</pubDate></item><item><title>A performance profile mystery: The cost of Stopwatch</title><description>&lt;p&gt;Measuring the length of time that a particular piece of code takes is a surprising challenging task. There are two aspects to this, the first is how do you ensure that the cost of getting the start and end times won&amp;rsquo;t interfere with the work you are doing. The second is how to actually get the time (potentially &lt;em&gt;many &lt;/em&gt;times a second) in as efficient way as possible.&lt;/p&gt;
&lt;p&gt;To give some context, &lt;a href="https://aakinshin.net/posts/stopwatch/"&gt;Andrey Akinshin&lt;/a&gt; does a great overview of how the Stopwatch class works in C#. On Linux, that is basically calling to the &lt;em&gt;clock_gettime&lt;/em&gt; system call, except that this is &lt;em&gt;not&lt;/em&gt; a system call. That is actually a piece of code that the Kernel sticks inside &lt;em&gt;your&lt;/em&gt; process that will then integrate with other aspects of the Kernel to optimize this. The idea is that this system call is &lt;em&gt;so frequent&lt;/em&gt; that you cannot pay the cost of the Kernel mode transition. There is a good coverage &lt;a href="https://berthub.eu/articles/posts/on-linux-vdso-and-clockgettime/"&gt;of this here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;In short, that is a very well-known problem and quite a lot of brainpower has been dedicated to solving it. And then we reached this situation:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://ayende.com/blog/Images/Open-Live-Writer/A-performance-profile-mystery_A64B/image_2.png"&gt;&lt;img style="margin: 0px; border: 0px currentcolor; display: inline; background-image: none;" title="image" src="https://ayende.com/blog/Images/Open-Live-Writer/A-performance-profile-mystery_A64B/image_thumb.png" alt="image" width="1201" height="141" border="0" /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;What you are seeing here is us testing the indexing process of RavenDB under the profiler. This is indexing roughly 100M documents, and according to the profiler, we are spending 15% of our time gathering metrics?&lt;/p&gt;
&lt;p&gt;The StatsScope.Start() method simply calls Stopwatch.Start(), so we are basically looking at a profiler output that says that Stopwatch is accounting for 15% of our runtime?&lt;/p&gt;
&lt;p&gt;Sorry, I don&amp;rsquo;t believe that. I mean, it is &lt;em&gt;possible&lt;/em&gt;, but it seems far-fetched.&lt;/p&gt;
&lt;p&gt;In order to test this, I wrote a &lt;a href="https://gist.github.com/ayende/563e0e4b0dae1bf986a660076f143aa6"&gt;very simple program&lt;/a&gt;, which will generate 100K integers and test whether&amp;nbsp;they are prime or not. I&amp;rsquo;m doing that to test compute-bound work, basically, and testing calling Start() and Stop() either across the whole loop or in each iteration.&lt;/p&gt;
&lt;p&gt;I run that a few times and I&amp;rsquo;m getting:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Windows: 311 ms with Stopwatch per iteration and 312 ms without&lt;/li&gt;
&lt;li&gt;Linux: 450 ms with Stopwatch per iteration and 455 ms without&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;On Linux, there is about 5ms overhead if we use a per iteration stopwatch, on Windows, it is either the same cost or slightly cheaper &lt;em&gt;with &lt;/em&gt;per iteration stopwatch.&lt;/p&gt;
&lt;p&gt;Here is the profiler output on Windows:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://ayende.com/blog/Images/Open-Live-Writer/A-performance-profile-mystery_A64B/image_4.png"&gt;&lt;img style="margin: 0px; border: 0px currentcolor; display: inline; background-image: none;" title="image" src="https://ayende.com/blog/Images/Open-Live-Writer/A-performance-profile-mystery_A64B/image_thumb_1.png" alt="image" width="862" height="123" border="0" /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;And on Linux:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://ayende.com/blog/Images/Open-Live-Writer/A-performance-profile-mystery_A64B/image_6.png"&gt;&lt;img style="margin: 0px; border: 0px currentcolor; display: inline; background-image: none;" title="image" src="https://ayende.com/blog/Images/Open-Live-Writer/A-performance-profile-mystery_A64B/image_thumb_2.png" alt="image" width="891" height="101" border="0" /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Now, that is what happens when we are doing a significant amount of work, what happens if the amount of work is negligible? I made the IsPrime() method very cheap, and I got:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://ayende.com/blog/Images/Open-Live-Writer/A-performance-profile-mystery_A64B/image_8.png"&gt;&lt;img style="margin: 0px; border: 0px currentcolor; display: inline; background-image: none;" title="image" src="https://ayende.com/blog/Images/Open-Live-Writer/A-performance-profile-mystery_A64B/image_thumb_3.png" alt="image" width="847" height="108" border="0" /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;So that is a good indication that this isn&amp;rsquo;t &lt;em&gt;free&lt;/em&gt;, but still&amp;hellip;&lt;/p&gt;
&lt;p&gt;Comparing the costs, it is utterly ridiculous that the profiler says that so much time is spent in those methods.&lt;/p&gt;
&lt;p&gt;Another aspect here may be the issue of the profiler impact itself. There are differences between using Tracing and Sampling methods, for example.&lt;/p&gt;
&lt;p&gt;I don&amp;rsquo;t have &lt;em&gt;an&lt;/em&gt; answer, just a lot of very curious questions.&lt;/p&gt;
</description><link>http://ayende.com/199874-a/a-performance-profile-mystery-the-cost-of-stopwatch?Key=095fd4b2-4515-479e-a926-ffeed604e5f3</link><guid>http://ayende.com/199874-a/a-performance-profile-mystery-the-cost-of-stopwatch?Key=095fd4b2-4515-479e-a926-ffeed604e5f3</guid><pubDate>Wed, 09 Aug 2023 12:00:00 GMT</pubDate></item><item><title>Production postmortem: The dog ate my request</title><description>&lt;p&gt;A customer called us, quite upset, because their RavenDB cluster was failing every few minutes. That was weird, because they were running on our cloud offering, so we had full access to the metrics, and we saw absolutely no problem on our end.&lt;/p&gt;
&lt;p&gt;During the call, it turned out that every now and then, but almost always immediately after a new deployment, RavenDB would fail some requests. On a fairly consistent basis, we could see two failures and a retry that was finally successful.&lt;/p&gt;
&lt;p&gt;Okay, so at least there is no user visible impact, but this was still super strange to see. On the backend, we couldn&amp;rsquo;t see any reason why we would get those sort of errors.&lt;/p&gt;
&lt;p&gt;Looking at the failure stack, we narrowed things down to an async operation that was invoked via DataDog. Our suspicions were focused on this being an error in the async machinery customization that DataDog uses for adding non-invasive monitoring.&lt;/p&gt;
&lt;p&gt;We created a custom build for the user that they could test and waited to get the results from their environment. Trying to reproduce this locally using DataDog integration didn&amp;rsquo;t raise any flags.&lt;/p&gt;
&lt;p&gt;The good thing was that we did find a smoking gun, a violation of the natural order and invariant breaking behavior.&lt;/p&gt;
&lt;p&gt;The not so good news was that it was in our own code. At least that meant that we could fix this.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s see if I can explain what is going on. The customer was using a custom configuration: FastestNode. This is used to find the nearest / least loaded node in the cluster and operate from it.&lt;/p&gt;
&lt;p&gt;How does RavenDB &lt;em&gt;know&lt;/em&gt; which is the fastest node? That is kind of hard to answer, after all. It checks.&lt;/p&gt;
&lt;p&gt;Every now and then, RavenDB replicates a read request to all nodes in the cluster. Something like this:&lt;/p&gt;
&lt;blockquote&gt;
&lt;script src="https://gist.github.com/ayende/692d3ecd0df97d3b7e2df3997632ca19.js"&gt;&lt;/script&gt;
&lt;/blockquote&gt;
&lt;p&gt;The idea is that we send the request to all the nodes, and wait for the &lt;em&gt;first&lt;/em&gt; one to arrive. Since this is the same request, all servers will do the same amount of work, and we&amp;rsquo;ll find the fastest node from our perspective.&lt;/p&gt;
&lt;p&gt;Did you notice the cancellation token in there? When we return from this function, we cancel the existing requests. Here is what this looks like from the monitoring perspective:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://ayende.com/blog/Images/Open-Live-Writer/Production-post-mortem_1004C/image_2.png"&gt;&lt;img style="margin: 0px; border: 0px currentcolor; display: inline; background-image: none;" title="image" src="https://ayende.com/blog/Images/Open-Live-Writer/Production-post-mortem_1004C/image_thumb.png" alt="image" width="1133" height="123" border="0" /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This looks &lt;em&gt;exactly&lt;/em&gt; like every few minutes, we have a couple of failures (and failover) in the system and was quite confusing until we figured out exactly what was going on.&lt;/p&gt;
</description><link>http://ayende.com/199745-a/production-postmortem-the-dog-ate-my-request?Key=80c35c6a-b89c-4ac6-bc7b-10fdefa2754a</link><guid>http://ayende.com/199745-a/production-postmortem-the-dog-ate-my-request?Key=80c35c6a-b89c-4ac6-bc7b-10fdefa2754a</guid><pubDate>Mon, 24 Jul 2023 12:00:00 GMT</pubDate></item><item><title>Solving heap corruption errors in managed applications</title><description>&lt;p&gt;RavenDB is a .NET application, written in C#. It also has a &lt;em&gt;non trivial&lt;/em&gt; amount of unmanaged memory usage. We absolutely need that to get the proper level of performance that we require.&lt;/p&gt;
&lt;p&gt;With managing memory manually, there is also the possibility that we&amp;rsquo;ll mess it up. We run into one such case, when running our full test suite (over 10,000 tests) we would get random crashes due to heap corruption. Those issues are &lt;em&gt;nasty&lt;/em&gt;, because there is a big separation between the root cause and the actual problem manifesting.&lt;/p&gt;
&lt;p&gt;I recently learned that you can use &lt;a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/gflags-and-pageheap"&gt;the gflags tool on .NET executables&lt;/a&gt;. We were able to narrow the problem to a single scenario, but we still had no idea where the problem really occurred. So I installed the &lt;a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/debugger-download-tools"&gt;Debugging Tools for Windows&lt;/a&gt; and then executed:&lt;/p&gt;
&lt;blockquote&gt;
&lt;pre&gt; &amp;amp;"C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\gflags.exe" /p /enable C:\Work\ravendb-6.0\test\Tryouts\bin\release\net7.0\Tryouts.exe&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;p&gt;What this does is enable a special debug heap at the executable level, which applies to &lt;em&gt;all&lt;/em&gt; operations (managed and native memory alike).&lt;/p&gt;
&lt;p&gt;With that enabled, I ran the scenario in question:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;PS C:\Work\ravendb-6.0\test\Tryouts&amp;gt;&amp;nbsp; C:\Work\ravendb-6.0\test\Tryouts\bin\release\net7.0\Tryouts.exe&lt;br /&gt; 42896&lt;br /&gt; Starting to run 0&lt;br /&gt; Max number of concurrent tests is: 16&lt;br /&gt; Ignore request for setting processor affinity. Requested cores: 3. Number of cores on the machine: 32.&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; To attach debugger to test process (x64), use proc-id: 42896. Url &lt;a href="http://127.0.0.1:51595"&gt;http://127.0.0.1:51595&lt;/a&gt;&lt;br /&gt; Ignore request for setting processor affinity. Requested cores: 3. Number of cores on the machine: 32.&amp;nbsp; License limits: A: 3/32. Total utilized cores: 3. Max licensed cores: 1024&lt;br /&gt; &lt;a href="http://127.0.0.1:51595/studio/index.html#databases/documents?&amp;amp;database=Should_correctly_reduce_after_updating_all_documents_1&amp;amp;withStop=true&amp;amp;disableAnalytics=true"&gt;http://127.0.0.1:51595/studio/index.html#databases/documents?&amp;amp;database=Should_correctly_reduce_after_updating_all_documents_1&amp;amp;withStop=true&amp;amp;disableAnalytics=true&lt;/a&gt;&lt;br /&gt; Fatal error. System.AccessViolationException: Attempted to read or write protected memory. This is often an indication that other memory is corrupt.&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; at Sparrow.Server.Compression.Encoder3Gram`1[[System.__Canon, System.Private.CoreLib, Version=7.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]].Encode(System.ReadOnlySpan`1&amp;lt;Byte&amp;gt;, System.Span`1&amp;lt;Byte&amp;gt;)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; at Sparrow.Server.Compression.HopeEncoder`1[[Sparrow.Server.Compression.Encoder3Gram`1[[System.__Canon, System.Private.CoreLib, Version=7.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]], Sparrow.Server, Version=6.0.0.0, Culture=neutral, PublicKeyToken=37f41c7f99471593]].Encode(System.ReadOnlySpan`1&amp;lt;Byte&amp;gt; ByRef, System.Span`1&amp;lt;Byte&amp;gt; ByRef)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; at Voron.Data.CompactTrees.PersistentDictionary.ReplaceIfBetter[[Raven.Server.Documents.Indexes.Persistence.Corax.CoraxDocumentTrainEnumerator, Raven.Server, Version=6.0.0.0, Culture=neutral, PublicKeyToken=37f41c7f99471593],[Raven.Server.Documents.Indexes.Persistence.Corax.CoraxDocumentTrainEnumerator, Raven.Server, Version=6.0.0.0, Culture=neutral, PublicKeyToken=37f41c7f99471593]](Voron.Impl.LowLevelTransaction, Raven.Server.Documents.Indexes.Persistence.Corax.CoraxDocumentTrainEnumerator, Raven.Server.Documents.Indexes.Persistence.Corax.CoraxDocumentTrainEnumerator, Voron.Data.CompactTrees.PersistentDictionary)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; at Raven.Server.Documents.Indexes.Persistence.Corax.CoraxIndexPersistence.Initialize(Voron.StorageEnvironment)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That pinpointed things so I was able to know exactly where we are messing up.&lt;/p&gt;
&lt;p&gt;I was also able to reproduce the behavior on the debugger:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://ayende.com/blog/Images/Open-Live-Writer/Solving-heap-corruption-errors-in-manage_B4AE/image%20(3)_2.png"&gt;&lt;img style="border: 0px currentcolor; display: inline; background-image: none;" title="image (3)" src="https://ayende.com/blog/Images/Open-Live-Writer/Solving-heap-corruption-errors-in-manage_B4AE/image%20(3)_thumb.png" alt="image (3)" width="1394" height="306" border="0" /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This saved me &lt;em&gt;hours &lt;/em&gt;or &lt;em&gt;days&lt;/em&gt; of trying to figure out where the problem actually is.&lt;/p&gt;
</description><link>http://ayende.com/199682-b/solving-heap-corruption-errors-in-managed-applications?Key=9d558341-07d7-46a6-be46-f94cb752404e</link><guid>http://ayende.com/199682-b/solving-heap-corruption-errors-in-managed-applications?Key=9d558341-07d7-46a6-be46-f94cb752404e</guid><pubDate>Wed, 05 Jul 2023 12:00:00 GMT</pubDate></item><item><title>Bug chasing, the process is more important than the result</title><description>&lt;p&gt;I&amp;rsquo;m doing a pretty major refactoring inside of RavenDB right now. I was able to finish a bunch of work and submitted things to the CI server for testing. RavenDB has several layers of tests, which we run depending on context.&lt;/p&gt;
&lt;p&gt;During development, we&amp;rsquo;ll usually run the FastTests. About 2,300 tests are being run to validate various behaviors for RavenDB, and on my machine, they take just over 3 minutes to complete. The next tier is the SlowTests, which run for about 3 hours on the CI server and run about 26,000 tests. Beyond that we actually have a few more layers, like tests that are being run only on the nightly builds and stress tests, which can take several minutes each to complete.&lt;/p&gt;
&lt;p&gt;In short, the usual process is that you write the code and write the relevant tests. You also validate that you didn&amp;rsquo;t break anything by running the FastTests locally. Then we let CI pick up the rest of the work. At the last count, we had about 9 dedicated machines as CI agents and given our workload, an actual full test run of a PR may complete the next day.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m mentioning all of that to explain that when I reviewed the build log for my PR, I found that there were a bunch of tests that failed. That was reasonable, given the scope of my changes. I sat down to grind through them, fixing them one at a time. Some of them were &lt;em&gt;quite&lt;/em&gt; important things that I didn&amp;rsquo;t take into account, after all. For example, one of the tests failed because I didn&amp;rsquo;t account for sorting on a dynamic numeric field. Sorting on a numeric field worked, and a dynamic text field also worked. But dynamic numeric field didn&amp;rsquo;t. It&amp;rsquo;s the sort of thing that I would never think of, but we got the tests to cover us.&lt;/p&gt;
&lt;p&gt;But when I moved to the next test, it didn&amp;rsquo;t fail. I ran it again, and it still didn&amp;rsquo;t fail. I ran it in a loop, and it failed on the 5th iteration. That&amp;hellip; sucked. Because it meant that I had a race condition in there somewhere. I ran the loop again, and it failed &lt;em&gt;again&lt;/em&gt; on the 5th. In fact, in every iteration I tried, it would only fail on the 5th iteration.&lt;/p&gt;
&lt;p&gt;When trying to isolate a test failure like that, I usually run that in a loop, and hope that with enough iterations, I&amp;rsquo;ll get it to reproduce. Having it happen constantly on the 5th iteration was&amp;hellip; really strange. I tried figuring out what was going on, and I realized that the test was generating 1000 documents using a random. The fact that I&amp;rsquo;m using Random is the reason it is non-deterministic, of course, except that this is the code inside my test base class:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://ayende.com/blog/Images/Open-Live-Writer/Bug-chasing-the-process-is-more-importan_FFE0/image_2.png"&gt;&lt;img style="border: 0px currentcolor; display: inline; background-image: none;" title="image" src="https://ayende.com/blog/Images/Open-Live-Writer/Bug-chasing-the-process-is-more-importan_FFE0/image_thumb.png" alt="image" width="450" height="47" border="0" /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;So this is &lt;em&gt;properly&lt;/em&gt; initialized with a seed, so it will be consistent.&lt;/p&gt;
&lt;p&gt;Read the code again, do you see the problem?&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://ayende.com/blog/Images/Open-Live-Writer/Bug-chasing-the-process-is-more-importan_FFE0/image_7.png"&gt;&lt;img style="margin: 0px; border: 0px currentcolor; display: inline; background-image: none;" title="image" src="https://ayende.com/blog/Images/Open-Live-Writer/Bug-chasing-the-process-is-more-importan_FFE0/image_thumb_2.png" alt="image" width="450" height="47" border="0" /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That is a &lt;em&gt;static&lt;/em&gt; value. So there are two problems here:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I&amp;rsquo;m getting the &lt;em&gt;bad&lt;/em&gt; values on the fifth run in a consistent manner because that is the set of results that reproduce the error.&lt;/li&gt;
&lt;li&gt;This is a &lt;em&gt;shared&lt;/em&gt; instance that may be called from multiple tests at once, leading to the wrong result because Random is not thread safe.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Before fixing this issue so it would run properly, I decided to use an ancient debugging technique. It&amp;rsquo;s called printf().&lt;/p&gt;
&lt;p&gt;In this case, I wrote out all the values that were generated by the test and wrote a &lt;em&gt;new&lt;/em&gt; test to replay them. &lt;em&gt;That&lt;/em&gt; one failed consistently.&lt;/p&gt;
&lt;p&gt;The problem was that it was still too big in scope. I iterated over this approach, trying to end up with a smaller section of the codebase that I could invoke to repeat this issue. &lt;em&gt;That&lt;/em&gt; took most of the day. But the end result is a test like this:&lt;/p&gt;
&lt;blockquote&gt;
&lt;script src="https://gist.github.com/ayende/1af4847d0f7dc293f6b075824ca8c3d8.js"&gt;&lt;/script&gt;
&lt;/blockquote&gt;
&lt;p&gt;As you can see, in terms of the amount of code that it invokes, it is pretty minimal. Which is pretty awesome, since that allowed me to figure out what the problem was:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://ayende.com/blog/Images/Open-Live-Writer/Bug-chasing-the-process-is-more-importan_FFE0/image_9.png"&gt;&lt;img style="margin: 0px; border: 0px currentcolor; display: inline; background-image: none;" title="image" src="https://ayende.com/blog/Images/Open-Live-Writer/Bug-chasing-the-process-is-more-importan_FFE0/image_thumb_3.png" alt="image" width="247" height="76" border="0" /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I&amp;rsquo;ve been developing software professionally for over two decades at this point. I still get caught up with things like that, sigh.&lt;/p&gt;
</description><link>http://ayende.com/199425-b/bug-chasing-the-process-is-more-important-than-the-result?Key=25f17f7f-bece-4fee-98d2-91ccbe9df5e8</link><guid>http://ayende.com/199425-b/bug-chasing-the-process-is-more-important-than-the-result?Key=25f17f7f-bece-4fee-98d2-91ccbe9df5e8</guid><pubDate>Thu, 04 May 2023 12:00:00 GMT</pubDate></item><item><title>Fight for every byte it takes: Decoding the entries</title><description>&lt;p&gt;In &lt;a href="https://ayende.com/blog/posts/series/199329-B/fight-for-every-byte-it-takes"&gt;this series&lt;/a&gt; so far, we reduced the storage cost of key/value lookups by a &lt;em&gt;lot&lt;/em&gt;. And in &lt;a href="https://ayende.com/blog/199364-B/fight-for-every-byte-it-takes-optimizing-the-encoding-process?key=6f580f42d30f4d9dbf237ce0a0deb6b4"&gt;the last post&lt;/a&gt; we optimized the process of encoding the keys and values significantly. This is great, but the toughest challenge is ahead of us, because as much as encoding efficiency matters, &lt;em&gt;the &lt;/em&gt;absolute cost we have is doing lookups. This is the most basic operation, which we do billions of times a second. Any amount of effort we&amp;rsquo;ll spend here will be worth it. That said, let&amp;rsquo;s look at the decoding process we have right now. It was built to be &lt;em&gt;understandable&lt;/em&gt; over all else, so it is a good start.&lt;/p&gt;
&lt;blockquote&gt;
&lt;script src="https://gist.github.com/ayende/a26f6c6bf4d2544545d5f516812a45bf.js"&gt;&lt;/script&gt;
&lt;/blockquote&gt;
&lt;p align="left"&gt;What this code does is to accept a buffer and an offset into the buffer. But the offset isn&amp;rsquo;t just a number, it is composed&amp;nbsp; of two values. The first 12 bits contain the offset in the page, but since we use 2-byte alignment for the entry position, we can just assume a zero bit at the start. That is why we compute the actual offset in the page by clearing the first &lt;em&gt;four&lt;/em&gt; bits and then shifting left by &lt;em&gt;three&lt;/em&gt; bits. That extracts the actual offset to the file, (usually a 13 bits value) using just 12 bits. The first four bits in the offset are the indicator for the key and value lengths. There are 15 known values, which we computed based on probabilities and one value reserved to say: Rare key/val length combination, the actual sizes are stored as the first byte in the entry.&lt;/p&gt;
&lt;p align="left"&gt;Note that in the code, we handle that scenario by reading the key and value lengths (stored as two nibbles in the first byte) and &lt;em&gt;incrementing&lt;/em&gt; the offset in the page. That means that we skip past the header byte in those rare situations.&lt;/p&gt;
&lt;p align="left"&gt;The rest of the code is basically copying the key and value bytes to the relevant variables, taking advantage of partial copy and little-endian encoding.&lt;/p&gt;
&lt;p align="left"&gt;The code in question takes 512 bytes and has 23 branches. In terms of performance, we can probably do much better, but the code is clear in what it is doing, at least.&lt;/p&gt;
&lt;p align="left"&gt;The first thing I want to try is to replace&amp;nbsp; the switch statement with a lookup table, just like we did before.&amp;nbsp; Here is what the new version looks like:&lt;/p&gt;
&lt;blockquote&gt;
&lt;script src="https://gist.github.com/ayende/73cf097f99529e4126cf29401ed2a44d.js"&gt;&lt;/script&gt;
&lt;/blockquote&gt;
&lt;p align="left"&gt;The size of the function dropped by almost half and we have only 7 more branches involved. There are also a couple of calls to the memory copy routines that weren&amp;rsquo;t inlined. In the encoding phase, we reduced branches due to bound checks using raw pointers, and we skipped the memory calls routines by copying a fixed size value at varied offsets to be able to get the data properly&amp;nbsp; aligned. In this case, we can&amp;rsquo;t really do the same. One thing that we have to be aware of is the following situation:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p align="left"&gt;&lt;a href="https://ayende.com/blog/Images/Open-Live-Writer/Fight-for-every-byte-it-takes-Decoding-t_94FC/image_2.png"&gt;&lt;img style="margin: 0px; border: 0px currentcolor; display: inline; background-image: none;" title="image" src="https://ayende.com/blog/Images/Open-Live-Writer/Fight-for-every-byte-it-takes-Decoding-t_94FC/image_thumb.png" alt="image" width="496" height="85" border="0" /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p align="left"&gt;In other words, we may have an entry that is at the &lt;em&gt;end&lt;/em&gt; of the page, if we&amp;rsquo;ll try to read unconditionally 8 bytes, we may read past the end of the buffer. That is not something that we can do. In the Encode() case, we &lt;em&gt;know&lt;/em&gt; that the caller gave us a buffer large enough to accommodate the largest possible size, so that isn&amp;rsquo;t an issue. That complicates things, sadly, but we can go the other way around.&lt;/p&gt;
&lt;p align="left"&gt;The Decode() function will always be called on an entry, and that is part of the page. The way we place entries means that we are starting at the top and moving down. The structure of the page means that we can never actually place an entry below the first 8 bytes of the page. That is where the header and the offsets array are going, after all. Given that, we can do an unconditional read &lt;em&gt;backward&lt;/em&gt; from the entry. As you can see in the image below, we are reading some data that we don&amp;rsquo;t care about, but this is fine, we can fix it later, and without any branches.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p align="left"&gt;&lt;a href="https://ayende.com/blog/Images/Open-Live-Writer/Fight-for-every-byte-it-takes-Decoding-t_94FC/image_4.png"&gt;&lt;img style="margin: 0px; border: 0px currentcolor; display: inline; background-image: none;" title="image" src="https://ayende.com/blog/Images/Open-Live-Writer/Fight-for-every-byte-it-takes-Decoding-t_94FC/image_thumb_1.png" alt="image" width="495" height="121" border="0" /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p align="left"&gt;The end result is that we can have the following changes:&lt;/p&gt;
&lt;blockquote&gt;
&lt;script src="https://gist.github.com/ayende/e7a2bac1daecd81b507b916670edd268.js"&gt;&lt;/script&gt;
&lt;/blockquote&gt;
&lt;p align="left"&gt;I changed the code to use a raw pointer, avoiding bound checks that we already reasoned about. Most interesting is the ReadBackward function. This is an inner function, and was properly inlined during JIT compilation, it implements the backward reading of the value. Here is what the assembly looks like:&lt;/p&gt;
&lt;blockquote&gt;
&lt;script src="https://gist.github.com/ayende/4b9095357ed262ba9fba0b2d0f1cada3.js"&gt;&lt;/script&gt;
&lt;/blockquote&gt;
&lt;p align="left"&gt;With this in place, we are now at 133 bytes and a &lt;em&gt;single&lt;/em&gt; branch operation. That is pretty awesome, but we can do better still. Consider the following code (explanations to follow):&lt;/p&gt;
&lt;blockquote&gt;
&lt;script src="https://gist.github.com/ayende/e293a5f106e53ab35257e9cbdbe47a9f.js"&gt;&lt;/script&gt;
&lt;/blockquote&gt;
&lt;p align="left"&gt;Note that the first element in the table here is different, it is now setting the 4th bit. This is because we are making use of that. The structure of the bytes in the table are two nibbles, but no other value in the table sets the 4th bit. That means that we can operate on that.&lt;/p&gt;
&lt;p align="left"&gt;Indeed, what we are doing is use the decoder byte to figure out what sort of shift we want. We have the byte from the table and the byte from the buffer. And we use the fact that masking this with 8 gives (just for this value) the value of 8. We can then use &lt;em&gt;that&lt;/em&gt; to select the appropriate byte. If we have an offloaded byte, then we&amp;rsquo;ll shift the value by 8, getting the byte from the buffer. For any other value, we&amp;rsquo;ll get 0 as the shift index, resulting in us getting the value from the table. That gives us a function with zero branches, and 141 bytes.&lt;/p&gt;
&lt;p align="left"&gt;I spent a&lt;em&gt; lot&lt;/em&gt; of time thinking about this, so now that we have those two approaches, let's benchmark them. The results were surprising:&lt;/p&gt;
&lt;blockquote&gt;
&lt;pre&gt;|                  Method |       Mean |    Error |   StdDev |
|------------------------ |-----------:|---------:|---------:|
|  DecodeBranchlessShifts | 2,107.1 ns | 20.69 ns | 18.34 ns |
|           DecodeBranchy |   936.2 ns |  1.89 ns |  1.68 ns |&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;p align="left"&gt;It turns out that the slightly smaller code with the branches is able to &lt;em&gt;beat up&lt;/em&gt; the branchless code. When looking into what they are doing, I think that I can guess why. Branches aren&amp;rsquo;t a huge problem if they are predictable, and in our case, the whole &lt;em&gt;point&lt;/em&gt; of all of this is that the offload process where we need to go to the entry to get the value is meant to be a rare event. In branchless code, on the other hand, you have to do something several times to avoid a branch (like shifting the value from the buffer up and maybe shifting it down, etc).&lt;/p&gt;
&lt;p align="left"&gt;You can&amp;rsquo;t really argue with a difference like that. We also tried an AVX version, to see if this would have better performance. It turns out that there is really no way for us to beat the version with the single branch. Everything else was at least twice as slow.&lt;/p&gt;
&lt;p align="left"&gt;At this point, I believe that we have a winner.&lt;/p&gt;
</description><link>http://ayende.com/199393-b/fight-for-every-byte-it-takes-decoding-the-entries?Key=f241e460-96cc-4efa-a99d-56e5df3e9e1c</link><guid>http://ayende.com/199393-b/fight-for-every-byte-it-takes-decoding-the-entries?Key=f241e460-96cc-4efa-a99d-56e5df3e9e1c</guid><pubDate>Mon, 01 May 2023 12:00:00 GMT</pubDate></item><item><title>Fight for every byte it takes: Fitting 64 values in 4 bits</title><description>&lt;p&gt;Moving to &lt;a href="https://ayende.com/blog/199362-B/fight-for-every-byte-it-takes-nibbling-at-the-costs?key=9b599e1acc764686bc2c4a6b18a09121"&gt;nibble encoding&lt;/a&gt; gave us a measurable improvement in the density of the entries in the page.&amp;nbsp;&amp;nbsp; The problem is that we pretty much run out of room to do so. We are currently using a byte per entry to hold the size of the entry (as two nibbles, of 4 bits each). You can&amp;rsquo;t really go lower than that.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s review again what we know about the structure of the data, we have an 8KB page, with three sections, fixed size header and variable size offsets and entries array. Here is what this looks like:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://ayende.com/blog/Images/Open-Live-Writer/Fight-for-every-byte-it-takes-Fitting_12C2D/image_2.png"&gt;&lt;img style="margin: 0px; border: 0px currentcolor; display: inline; background-image: none;" title="image" src="https://ayende.com/blog/Images/Open-Live-Writer/Fight-for-every-byte-it-takes-Fitting_12C2D/image_thumb.png" alt="image" width="451" height="458" border="0" /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This is called a slotted page design. The idea is that the offset array at the bottom of the page is maintaining the sort orders of the entries, and that we can write the entries from the top of the page. When we need to sort the entries, we just need to touch the offsets array (shown in yellow in the image).&lt;/p&gt;
&lt;p&gt;Given that we are talking about size and density, we spent a lot of time trying to reduce the size of the entries, but can we do something with the header or the offsets? The header is just 4 bytes right now, two shorts that denote the location of the bottom and the top position in the page. Given that the page is 8KB in size, we have to use 16 bits integer to cover the range. For offsets, the situation is the same. We have to be able to point to the entry location on the page, and that means that we have to reach 8KB. So the offsets are actually 16 bits ints and take two bytes.&lt;/p&gt;
&lt;p&gt;In other words, there is a &lt;em&gt;hidden&lt;/em&gt; overhead of 2 bytes per entry that we didn&amp;rsquo;t even consider. In the case of our latest success, we were able to push 759 entries into the page, which means that we are actually using 18.5% of the page just to hold the &lt;em&gt;offsets&lt;/em&gt; of the entries. That is 1.48 KB that is being used.&lt;/p&gt;
&lt;p&gt;The problem is that we &lt;em&gt;need&lt;/em&gt; to use this. We have to be able to point to an entry anywhere in the page, which means that we have to reach 0 .. 8192. The minimum size we can use is 16 bits or two bytes.&lt;/p&gt;
&lt;p&gt;Or do we?&lt;/p&gt;
&lt;p&gt;16 bits gives us a range of 0 .. 65,535, after all. That is far in excess of what we need. We &lt;em&gt;could&lt;/em&gt; use a 64KB page, but there are other reasons to want to avoid that.&lt;/p&gt;
&lt;p&gt;To cover 8KB, we only need 13 bits to cover the range we need, after all. For that matter, we can extend that bit by 25%. If we decide that an entry should be 2 bytes aligned, we can access the entire page in 12 bits.&lt;/p&gt;
&lt;p&gt;That means that we have &lt;em&gt;4 whole free bits&lt;/em&gt; to play with. The first idea is to change the offsets array from 16 bits ints to 12 bits ints. That would save us 380 bytes at 759 entries per page. That is quite a lot. Unfortunately, working with bits in this manner would be &lt;em&gt;super&lt;/em&gt; awkward. We are doing a lot of random access and moves while we are building the page. It is &lt;em&gt;possible&lt;/em&gt; to do this using bits, but not fun.&lt;/p&gt;
&lt;p&gt;So we can set things up so we have a nibble free to use. We &lt;em&gt;just&lt;/em&gt; used nibbles to save on the cost of variable size ints, to great success.&lt;/p&gt;
&lt;p&gt;However, we don&amp;rsquo;t need just &lt;em&gt;a&lt;/em&gt; nibble, we need two of them. We need to store the size of the key and the value in bytes. Actually, we don&amp;rsquo;t need two nibbles. The size of the key and the value maxes at 8 bytes, after all. We can encode that in 3 bits. In other words, we need 6 bits to encode this information.&lt;/p&gt;
&lt;p&gt;We only have 4 bits, however. It is a &lt;em&gt;really&lt;/em&gt; nice idea, however, and I kept turning that in my head, trying to come up with all sorts of clever ways to figure out how we can push 64 values in 4 bits. The impact of that would be pretty amazing.&lt;/p&gt;
&lt;p&gt;Eventually, I realized that it is fairly easy to &lt;em&gt;prove&lt;/em&gt;, using math, that there is no way to do so. Faced with this failure, I realigned my thinking and found a solution. I don&amp;rsquo;t &lt;em&gt;need&lt;/em&gt; to have a perfect answer, I can have a &lt;em&gt;good &lt;/em&gt;one.&lt;/p&gt;
&lt;p&gt;4 bits give me a range of 16 values (out of the possible 64). If I give up on trying to solve the whole problem, can I solve a meaningful part of it?&lt;/p&gt;
&lt;p&gt;And I came up with the following idea. We can do a two-stage approach, we&amp;rsquo;ll map the most common 15 values of key and value sizes to those 4 bits. The last value will be a marker that you have to go and look elsewhere.&lt;/p&gt;
&lt;p&gt;Using just the data in the offset, I&amp;rsquo;m able to figure out what the location of the entry in the page is as well as the size of the key and value for most cases. For the (hopefully rare) scenarios where that is &lt;em&gt;not&lt;/em&gt; the case, we fall back to storing the size information as two nibbles preceding the entry data.&lt;/p&gt;
&lt;p&gt;This is a pretty neat idea, even if I say so myself, and it has a good chance to allow us to save about 1 byte per entry in the common case. In fact, I tested that and about 90% of the cases in my test case are covered by the top 15 cases. That is a pretty good indication that I&amp;rsquo;m on the right track.&lt;/p&gt;
&lt;p&gt;All of that said, let&amp;rsquo;s look at how this looks in code:&lt;/p&gt;
&lt;blockquote&gt;
&lt;script src="https://gist.github.com/ayende/766084b0333cf3eb90e4fdf66ec6f58c.js"&gt;&lt;/script&gt;
&lt;/blockquote&gt;
&lt;p&gt;I&amp;rsquo;m using a switch expression here for readability, so it is clear what is going on. If the key and value sizes are in one of the known patterns, we can put that in the nibble we&amp;rsquo;ll return. If the value is not, we&amp;rsquo;ll write it to the entry buffer.&lt;/p&gt;
&lt;p&gt;The Set method itself had to change in some subtle but crucial ways, let&amp;rsquo;s look at it first, then I&amp;rsquo;ll discuss those changes:&lt;/p&gt;
&lt;blockquote&gt;
&lt;script src="https://gist.github.com/ayende/e23479d9e77e0a81abaadc81704736ea.js"&gt;&lt;/script&gt;
&lt;/blockquote&gt;
&lt;p&gt;As before, we encode the entry into a temporary buffer. Now, in addition to getting the length of the entry, we are also getting the nibble that we&amp;rsquo;ll need to store.&lt;/p&gt;
&lt;p&gt;You can see the changes in how we work with the offsets array following that. When we need to update an existing value, we are using this construction to figure out the actual entry offset:&lt;/p&gt;
&lt;blockquote&gt;
&lt;pre&gt;&lt;span style="color: #9b00d3;"&gt;var&lt;/span&gt; actualEntryOffset = ((offsets[idx] &amp;amp; &lt;span style="color: #9b00d3;"&gt;0xFFF0&lt;/span&gt;) &amp;gt;&amp;gt; 3);&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;p&gt;What exactly is going on here? Don&amp;rsquo;t try to figure it out yet, let&amp;rsquo;s see how we are &lt;em&gt;writing &lt;/em&gt;the data:&lt;/p&gt;
&lt;blockquote&gt;
&lt;pre&gt;top = (&lt;span style="color: #0000ff;"&gt;ushort&lt;/span&gt;)((top - reqEntryLen) &amp;amp; ~1); &lt;span style="color: #9bbb59;"&gt;// align on two bytes boundary &lt;/span&gt;&lt;/pre&gt;
&lt;p&gt;offsets[idx] = (&lt;span style="color: #0000ff;"&gt;ushort&lt;/span&gt;)(top &amp;lt;&amp;lt; 3 | nibble);&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Those two code snippets may look very odd, so let&amp;rsquo;s go over them in detail.&lt;/p&gt;
&lt;p&gt;First, remember that we have an 8KB page to work with, but we need to use 4 bits for the size nibble we got from encoding the entry. To address the full 8,192 values in the page, we&amp;rsquo;ll need to reserve 13 bits. That is&amp;hellip; a problem. We solve that by saying that the entry addresses must always be aligned on two bytes boundary. That is handled by clearing the first bit in the new top computation. Since we are growing down, that has the effect of ensuring aligned-by-two.&lt;/p&gt;
&lt;p&gt;Then, we merge the top location and the nibble together. We &lt;em&gt;know&lt;/em&gt; that the bottom-most of the top is cleared, so we can just move the value by 3 bits and we know that we&amp;rsquo;ve 4 cleared bits ready.&lt;/p&gt;
&lt;p&gt;Conversely, when we want to &lt;em&gt;read&lt;/em&gt;, we clear the first 4 bits and then we shift by three. That has the effect of returning us back to the original state.&lt;/p&gt;
&lt;p&gt;A little bit confusing, but we managed to get to squeeze 784 entries into the page using the realistic dataset and 765 using the full one. That is another 3.5% of space savings over the previous nibble attempt and over 10% increase in capacity from the variable integer approach.&lt;/p&gt;
&lt;p&gt;And at this point, I don&amp;rsquo;t believe that there is anything more that I &lt;em&gt;can&lt;/em&gt; do to reduce the size in a significant manner without having a negative impact elsewhere.&lt;/p&gt;
&lt;p&gt;We are not done yet, however. We are done with the size aspect, but we also have much to do in terms of performance and optimizations for runtime.&lt;/p&gt;
&lt;p&gt;In the meantime, you can see &lt;a href="https://gist.github.com/ayende/af3c4a7acc8cc2508e0b676bc9a8eee5"&gt;my full code here&lt;/a&gt;. In the next post, we are going to start talking about the actual machine code and how we can optimize it.&lt;/p&gt;
</description><link>http://ayende.com/199363-b/fight-for-every-byte-it-takes-fitting-64-values-in-4-bits?Key=e403cbee-47d0-4370-8a9d-07eb165b8f7c</link><guid>http://ayende.com/199363-b/fight-for-every-byte-it-takes-fitting-64-values-in-4-bits?Key=e403cbee-47d0-4370-8a9d-07eb165b8f7c</guid><pubDate>Thu, 27 Apr 2023 12:00:00 GMT</pubDate></item><item><title>Fight for every byte it takes: Variable size data</title><description>&lt;p&gt;In &lt;a href="https://ayende.com/blog/199329-B/fight-for-every-byte-it-takes-storing-raw-numbers?key=8cb96c2c2e9c49a58ae7a0fd14b35d6b"&gt;my previous post&lt;/a&gt;, we stored keys and values as raw numbers inside the 8KB page. That was simple, but wasteful. For many scenarios, we are never going to need to utilize the full 8 bytes range for a long. Most numbers are far smaller.&lt;/p&gt;
&lt;p&gt;In the example I gave in the last post, we are storing the following range of numbers (file offsets, basically). I&amp;rsquo;m using two test scenarios, one where I&amp;rsquo;m testing the full range (for correctness) and one where I&amp;rsquo;m testing files under 512 GB in size. Given that we are trying to compress the space, once we hit the 512GB mark, it is probably &lt;em&gt;less&lt;/em&gt; urgent, after all.&lt;/p&gt;
&lt;p&gt;Here are the number generations that I&amp;rsquo;m using:&lt;/p&gt;
&lt;blockquote&gt;
&lt;script src="https://gist.github.com/ayende/afc45a55389d8dfe9348051f2b2e378f.js"&gt;&lt;/script&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;What this &lt;em&gt;means&lt;/em&gt; is:&lt;/p&gt;
&lt;table border="0" cellspacing="5" cellpadding="5"&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td valign="top"&gt;Full data set&lt;/td&gt;
&lt;td valign="top"&gt;Realistic data set&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td valign="top"&gt;
&lt;ul&gt;&lt;!--StartFragment--&gt;
&lt;li&gt;&amp;nbsp; 3% in the first 128 bytes&lt;/li&gt;
&lt;li&gt;&amp;nbsp; 7% in the first 64 KB&lt;/li&gt;
&lt;li&gt;25% in the first 8 MB&lt;/li&gt;
&lt;li&gt;35% in the first 2 GB&lt;/li&gt;
&lt;li&gt;15% in the first 512 GB&lt;/li&gt;
&lt;li&gt;5% in the first 128 TB&lt;/li&gt;
&lt;li&gt;3% in the first 32 Petabytes&lt;/li&gt;
&lt;li&gt;2% in the first 4 Exabytes&lt;/li&gt;
&lt;!--EndFragment--&gt;&lt;/ul&gt;
&lt;/td&gt;
&lt;td valign="top"&gt;
&lt;ul&gt;&lt;!--StartFragment--&gt;
&lt;li&gt;&amp;nbsp; 1% in the first 128 bytes&lt;/li&gt;
&lt;li&gt;&amp;nbsp; 2% in the first 64 KB&lt;/li&gt;
&lt;li&gt;27% in the first 8 MB&lt;/li&gt;
&lt;li&gt;35% in the first 2 GB&lt;/li&gt;
&lt;li&gt;25% in the first 512 GB&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul&gt;&lt;!--EndFragment--&gt;&lt;/ul&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;This is meant to verify that we can handle &lt;em&gt;any&lt;/em&gt; scenario, in practice, we can usually focus on the first 512 GB, which is far more common.&lt;/p&gt;
&lt;p&gt;Using both approaches, I can fit using &lt;a href="https://ayende.com/blog/199329-B/fight-for-every-byte-it-takes-storing-raw-numbers?key=8cb96c2c2e9c49a58ae7a0fd14b35d6b"&gt;my previous approach&lt;/a&gt;, up to 511 entries per page. That makes sense, we are storing the data raw, so how can we do better? Most of the time, we don&amp;rsquo;t need anywhere near 8 bytes per value. For that reason, we have &lt;a href="https://en.wikipedia.org/wiki/Variable-length_quantity"&gt;variable length encoding&lt;/a&gt;, which has many names, such as variable size int, 7 bits integers, etc. I adapted some methods from the .NET codebase to allow me to operate on Spans, like so:&lt;/p&gt;
&lt;blockquote&gt;
&lt;script src="https://gist.github.com/ayende/593bdcab072b3ddd94646e6c724870e5.js"&gt;&lt;/script&gt;
&lt;/blockquote&gt;
&lt;p&gt;Let&amp;rsquo;s check what sort of savings we can get using this approach:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Under 127 bytes&amp;ndash; 1 byte&lt;/li&gt;
&lt;li&gt;128 bytes .. 32 KB &amp;ndash; 2 bytes&lt;/li&gt;
&lt;li&gt;32KB .. 8MB &amp;ndash; 3 bytes&lt;/li&gt;
&lt;li&gt;8MB .. 2GB &amp;ndash; 4 bytes&lt;/li&gt;
&lt;li&gt;2 GB .. 512 GB &amp;ndash; 5 bytes&lt;/li&gt;
&lt;li&gt;512GB .. 128 TB &amp;ndash; 6 bytes&lt;/li&gt;
&lt;li&gt;128TB .. 32 Petabytes &amp;ndash; 7 bytes&lt;/li&gt;
&lt;li&gt;32 Petabytes .. 8 Exabytes &amp;ndash; 8 bytes&lt;/li&gt;
&lt;li&gt;Greater than 8 Exabytes &amp;ndash; 9 bytes&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That is really cool, since for the realistic data set, we can pack a lot more data into the page.&lt;/p&gt;
&lt;p&gt;It comes with a serious issue, however. The data is no longer fixed size (well, that &lt;em&gt;is &lt;/em&gt;the point, no?). Why is that a problem? Because we want to be able to do a binary search on that, which means that we need to be able to access the data by index. As usual, the solution is to utilize indirection. We&amp;rsquo;ll dedicate the bottom of the page to an array of fixed-size int (16 bits &amp;ndash; sufficient to cover the 8KB range of the page) that will point to the actual location of the entry. Like before, we are going to reserve the first few bytes as a header, in this case we&amp;rsquo;ll use 4 bytes, divided into two shorts. Those will keep track of the writes to the bottom and the top of the page.&lt;/p&gt;
&lt;p&gt;At the bottom, we&amp;rsquo;ll have the actual offsets that point to the entries, and at the top, we write the actual entries. Here is what this looks like:&lt;/p&gt;
&lt;blockquote&gt;
&lt;script src="https://gist.github.com/ayende/fd693cbc0689ba30da6ea0035044e817.js"&gt;&lt;/script&gt;
&lt;/blockquote&gt;
&lt;p&gt;Let&amp;rsquo;s see how our reading from the page will look now. As you can see, it is very similar to what we had before, but instead of going directly to the key by its offset, we have to use the indirection:&lt;/p&gt;
&lt;blockquote&gt;
&lt;script src="https://gist.github.com/ayende/63a0469aed1da7724b6b58c06d57d286.js"&gt;&lt;/script&gt;
&lt;/blockquote&gt;
&lt;p&gt;The offsets array contains the location of the entry in the page, and that is laid out as the [varint-key][varint-val]. So we read (and discard) the key from the offset we found (we have to do that to discover its size) and then we read and return the actual value.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s look at how we implemented the actual binary search in the page:&lt;/p&gt;
&lt;blockquote&gt;
&lt;script src="https://gist.github.com/ayende/f5657de04f85b10f363aaff80096dd4c.js"&gt;&lt;/script&gt;
&lt;/blockquote&gt;
&lt;p&gt;This is a bog standard binary search, with the only interesting bit that we are going through the offsets array to find the actual location of the key, which we then read using variable size decoding.&lt;/p&gt;
&lt;p&gt;The interesting part of this model happens when we need to &lt;em&gt;set&lt;/em&gt; a value. Here is what this looks like, with my notes following the code.&lt;/p&gt;
&lt;blockquote&gt;
&lt;script src="https://gist.github.com/ayende/03d11c5a244bce32a52c8579c6233764.js"&gt;&lt;/script&gt;
&lt;/blockquote&gt;
&lt;p&gt;This is quite a lot, I&amp;rsquo;ll admit. Let&amp;rsquo;s try to break up into individual pieces what is going on here.&lt;/p&gt;
&lt;p&gt;First, we get the header values (bottom, top) and initialize them if empty (note that bottom is set to 4, &lt;em&gt;after &lt;/em&gt;the header, while top is set to the &lt;em&gt;end&lt;/em&gt; of the buffer). The idea is that the bottom grows up and the top grows down. This is called &lt;a href="https://siemens.blog/posts/database-page-layout/"&gt;Slotted Page design&lt;/a&gt; and it is a staple of database design.&lt;/p&gt;
&lt;p&gt;We then encode the key and the value into a temporary buffer. We need to do that so we&amp;rsquo;ll know what size the entry will take. Then we need to figure out if we are updating an existing record or creating a new one.&lt;/p&gt;
&lt;p&gt;Updating an existing record is complex. This is because the &lt;em&gt;size&lt;/em&gt; of the new record may be &lt;em&gt;greater&lt;/em&gt; than the size of the old one. So we can&amp;rsquo;t put it in the same location. I&amp;rsquo;m handling this by just allocating new space for this entry, ignoring the old space that was allocated to it.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I&amp;rsquo;m not handling any deletes / space reclamation on this series. That is a separate subject, not complex, but fairly tedious to do properly. So I&amp;rsquo;m going to focus solely on writes.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Updates to an existing entry that also change its size aren&amp;rsquo;t in my test dataset, so I&amp;rsquo;m not worried about it too much here. I mention this to point out that variable length records bring with them considerations that we wouldn&amp;rsquo;t have run into with the fixed-size model.&lt;/p&gt;
&lt;p&gt;And after all of this work? What are the results?&lt;/p&gt;
&lt;p&gt;With the fixed-size version, we could fit 511 entries into the page. With the variable size int, however, we can do better.&lt;/p&gt;
&lt;p&gt;For the realistic dataset, I can fit 712 entries for the page, and for the full dataset, 710 (there are very few very big elements even there, but we can see that it has an impact).&lt;/p&gt;
&lt;p&gt;511 vs. 712 may not sound like much, but that is 40% increase in the number of entries that I can fit. To give some context, using 8KB pages, that is a difference of 5 MB per million entries. That &lt;em&gt;adds&lt;/em&gt; up.&lt;/p&gt;
&lt;p&gt;The question is, can we do &lt;em&gt;better&lt;/em&gt;? More on that in my next post&amp;hellip;&lt;/p&gt;
</description><link>http://ayende.com/199361-b/fight-for-every-byte-it-takes-variable-size-data?Key=73f1175b-8518-4bd4-8657-233bf8dd7942</link><guid>http://ayende.com/199361-b/fight-for-every-byte-it-takes-variable-size-data?Key=73f1175b-8518-4bd4-8657-233bf8dd7942</guid><pubDate>Tue, 25 Apr 2023 12:00:00 GMT</pubDate></item><item><title>Fight for every byte it takes: Storing raw numbers</title><description>&lt;p&gt;I write databases for a living, which means that I&amp;rsquo;m thinking a &lt;em&gt;lot&lt;/em&gt; about persistence. Here is a fun challenge that we went through recently. We have the need to store a list of keys and values and then lookup a value by key. Pretty standard stuff. The keys and values are both 64 bits integers. In other words, what I would like to have is:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Dictionary&amp;lt;long,long&amp;gt; lookup;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That would be perfect, except that I&amp;rsquo;ve to persist the data, which means that I have to work with raw bytes. It&amp;rsquo;s easiest to think about it if we have some code in front of us. Here is the interface that I need to implement:&lt;/p&gt;
&lt;blockquote&gt;
&lt;script src="https://gist.github.com/ayende/3db756efe28b2f9a06a59647af4d26de.js"&gt;&lt;/script&gt;
&lt;/blockquote&gt;
&lt;p&gt;As you can see, we have a byte buffer (8KB in size) and we want to add or lookup values from the buffer. All the data resides&amp;nbsp;&lt;em&gt;in&lt;/em&gt; the buffer, nothing is external. And we cannot unpack it in memory, because this is used for lookups, so this needs to be really fast.&lt;/p&gt;
&lt;p&gt;The keys we are storing are file offsets, so they correlate quite nicely to the overall size of the file. Meaning that you&amp;rsquo;ll have a lot of small values, but also large ones. Given a key, we need to be able to look its value quickly, since we may run this lookup billions of times.&lt;/p&gt;
&lt;p&gt;Given that I have 8KB of data, I can do the following, just treat the buffer as a sorted array, which means that I get a pretty easy way to search for a particular value and a simple way to actually store things.&lt;/p&gt;
&lt;p&gt;Theoretically, given an 8KB page, and 16 bytes per each (key, value) entry, we can store up to 512 entries per page. But it turns out that this is just a theory. We also need to keep track of the &lt;em&gt;number&lt;/em&gt; of items that we have, and that takes some space. Just a couple of bytes, but it means that we don&amp;rsquo;t have those bytes available. A page can now contain up to 511 entries, and even at full capacity, we have 14 bytes wasted (2 for the number of entries, and the rest are unused).&lt;/p&gt;
&lt;p&gt;Here is what this looks like in code:&lt;/p&gt;
&lt;blockquote&gt;
&lt;script src="https://gist.github.com/ayende/46510091980b51cfb862456b8c7a9d8f.js"&gt;&lt;/script&gt;
&lt;/blockquote&gt;
&lt;p&gt;As you can see, we are creating two arrays, the keys are growing from the bottom of the page and the values are growing from the top. The idea is that I can utilize the BinarySearch() method to quickly find the index of a key (or where it&amp;nbsp;&lt;em&gt;ought&lt;/em&gt;) to go. From there, I can look at the corresponding values array to get the actual value. The fact that they are growing separately (and toward each other) means that I don&amp;rsquo;t need to move as much memory if I&amp;rsquo;m getting values out of order.&lt;/p&gt;
&lt;p&gt;For now, I want to set up the playground in which we&amp;rsquo;ll operate. The &lt;em&gt;type&lt;/em&gt; of data that you write into such a system is important. I decided to use the following code to generate the test set:&lt;/p&gt;
&lt;blockquote&gt;
&lt;script src="https://gist.github.com/ayende/288e87d6f1209e33285043c9a072e6ed.js"&gt;&lt;/script&gt;
&lt;/blockquote&gt;
&lt;p&gt;The idea is that we&amp;rsquo;ll generate a random set of numbers, in the given distribution. Most of the values are in the range of 8MB to 512GB, representing a pretty good scenario overall, I think.&lt;/p&gt;
&lt;p&gt;And with that, we just need to figure out what metrics we want to use for this purpose. My goal is to push as many values as I can into the buffer, while maintaining the ability to get a value by its key as fast as possible.&lt;/p&gt;
&lt;p&gt;The current approach, for example, does a binary search on a sorted array plus an extra lookup to the companion values array. You really can&amp;rsquo;t beat this, if you allow to store arbitrary keys. Here is my test bench:&lt;/p&gt;
&lt;blockquote&gt;
&lt;script src="https://gist.github.com/ayende/6e53c1d996068c9990878006c4ae76aa.js"&gt;&lt;/script&gt;
&lt;/blockquote&gt;
&lt;p&gt;This will insert key/value pairs into the page until it is full. Note that we allow duplicates (we&amp;rsquo;ll just update the value), so we need to keep track of the number of &lt;em&gt;entries&lt;/em&gt; inserted, not just the number of insertions.&amp;nbsp; We also validate the structure at any step of the way, to ensure that we always get the right behavior.&lt;/p&gt;
&lt;p&gt;This code runs as expected and we can put 511 values into the page before it gives up. This approach works, it is simple to reason about and has very few flaws. It is also quite wasteful in terms of information density. I would like to do better than 511 entries / pager. &lt;em&gt;Is&lt;/em&gt; it possible to drop below 16 bytes per entry?&lt;/p&gt;
&lt;p&gt;Give it some thought, I&amp;rsquo;m going to present several ways of doing just that in my next post&amp;hellip;&lt;/p&gt;
</description><link>http://ayende.com/199329-b/fight-for-every-byte-it-takes-storing-raw-numbers?Key=8cb96c2c-2e9c-49a5-8ae7-a0fd14b35d6b</link><guid>http://ayende.com/199329-b/fight-for-every-byte-it-takes-storing-raw-numbers?Key=8cb96c2c-2e9c-49a5-8ae7-a0fd14b35d6b</guid><pubDate>Mon, 24 Apr 2023 12:00:00 GMT</pubDate></item><item><title>Looking into Corax’s posting lists: Part III</title><description>&lt;p&gt;We looked into the internal of Corax&amp;rsquo;s posting list and in &lt;a href="https://ayende.com/blog/199267-A/looking-into-coraxs-posting-lists-part-ii?key=dc8bc34f37ca47beb2058b8c0de223e8"&gt;the last post&lt;/a&gt; I mentioned that we have a problem with the Baseline of the page.&lt;/p&gt;
&lt;p&gt;We are by no means the first people to work with posting lists, and there is a &lt;em&gt;wide&lt;/em&gt; body of knowledge on the topic. When it came time to figure out the compression format for our posting list, we used the PFOR format (Patched Frame of Reference). It, like pretty much all other integer compression methods, uses 32 bits integers. Corax utilizes 64 bits integer as the document ids, so that was a problem. We solved that problem by using a Baseline for each page. In other words, each page would be able to contain values in a range of 2.1 billion of one another. That is a very reasonable range, given that a page is 8KB in size.&lt;/p&gt;
&lt;p&gt;There was a problem as we built more features into Corax. The posting list needed to store not just the document id, but also the frequency of the term in the source document. It turns out that we need 8 bits to do so, and we already have 64 bits range so&amp;hellip; Instead of creating another location to store the frequencies, we put them directly inside the posting list. But that meant that we reserved a range of bits. We have 64 bits overall, so not a big problem, right? Except that on a page basis, we have a lot less. Before, a page could contain a range of 2.1 billion, but we reserved 10 bits (frequency and tag, the details of which are not important to our story) and we ended up with a range that is 4 &lt;em&gt;million &lt;/em&gt;per page. That little tidbit meant that we could only store in a page items that were within 4MB of one another. And &lt;em&gt;that&lt;/em&gt; was a problem. Whenever we had a posting list where two values would be more than 4MB from one another, we would need to split the page. And since the posting list and the entries live on the same file, having more page splits means that entries are now further apart.&lt;/p&gt;
&lt;p&gt;Here is an example of what this looks like:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://ayende.com/blog/Images/Open-Live-Writer/Looking-into-Coraxs-posting-lists-Part-I_E657/image_2.png"&gt;&lt;img style="margin: 0px; border: 0px currentcolor; display: inline; background-image: none;" title="image" src="https://ayende.com/blog/Images/Open-Live-Writer/Looking-into-Coraxs-posting-lists-Part-I_E657/image_thumb.png" alt="image" width="988" height="101" border="0" /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The index is taking more space than the source data, and most of that is used to store&amp;hellip; nothing, since we ended up with a very wide posting list containing very few entries. One of the cases of two independent issues compounding each other &lt;em&gt;very&lt;/em&gt; quickly.&lt;/p&gt;
&lt;p&gt;So we changed things again, instead of limiting ourselves to 32 bits range per page, we changed the PFor format to allow for 64 bits integers directly. Once again, that leads to simplification in the codebase and has greatly reduced the amount of disk space that we are actually using.&lt;/p&gt;
&lt;p&gt;To give some context, here is the current amount of disk space taken by the same entity that previously took 800+GB:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://ayende.com/blog/Images/Open-Live-Writer/Looking-into-Coraxs-posting-lists-Part-I_E657/image_4.png"&gt;&lt;img style="margin: 0px; border: 0px currentcolor; display: inline; background-image: none;" title="image" src="https://ayende.com/blog/Images/Open-Live-Writer/Looking-into-Coraxs-posting-lists-Part-I_E657/image_thumb_1.png" alt="image" width="1076" height="41" border="0" /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The problem wasn&amp;rsquo;t with the number of entries, but that each entry would consume 8KB of disk space on its own, and in the image, you are seeing the number of &lt;em&gt;posting lists, &lt;/em&gt;not the number of posting lists entries.&lt;/p&gt;
</description><link>http://ayende.com/199268-a/looking-into-corax-s-posting-lists-part-iii?Key=632f3344-32af-489b-b7aa-c3c3f707042c</link><guid>http://ayende.com/199268-a/looking-into-corax-s-posting-lists-part-iii?Key=632f3344-32af-489b-b7aa-c3c3f707042c</guid><pubDate>Mon, 17 Apr 2023 12:00:00 GMT</pubDate></item><item><title>Looking into Corax’s posting lists: Part II</title><description>&lt;p&gt;In a &lt;a href="https://ayende.com/blog/198594-A/looking-into-coraxs-posting-lists-part-i?key=5172525066dd45348327285e42ef0483"&gt;previous post&lt;/a&gt; (which went out a long time ago) I explained that we have the notion of a set of uint64 values that are used for document IDs. We build a B+Tree with different behaviors for branch pages and leaf pages, allowing us to pack a lot of document IDs (thousands or more) per page.&lt;/p&gt;
&lt;p&gt;The problem is that this structure hold the data compressed, so when we add or remove a value, we don&amp;rsquo;t &lt;em&gt;know&lt;/em&gt; if it exists already or not. That is a problem, because while we are able to do any relevant fixups to skip duplicates and erase removed values, we end up in a position where the number of entries in the set is not accurate. That is a Problem, with a capital P, since we use that for query optimizations.&lt;/p&gt;
&lt;p&gt;The solution for that is to move to a different manner of storing the data in the leaf page, instead of going with a model where we add the data directly to the page and compress when the raw values section overflows, we&amp;rsquo;ll use the following format instead:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://ayende.com/blog/Images/Open-Live-Writer/Looking-into-Coraxs-posting-lists-Part-I_129A4/image_2.png"&gt;&lt;img style="margin: 0px; border: 0px currentcolor; display: inline; background-image: none;" title="image" src="https://ayende.com/blog/Images/Open-Live-Writer/Looking-into-Coraxs-posting-lists-Part-I_129A4/image_thumb.png" alt="image" width="504" height="334" border="0" /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Basically, I removed the raw values section from the design entirely. That means that whenever we want to add a new value, we need to find the relevant compressed segment inside the page and add to it (potentially creating a page split, etc).&lt;/p&gt;
&lt;p&gt;Obviously, that is &lt;em&gt;not&lt;/em&gt; going to perform well for write. Since on each addition, we&amp;rsquo;ll need to decompress the segment, add to it and then compress it again.&lt;/p&gt;
&lt;p&gt;The idea here is that we don&amp;rsquo;t &lt;em&gt;need&lt;/em&gt; to do that. Instead of trying to store the entries in the set immediately, we are going to keep them in memory for the duration of the transaction. Just before we commit the transaction, we are going to have two lists of document IDs to go through. One of added documents and one of removed documents. We can then sort those ids and then start walking over the list, find the relevant page for each section in the list, and merging it with the compressed values.&lt;/p&gt;
&lt;p&gt;By moving the buffering stage from the per-page model to the per-transaction model, we actually gain quite a lot of performance, since if we have a lot of changes to a single page, we can handle compression of the data only once. It is a very strange manner of working, to be honest, because I&amp;rsquo;m used to doing the operation immediately. By delaying the cost to the end of the transaction, we are able to gain two major benefits. First, we have a big opportunity for batching and optimizing work on large datasets. Second, we have a single code path for this operation. It&amp;rsquo;s always: &amp;ldquo;Get a batch of changes and apply them as a unit&amp;rdquo;. It turns out that this is far easier to understand and work with. And that is for the writes portion of Corax.&lt;/p&gt;
&lt;p&gt;Remember, however, that Corax is a search engine, so we expect a &lt;em&gt;lot&lt;/em&gt; of reads. For reads, we can now stream the results directly from the compressed segments. Given that we can usually pack a &lt;em&gt;lot&lt;/em&gt; of numbers into a segment, and that we don&amp;rsquo;t need to compare to the uncompressed portion, that ended up benefiting us significantly on the read side as well, surprisingly.&lt;/p&gt;
&lt;p&gt;Of course, there is also another issue, look at the Baseline in the Page Header? We&amp;rsquo;ll discuss that in the next post, turned out that it wasn&amp;rsquo;t such a good idea.&lt;/p&gt;
</description><link>http://ayende.com/199267-a/looking-into-corax-s-posting-lists-part-ii?Key=dc8bc34f-37ca-47be-b205-8b8c0de223e8</link><guid>http://ayende.com/199267-a/looking-into-corax-s-posting-lists-part-ii?Key=dc8bc34f-37ca-47be-b205-8b8c0de223e8</guid><pubDate>Fri, 14 Apr 2023 12:00:00 GMT</pubDate></item></channel></rss>