<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ian Cowley</title>
    <description>The latest articles on DEV Community by Ian Cowley (@iancowley).</description>
    <link>https://dev.to/iancowley</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3928889%2F2cfd8346-cffe-47c8-8e57-0aef2a0a4abc.jpeg</url>
      <title>DEV Community: Ian Cowley</title>
      <link>https://dev.to/iancowley</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/iancowley"/>
    <language>en</language>
    <item>
      <title>Data-Oriented Design in C#: Why Objects Are Slowing You Down</title>
      <dc:creator>Ian Cowley</dc:creator>
      <pubDate>Tue, 23 Jun 2026 12:54:16 +0000</pubDate>
      <link>https://dev.to/iancowley/data-oriented-design-in-c-why-objects-are-slowing-you-down-51ak</link>
      <guid>https://dev.to/iancowley/data-oriented-design-in-c-why-objects-are-slowing-you-down-51ak</guid>
      <description>&lt;h1&gt;
  
  
  Data-Oriented Design in C#: Why Objects Are Slowing You Down
&lt;/h1&gt;

&lt;p&gt;In my previous article, we talked about starving the Garbage Collector by moving away from heap-allocated &lt;code&gt;class&lt;/code&gt; types and leaning heavily into &lt;code&gt;struct&lt;/code&gt;, &lt;code&gt;Span&amp;lt;T&amp;gt;&lt;/code&gt;, and &lt;code&gt;ArrayPool&amp;lt;T&amp;gt;&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;That’s a critical first step, but it only solves half the problem. You’ve stopped the GC from pausing your app, but you might still be leaving massive amounts of CPU performance on the table. Why? Because of how your data is structured. &lt;/p&gt;

&lt;p&gt;It’s time to talk about &lt;strong&gt;Data-Oriented Design (DoD)&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Object-Oriented Trap
&lt;/h2&gt;

&lt;p&gt;We are taught from day one to model our code after the real world. If you are building a social network graph, you might write something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserNode&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Edge&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Connections&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Edge&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;UserNode&lt;/span&gt; &lt;span class="n"&gt;Target&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;Weight&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes perfect logical sense. A user has connections, and those connections point to other users. &lt;/p&gt;

&lt;p&gt;But modern CPUs don't care about your logical models. A CPU only cares about reading data from memory into its L1/L2 caches as fast as possible. When a CPU reads a byte from RAM, it doesn't just read that one byte; it pulls a whole 64-byte "cache line" under the assumption that you will probably want the neighboring bytes next.&lt;/p&gt;

&lt;p&gt;When you loop through a &lt;code&gt;List&amp;lt;UserNode&amp;gt;&lt;/code&gt;, traversing from object to object, you are jumping randomly across the heap. The CPU pulls a cache line, reads your data, and then has to go fetch a completely different block of RAM for the next node. This is called &lt;strong&gt;pointer chasing&lt;/strong&gt;, and the resulting &lt;strong&gt;cache misses&lt;/strong&gt; are devastating to performance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enter Data-Oriented Design: Struct of Arrays (SoA)
&lt;/h2&gt;

&lt;p&gt;Data-Oriented Design says: &lt;em&gt;Stop modeling the real world. Model the data the way the hardware wants to consume it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Instead of an &lt;strong&gt;Array of Structs (AoS)&lt;/strong&gt; (or an array of objects), we invert the architecture to a &lt;strong&gt;Struct of Arrays (SoA)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If we look at how the native DataFrame engine &lt;a href="https://github.com/ian-cowley/Glacier.Polaris" rel="noopener noreferrer"&gt;Glacier.Polaris&lt;/a&gt; or the graph engine &lt;a href="https://github.com/ian-cowley/Glacier.Graph" rel="noopener noreferrer"&gt;Glacier.Graph&lt;/a&gt; operates, there are no &lt;code&gt;Node&lt;/code&gt; or &lt;code&gt;Edge&lt;/code&gt; classes. Instead, we use flat, primitive arrays. &lt;/p&gt;

&lt;p&gt;To represent a graph, &lt;a href="https://github.com/ian-cowley/Glacier.Graph" rel="noopener noreferrer"&gt;Glacier.Graph&lt;/a&gt; uses the &lt;strong&gt;Compressed Sparse Row (CSR)&lt;/strong&gt; format. The entire graph structure is flattened into a few dense integer arrays:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CsrGraph&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// The index in the _to array where a node's edges begin&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;_head&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; 

    &lt;span class="c1"&gt;// The target node IDs&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;_to&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   

    &lt;span class="c1"&gt;// The relationship types or weights&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;_relation&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; 
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Cache-Friendly Loop
&lt;/h2&gt;

&lt;p&gt;Let's say we want to find all connections for Node 5. In the OOP world, we follow pointers on the heap. In the CSR world, we do this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;startEdgeIndex&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_head&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;endEdgeIndex&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_head&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;6&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="c1"&gt;// Look at how perfectly sequential this is!&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;startEdgeIndex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;endEdgeIndex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;++)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;targetNode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_to&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;relationWeight&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_relation&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="c1"&gt;// Process connection...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why is this so much faster?&lt;br&gt;
Because &lt;code&gt;_to&lt;/code&gt; and &lt;code&gt;_relation&lt;/code&gt; are dense, contiguous &lt;code&gt;int&lt;/code&gt; arrays. As we loop through &lt;code&gt;i&lt;/code&gt;, the CPU's pre-fetcher easily predicts our access pattern. It loads the cache lines ahead of time. By the time our loop needs &lt;code&gt;_to[i+1]&lt;/code&gt;, it is already sitting in the blazing fast L1 cache.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Secret Weapon: SIMD
&lt;/h2&gt;

&lt;p&gt;But cache lines are only the beginning. Once your data is sitting in a flat primitive array, you unlock the real superpower of modern .NET: SIMD (Single Instruction, Multiple Data).&lt;/p&gt;

&lt;p&gt;You cannot pass an array of &lt;code&gt;UserNode&lt;/code&gt; objects into an AVX-512 vector register to evaluate them simultaneously. But you can load 8 consecutive integers from that &lt;code&gt;_relation&lt;/code&gt; array into a &lt;code&gt;Vector256&amp;lt;int&amp;gt;&lt;/code&gt; and evaluate them all in a single CPU clock cycle.&lt;/p&gt;

&lt;p&gt;When you align your memory this way, C# allows you to drop right down to the metal. This is exactly how &lt;a href="https://github.com/ian-cowley/Glacier.Chrono" rel="noopener noreferrer"&gt;Glacier.Chrono&lt;/a&gt; hits over 2 Billion operations per second without breaking a sweat.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bottom Line
&lt;/h2&gt;

&lt;p&gt;Object-Oriented Programming is fantastic for UI components and high-level business logic. But when you drop down into the engine room—when you are building dataframes, graph databases, or processing millions of records a second—you have to think like the CPU.&lt;/p&gt;

&lt;p&gt;Flatten your objects. Separate your properties into contiguous arrays. Design for cache hits, and your code will run orders of magnitude faster.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>csharp</category>
      <category>dotnet</category>
      <category>performance</category>
    </item>
    <item>
      <title>Replacing Electron with .NET 10: Writing a zero-latency, SIMD-accelerated IDE</title>
      <dc:creator>Ian Cowley</dc:creator>
      <pubDate>Sat, 20 Jun 2026 17:28:18 +0000</pubDate>
      <link>https://dev.to/iancowley/replacing-electron-with-net-10-writing-a-zero-latency-simd-accelerated-ide-322j</link>
      <guid>https://dev.to/iancowley/replacing-electron-with-net-10-writing-a-zero-latency-simd-accelerated-ide-322j</guid>
      <description>&lt;p&gt;Modern development tools have normalized lag. As an industry, we’ve somehow accepted that opening a text editor should consume 2GB of RAM, and that typing rapidly should occasionally stutter while an extension parses a JSON tree on the UI thread.&lt;/p&gt;

&lt;p&gt;The standard playbook for building cross-platform editors today is to use Web Technologies (Electron/Tauri) or to drag along a decades-old monolithic C++ architecture.&lt;/p&gt;

&lt;p&gt;But when it comes to high-performance, memory-intensive data processing, &lt;strong&gt;C# and .NET 10 are absolute bare-metal contenders&lt;/strong&gt;. We don't need to default to browser engines or Rust to get sub-millisecond startup times and zero-allocation execution.&lt;/p&gt;

&lt;p&gt;To prove it, I wrote &lt;strong&gt;SpanCoder&lt;/strong&gt;: a cutting-edge, extensibility-first IDE built entirely on .NET 10 and Avalonia UI. It features a custom Skia-rendered canvas, a zero-allocation piece-table text buffer, hardware-accelerated SIMD parsing, and a heavily decoupled process architecture.&lt;/p&gt;

&lt;p&gt;Here is how you build a premium, out-of-process IDE with C# without waking up the Garbage Collector.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;1. The Death of the Monolith: Out-of-Process Resiliency&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;If a single third-party extension crashes, your IDE should not freeze.&lt;/p&gt;

&lt;p&gt;Instead of a monolithic architecture where a heavy UI thread handles rendering, buffer parsing, and extensions, SpanCoder isolates everything into dedicated processes communicating over low-latency binary-serialized TCP channels and standard Stdio redirects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SpanCoder.App (Shell UI):&lt;/strong&gt; Handles only UI layout, custom canvas rendering, and input events on the main thread.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SpanCoder.Engine:&lt;/strong&gt; A completely isolated background process handling the heavy text data structures and file I/O.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Language Servers (LSP) &amp;amp; Debuggers (DAP):&lt;/strong&gt; Spawned entirely out-of-process via standard JSON-RPC.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plugin Hosts:&lt;/strong&gt; Third-party code runs in external sandboxes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because the UI is isolated, we maintain an &lt;strong&gt;Active Replay Ledger&lt;/strong&gt;. If the background text engine crashes due to an out-of-memory error from a massive file, the UI instantly respawns it, replays the transaction ledger over the socket, and restores the exact state. You lose zero keystrokes, and the UI never locks up.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;2. The Text Engine: Piece Tables and Zero Allocations&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;String concatenation is the enemy of performance. If you open a 2GB log file and type a single character in the middle, re-allocating that array will bring the system to its knees.&lt;/p&gt;

&lt;p&gt;SpanCoder’s engine uses a &lt;strong&gt;Piece Table Buffer&lt;/strong&gt;. Text modifications are stored as a sequence of span descriptors pointing either back to the immutable original file on disk or to a thread-safe, pre-allocated ArrayPool&amp;lt;char&amp;gt;.&lt;/p&gt;

&lt;p&gt;Edits are O(edits) rather than O(document length).&lt;/p&gt;

&lt;p&gt;When the custom Skia UI canvas needs to render a line, the engine checks if the segment lies contiguously in memory. If it does, it yields a ReadOnlySpan&amp;lt;char&amp;gt; directly back to the UI. The bytes go from the buffer to the screen with exactly &lt;strong&gt;zero heap allocations&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;3. SIMD-Accelerated Line Indexing&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;When you insert a new line at the top of a 100,000-line document, the byte offsets for the remaining 99,999 lines all shift. In a standard linear loop, this takes time.&lt;/p&gt;

&lt;p&gt;To fix this, SpanCoder maps absolute byte offsets using hardware intrinsics. When an edit occurs, we use .NET 10 Vector&amp;lt;long&amp;gt; from System.Numerics to shift the line array adjustments in parallel blocks. If your hardware supports AVX-512, we update 8 lines per CPU clock cycle.&lt;/p&gt;

&lt;p&gt;C# is routinely underestimated in the data analytics and processing arena, but when you leverage memory-mapped files and SIMD instructions, it performs like a native, unmanaged language.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;4. Sub-Millisecond Startup via Native AOT&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Modern C# isn't just fast at runtime; it can compile down to a single, zero-dependency native executable.&lt;/p&gt;

&lt;p&gt;However, to survive Native AOT compilation, you have to eliminate Reflection entirely. Most IDEs use reflection at startup to scan for commands, menus, and plugins, which destroys cold-start times.&lt;/p&gt;

&lt;p&gt;SpanCoder utilizes &lt;strong&gt;C# Source Generators&lt;/strong&gt;. When you tag a method with [Command] or [MenuItem], Roslyn analyzes it at compile-time and injects a static routing dictionary directly into the build.&lt;/p&gt;

&lt;p&gt;The result? Zero reflection, complete trimmer safety, and an IDE that opens virtually the moment you click the icon.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;5. Built for the Modern Workflow&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;While performance is the foundation, an IDE has to actually be useful. SpanCoder wraps this high-performance core in a premium developer experience:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zero-Config Local AI (Ghost Text):&lt;/strong&gt; Auto-detects local Ollama instances and pulls the qwen2.5-coder model in the background. It uses Fill-in-the-Middle (FIM) to render sub-100ms inline autocompletions (Ghost Text) completely offline, with zero API costs.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Embedded AI Agent (YOLO Dev):&lt;/strong&gt; A built-in terminal and chat agent that coordinates LLM reasoning and workspace tool executions in the background engine without blocking the UI.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hardware Debugging:&lt;/strong&gt; Built-in graphical target managers for flashing RP2040s or STM32s, complete with an embedded PTY terminal and Microcontroller DAP stepping.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real-Time Collaboration:&lt;/strong&gt; Synchronizes editor buffers between developers using a low-latency Conflict-free Replicated Data Type (CRDT) protocol over WebSockets.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Takeaway&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;It is incredibly easy to look at the current software landscape and assume that everything has to be built on web technologies to be cross-platform, or built in C++/Rust to be fast.&lt;/p&gt;

&lt;p&gt;SpanCoder is proof that .NET 10, combined with Avalonia UI and a healthy dose of mechanical sympathy, provides an incredible sweet spot. C# allows you to build safe, highly abstracted code where you want it, and terrifyingly fast, pointer-manipulating, zero-allocation data engines where you need it.&lt;/p&gt;

&lt;p&gt;SpanCoder is currently under heavy development.&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;Check out the architecture here:&lt;/strong&gt; &lt;a href="https://github.com/ian-cowley/SpanCoder" rel="noopener noreferrer"&gt;github.com/ian-cowley/SpanCoder&lt;/a&gt;&lt;/p&gt;

</description>
      <category>csharp</category>
      <category>dotnet</category>
      <category>performance</category>
      <category>showdev</category>
    </item>
    <item>
      <title>I built a zero-allocation C# Time-Series Engine to replace Postgres (and it hits 2 Billion values/sec)</title>
      <dc:creator>Ian Cowley</dc:creator>
      <pubDate>Tue, 16 Jun 2026 13:47:14 +0000</pubDate>
      <link>https://dev.to/iancowley/i-built-a-zero-allocation-c-time-series-engine-to-replace-postgres-and-it-hits-2-billion-3lg4</link>
      <guid>https://dev.to/iancowley/i-built-a-zero-allocation-c-time-series-engine-to-replace-postgres-and-it-hits-2-billion-3lg4</guid>
      <description>&lt;p&gt;I’ve been building systems for a while now, and if there’s one trend in modern software engineering that drives me crazy, it’s the default reaction to "we have data."&lt;/p&gt;

&lt;p&gt;Need to log AI agent telemetry, financial ticks, or server metrics? The modern playbook says: spin up a massive Docker container, deploy PostgreSQL, install the TimescaleDB extension, configure connection pools, and pull in a heavy ORM.&lt;/p&gt;

&lt;p&gt;TimescaleDB is an incredible piece of engineering—it bridges the gap between fast row-based ingestion and compressed columnar analytics. But why do we have to cross a network boundary, suffer IPC overhead, and serialize data just to do it?&lt;/p&gt;

&lt;p&gt;C# and .NET 10 are absolute weapons for data analysis. We don't need to default to heavy database servers or Python for this.&lt;/p&gt;

&lt;p&gt;So, I built &lt;strong&gt;&lt;a href="https://github.com/ian-cowley/Glacier.Chrono" rel="noopener noreferrer"&gt;Glacier.Chrono&lt;/a&gt;&lt;/strong&gt;: an embedded, zero-allocation, in-process time-series database in pure C#. It mirrors the hybrid row-to-columnar architecture of TimescaleDB, implements Facebook's Gorilla compression, and hits over &lt;strong&gt;2 Billion values per second&lt;/strong&gt; with exactly zero heap allocations.&lt;/p&gt;

&lt;p&gt;Here is how I bypassed the bloat and built it to run on the metal.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Architecture: Row-to-Columnar Hybridity&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;To build a time-series engine, you have to solve two conflicting problems:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Ingestion&lt;/strong&gt; needs to be row-based (Array of Structs) so you can blast data into memory instantly.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Analytics&lt;/strong&gt; needs to be columnar (Struct of Arrays) so you can run SIMD instructions across continuous blocks of a single data type without thrashing the CPU cache.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here is how Glacier.Chrono handles it:&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;1. The Hot Ingest (Zero-Allocation)&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Data is ingested into a pre-allocated, lock-free HotRingBuffer&amp;lt;T&amp;gt;. We restrict T to unmanaged C# structs (using [StructLayout(LayoutKind.Sequential)]). Writing to the database is literally just advancing an Interlocked.Increment pointer and writing raw bytes. Multiple threads can blast telemetry at it simultaneously with zero lock contention and zero garbage collection.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;2. The Pivot&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;When a chunk hits 10,000 rows, a background thread performs a matrix transpose. It takes the row-based data and slices it into columnar Span&amp;lt;T&amp;gt; buffers.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;3. The Compression Engine&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;This is where the magic happens. We apply data-type-specific algorithms directly to the Span&amp;lt;T&amp;gt; buffers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Timestamps:&lt;/strong&gt; Delta-of-Delta (DoD). If you log every second perfectly, the DoD is 0. We pack thousands of timestamps into a handful of bits.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Floats:&lt;/strong&gt; Facebook Gorilla XOR. We XOR consecutive floats and strip the zeros, compressing slowly changing metrics like CPU usage by 90%+.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State / Enums:&lt;/strong&gt; Run-Length Encoding (RLE). 5,000 consecutive "Running" states become a tiny [Value: 1, Count: 5000] tuple.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The 64-Bit Accumulator Trick&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;To get these algorithms to scream, I couldn't use standard bit-by-bit while loops.&lt;/p&gt;

&lt;p&gt;Instead, Glacier.Chrono uses a &lt;strong&gt;64-bit CPU register accumulator trick&lt;/strong&gt;. We accumulate bits directly into a ulong register and only write bytes to the managed memory array when the register is full. This single mechanical sympathy optimization provided a &lt;strong&gt;19x speedup&lt;/strong&gt; over standard bit-packing logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Raw Numbers&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;I ran BenchmarkDotNet on a modern CPU with AVX-512 extensions using .NET 10. The results are frankly absurd for a managed language.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Notice the "Allocated" column.&lt;/em&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Phase&lt;/th&gt;
&lt;th&gt;Mean Execution&lt;/th&gt;
&lt;th&gt;Allocated Memory&lt;/th&gt;
&lt;th&gt;Throughput&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Hot Ingest (10k rows)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;138.83 μs&lt;/td&gt;
&lt;td&gt;0 B (Steady State)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~72.0M writes/sec&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Gorilla Float Compress&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;33.27 μs&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0 B&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;300.5M values/sec&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Delta-of-Delta Compress&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;4.95 μs&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0 B&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2.01B values/sec&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SIMD Query Engine&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;325.96 μs&lt;/td&gt;
&lt;td&gt;577 B (OS Handles)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;30.7M records/sec&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;We are compressing over &lt;strong&gt;2 Billion timestamps per second&lt;/strong&gt; without touching the Garbage Collector once.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The "No-ORM" Query Engine (Powered by Source Generators)&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;In a traditional database, you'd send a dynamic query string like SELECT AVG(CpuTemp) FROM metrics WHERE ServerId = 1. But parsing a SQL string, building an Abstract Syntax Tree, and dynamically matching columns at runtime requires memory allocations and branching logic—which completely violates our zero-allocation philosophy.&lt;/p&gt;

&lt;p&gt;So instead of a dynamic query engine, Glacier.Chrono uses &lt;strong&gt;C# Source Generators&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;You define your custom telemetry schema, mark it with [ChronoTable], and specify how you want each field compressed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ChronoTable&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;StructLayout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LayoutKind&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sequential&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Pack&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;  
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;SystemMetric&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IComparable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;SystemMetric&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;  
&lt;span class="p"&gt;{&lt;/span&gt;  
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Timestamp&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="c1"&gt;// Delta-of-Delta  &lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Metric&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="c1"&gt;// Gorilla XOR  &lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;CpuTemp&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Category&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="c1"&gt;// Run-Length Encoding  &lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;ServerId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;CompareTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SystemMetric&lt;/span&gt; &lt;span class="n"&gt;other&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CompareTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;other&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At compile time, Roslyn analyzes your struct and automatically emits a dedicated, zero-allocation compactor and a custom SIMD query engine directly into your project.&lt;/p&gt;

&lt;p&gt;When you want to query the data, you use the strongly-typed, auto-generated methods. Glacier.Chrono maps &lt;em&gt;only&lt;/em&gt; the specific columns you need directly into the OS virtual address space via MemoryMappedFile projection, ignoring the timestamp and unrelated columns completely.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The Source Generator automatically created 'GetAverageCpuTempForServerId'  &lt;/span&gt;
&lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;avgCpuTemp&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SystemMetricQueryEngine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetAverageCpuTempForServerId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;  
    &lt;span class="n"&gt;chunkFilePath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"./data/chunk_0.glacier"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   
    &lt;span class="n"&gt;targetServerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   
    &lt;span class="n"&gt;queryBuffers&lt;/span&gt;  
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You get a beautiful, strongly-typed developer experience with autocomplete, but under the hood, the compiler has emitted the exact, bare-metal AVX-512 vector loops for those specific columns.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Takeaway&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The data analysis and AI telemetry space has been entirely dominated by heavy database servers, Python wrappers, and massive C++ frameworks.&lt;/p&gt;

&lt;p&gt;But .NET 10, combined with C# Source Generators, unmanaged memory, and hardware intrinsics, proves that C# is an absolute heavyweight contender. You don't need a heavy Docker-bound stack to get world-class, column-compressed time-series analytics. You just need good, native, mechanically sympathetic engineering.&lt;/p&gt;

&lt;p&gt;Glacier.Chrono is open-source and part of the Glacier high-performance storage suite.&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;Check out the repo here:&lt;/strong&gt; &lt;a href="https://github.com/ian-cowley/Glacier.Chrono" rel="noopener noreferrer"&gt;github.com/ian-cowley/Glacier.Chrono&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Starving the Garbage Collector: A Pragmatic Guide to Zero-Allocation C#</title>
      <dc:creator>Ian Cowley</dc:creator>
      <pubDate>Sun, 14 Jun 2026 10:06:29 +0000</pubDate>
      <link>https://dev.to/iancowley/starving-the-garbage-collector-a-pragmatic-guide-to-zero-allocation-c-oj5</link>
      <guid>https://dev.to/iancowley/starving-the-garbage-collector-a-pragmatic-guide-to-zero-allocation-c-oj5</guid>
      <description>&lt;p&gt;Over the last few weeks, I’ve open-sourced a suite of high-performance, zero-dependency C# engines. This includes a native DataFrame library (&lt;em&gt;&lt;a href="https://github.com/ian-cowley/Glacier.Polaris" rel="noopener noreferrer"&gt;Glacier.Polaris&lt;/a&gt;), a blistering fast text searcher (&lt;/em&gt;&lt;a href="https://github.com/ian-cowley/Glacier.Grep" rel="noopener noreferrer"&gt;Glacier.Grep&lt;/a&gt;), and a semantic Markdown parser for RAG contexts (&lt;a href="https://github.com/ian-cowley/Glacier.DocTree" rel="noopener noreferrer"&gt;Glacier.DocTree&lt;/a&gt;). You can find the source code for all of these on my &lt;a href="https://github.com/ian-cowley?tab=repositories" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;A recurring question I’m getting from other devs looking at these repositories is simple: &lt;em&gt;How exactly are you bypassing the Garbage Collector to get these speeds?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I’ve never hidden my distaste for heavy, magic-filled frameworks. Whether it's an unwieldy data access library or a bloated client-side framework, they all share a common flaw: they wrap your code in layers of hidden allocations that murder your CPU caches and force the .NET Garbage Collector (GC) into overdrive.&lt;/p&gt;

&lt;p&gt;When you want to build systems that process millions of rows a second or rival native C/C++ in raw compute speed, you have to take control of your memory. To give you a fighting chance at writing your own high-performance engines, let's break down how memory allocation actually works in C#, using the architecture of the &lt;code&gt;Glacier&lt;/code&gt; repositories as our guide.&lt;/p&gt;

&lt;h2&gt;
  
  
  Level 1: The Heap, the Stack, and Cache Locality
&lt;/h2&gt;

&lt;p&gt;In C#, every time you use the &lt;code&gt;new&lt;/code&gt; keyword on a &lt;code&gt;class&lt;/code&gt; (a reference type), you are asking the runtime to find a contiguous block of free memory on the Managed Heap.&lt;/p&gt;

&lt;p&gt;The heap is a messy place. When it gets full, the GC steps in. It pauses your application threads, traverses the object graph to see what you are still using, compacts the memory, and cleans up the garbage. For standard CRUD apps, this pause is negligible. For a DataFrame engine like &lt;code&gt;Glacier.Polaris&lt;/code&gt; processing millions of rows, a GC pause is a catastrophic event. It's a heavy tax on your CPU cycles.&lt;/p&gt;

&lt;p&gt;The alternative is the Stack. The stack is a tightly managed, incredibly fast area of memory assigned exclusively to the current thread. When you create a &lt;code&gt;struct&lt;/code&gt; (a value type), it goes on the stack. When the method finishes, the stack unwinds, and the memory is instantly freed. No GC involved. Zero tax.&lt;/p&gt;

&lt;p&gt;But dropping classes for structs isn't just about dodging the GC; it's about mechanical sympathy. Modern CPUs don't read bytes from RAM one at a time; they pull 64-byte "cache lines." By using &lt;code&gt;struct&lt;/code&gt; and explicitly packing your data via &lt;code&gt;[StructLayout(LayoutKind.Sequential)]&lt;/code&gt;, you ensure that when the CPU grabs a cache line, it receives highly relevant, tightly packed data, drastically reducing cache misses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Golden Rule:&lt;/strong&gt; If you want to go fast in a tight loop, favor &lt;code&gt;struct&lt;/code&gt; over &lt;code&gt;class&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Level 2: Slicing Without Allocating &amp;amp; The Async Trap
&lt;/h2&gt;

&lt;p&gt;Value types are great, but what about arrays and strings? Historically, if you wanted a subset of an array or a string, you called &lt;code&gt;.Substring()&lt;/code&gt; or &lt;code&gt;.Skip().Take()&lt;/code&gt;. These operations allocate new objects on the heap, copying the data over.&lt;/p&gt;

&lt;p&gt;If you look at the source for &lt;code&gt;Glacier.DocTree&lt;/code&gt; or &lt;code&gt;Glacier.Grep&lt;/code&gt;, you'll notice we rarely allocate new strings when reading text. Instead, we use &lt;code&gt;Span&amp;lt;T&amp;gt;&lt;/code&gt; and &lt;code&gt;ReadOnlySpan&amp;lt;T&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;Span&amp;lt;T&amp;gt;&lt;/code&gt; is a &lt;code&gt;ref struct&lt;/code&gt;. It is essentially a pointer to a block of memory and a length, meaning it &lt;em&gt;must&lt;/em&gt; live on the stack. You can slice a massive buffer into smaller chunks, and it costs absolutely nothing. Zero allocations. Zero copying.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The old, bloated way that triggers the GC&lt;/span&gt;
&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Error: Connection Timeout"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Substring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;7&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Allocates a new string on the heap&lt;/span&gt;

&lt;span class="c1"&gt;// The Glacier way (Zero-Allocation)&lt;/span&gt;
&lt;span class="n"&gt;ReadOnlySpan&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;char&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;lineSpan&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Error: Connection Timeout"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsSpan&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;ReadOnlySpan&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;char&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;messageSpan&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lineSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;7&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Just a view into memory!&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Async Trap
&lt;/h3&gt;

&lt;p&gt;Because &lt;code&gt;Span&amp;lt;T&amp;gt;&lt;/code&gt; is tied to the stack, the compiler will stop you if you try to use it across an &lt;code&gt;await&lt;/code&gt; boundary (like asynchronously reading a file stream). State machines generated by &lt;code&gt;async/await&lt;/code&gt; cannot preserve stack-only references.&lt;/p&gt;

&lt;p&gt;To bridge this gap, we use &lt;code&gt;Memory&amp;lt;T&amp;gt;&lt;/code&gt;. &lt;code&gt;Memory&amp;lt;T&amp;gt;&lt;/code&gt; can safely live on the heap and travel through async pipelines. Once the I/O operation yields and you are ready to do synchronous, CPU-bound processing, you simply call &lt;code&gt;.Span&lt;/code&gt; on your &lt;code&gt;Memory&amp;lt;T&amp;gt;&lt;/code&gt; and begin slicing at zero cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  Level 3: Custom Allocators and the Rental Market
&lt;/h2&gt;

&lt;p&gt;The stack is fast, but it's small (typically around 1MB). If you try to put a massive DataFrame column there, you will crash your app with a &lt;code&gt;StackOverflowException&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;Glacier.Polaris&lt;/code&gt;, we are mapping primitive types directly to dense arrays to avoid the overhead of boxing. But allocating massive arrays in a tight loop with &lt;code&gt;new int[100000]&lt;/code&gt; will trigger a Gen 0 GC collection almost instantly.&lt;/p&gt;

&lt;p&gt;Instead of relying on standard arrays, Polaris uses custom allocators and structures like &lt;code&gt;MemoryOwnerColumn&lt;/code&gt; and &lt;code&gt;ValidityMask&lt;/code&gt;. This allows us to maintain C-like memory control while remaining safe within the .NET ecosystem. When we need temporary buffers, we rent them from &lt;code&gt;System.Buffers.ArrayPool&amp;lt;T&amp;gt;.Shared&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Rent an array of AT LEAST the requested size&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ArrayPool&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;.&lt;/span&gt;&lt;span class="n"&gt;Shared&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Rent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;100000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt; 
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Wrap it in a span for safe, fast access&lt;/span&gt;
    &lt;span class="n"&gt;Span&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;workSpan&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsSpan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;100000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// Do heavy data processing...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;finally&lt;/span&gt; 
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Always return it! The GC never sees a new allocation.&lt;/span&gt;
    &lt;span class="n"&gt;ArrayPool&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;.&lt;/span&gt;&lt;span class="n"&gt;Shared&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Return&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Level 4: Unleashing Compute (SIMD &amp;amp; MemoryMarshal)
&lt;/h2&gt;

&lt;p&gt;Once your memory is flat, contiguous, and not bothering the Garbage Collector, you can unleash the CPU.&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;Glacier.Polaris&lt;/code&gt;, the math isn't done row-by-row in a simple &lt;code&gt;for&lt;/code&gt; loop. We process data in chunks using SIMD (Single Instruction, Multiple Data) CPU vector instructions.&lt;/p&gt;

&lt;p&gt;In older .NET versions, this meant hardcoding explicit intrinsics and pinning arrays with &lt;code&gt;fixed&lt;/code&gt;, which added slight overhead. Modern .NET abstracts this beautifully. We use &lt;code&gt;MemoryMarshal.GetReference&lt;/code&gt; to grab a lightweight ref to our data without pinning it, and feed it into cross-platform &lt;code&gt;Vector256&lt;/code&gt; logic that works efficiently on both x64 and ARM64 processors.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Runtime.CompilerServices&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Runtime.InteropServices&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Runtime.Intrinsics&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;SimdSum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ReadOnlySpan&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;sum&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Grab a fast, unpinned reference to the underlying data&lt;/span&gt;
    &lt;span class="k"&gt;ref&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;ref&lt;/span&gt; &lt;span class="n"&gt;MemoryMarshal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetReference&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Process 8 integers at a time (if hardware supports 256-bit vectors)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Vector256&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsHardwareAccelerated&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;Vector256&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;.&lt;/span&gt;&lt;span class="n"&gt;Count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Vector256&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;vSum&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Vector256&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;.&lt;/span&gt;&lt;span class="n"&gt;Zero&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;Vector256&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;.&lt;/span&gt;&lt;span class="n"&gt;Count&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;Vector256&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;.&lt;/span&gt;&lt;span class="n"&gt;Count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// Load 8 contiguous integers directly into the CPU register&lt;/span&gt;
            &lt;span class="n"&gt;Vector256&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;vData&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Vector256&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LoadUnsafe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ref&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;nuint&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="c1"&gt;// Add them in parallel&lt;/span&gt;
            &lt;span class="n"&gt;vSum&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;vData&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; 
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// Horizontal add to collapse the vector lanes into a final scalar sum&lt;/span&gt;
        &lt;span class="n"&gt;sum&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;Vector256&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vSum&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; 
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Process any remaining elements normally&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;++)&lt;/span&gt; 
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;sum&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;Unsafe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ref&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is where the magic happens. We've bypassed the GC to keep our memory clean, extracted an unpinned reference, and fed it directly into the CPU's vector lanes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Level 5: Show the Receipts
&lt;/h2&gt;

&lt;p&gt;It’s easy to talk about zero-allocation theory, but advanced developers deal in metrics. When you strip away the frameworks and embrace the mechanics outlined above, the results in &lt;code&gt;BenchmarkDotNet&lt;/code&gt; look like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Mean&lt;/th&gt;
&lt;th&gt;Allocated&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Standard Substring&lt;/td&gt;
&lt;td&gt;18.45 ns&lt;/td&gt;
&lt;td&gt;32 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Glacier Span Slice&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.02 ns&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0 B&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Seeing that &lt;code&gt;0 B&lt;/code&gt; under the allocated column is the entire point.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bottom Line
&lt;/h2&gt;

&lt;p&gt;Getting away from the Garbage Collector isn't about rewriting every line of business logic you have. It's about surgical precision. Identify the hot paths—the tight loops where data flows by the gigabyte—and strip away the abstractions.&lt;/p&gt;

&lt;p&gt;Drop the heavy frameworks. Stop calling &lt;code&gt;new&lt;/code&gt; in a loop. Embrace &lt;code&gt;struct&lt;/code&gt;, slice with &lt;code&gt;Span&amp;lt;T&amp;gt;&lt;/code&gt;, use the &lt;code&gt;ArrayPool&lt;/code&gt;, and build custom column allocators when the scale demands it. Take a look through the &lt;em&gt;&lt;a href="https://github.com/ian-cowley?tab=repositories" rel="noopener noreferrer"&gt;Glacier&lt;/a&gt;&lt;/em&gt; repositories to see these patterns in action. This is how you build engines that don't just participate in the .NET ecosystem, but actually push it to its absolute limits.&lt;/p&gt;

</description>
      <category>csharp</category>
      <category>dotnet</category>
      <category>opensource</category>
      <category>performance</category>
    </item>
    <item>
      <title>I built a native C# Grep engine that's holding it's own with ripgrep (with zero allocations)</title>
      <dc:creator>Ian Cowley</dc:creator>
      <pubDate>Tue, 09 Jun 2026 15:26:28 +0000</pubDate>
      <link>https://dev.to/iancowley/i-built-a-native-c-grep-engine-thats-holding-its-own-with-ripgrep-with-zero-allocations-2nf7</link>
      <guid>https://dev.to/iancowley/i-built-a-native-c-grep-engine-thats-holding-its-own-with-ripgrep-with-zero-allocations-2nf7</guid>
      <description>&lt;p&gt;Let’s be honest: the golden rule of modern software engineering is &lt;em&gt;"never rewrite grep."&lt;/em&gt; Tools like &lt;code&gt;ripgrep&lt;/code&gt; are written in native Rust, compiled straight to the metal, and aggressively optimized. For 99% of use cases, wrapping a CLI tool or pulling in a massive external dependency is what people do.&lt;/p&gt;

&lt;p&gt;But when you are building an ultra-low-latency AI context layer where sub-20ms execution is the hard ceiling, spinning up external CLI processes, handling inter-process communication, and parsing standard output strings back onto the managed heap destroys performance. The Garbage Collector pressure alone kills your agentic execution loop.&lt;/p&gt;

&lt;p&gt;So, I decided to see how close I could get with pure, unadulterated &lt;strong&gt;.NET 10&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The result? &lt;strong&gt;&lt;a href="https://github.com/ian-cowley/Glacier.Grep" rel="noopener noreferrer"&gt;Glacier.Grep&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;On a typical developer workload scanning a 257 MB workspace (590 files), it doesn't just rival &lt;code&gt;ripgrep&lt;/code&gt;—&lt;strong&gt;it actually beats it on case-sensitive paths.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Raw Benchmarks (Warmed)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Target:&lt;/strong&gt; 590 files, 257.83 MB text data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OS:&lt;/strong&gt; Windows (x64)&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Engine&lt;/th&gt;
&lt;th&gt;Query&lt;/th&gt;
&lt;th&gt;Execution Time&lt;/th&gt;
&lt;th&gt;Performance Ratio&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Ripgrep (Rust)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;"public class"&lt;/code&gt; (Sensitive)&lt;/td&gt;
&lt;td&gt;134.9 ms&lt;/td&gt;
&lt;td&gt;1.12x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Glacier.Grep (.NET 10)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;"public class"&lt;/code&gt; (Sensitive)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;120.4 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.00x (FASTER)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Ripgrep (Rust)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;"public class"&lt;/code&gt; (Insensitive)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;210.4 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1.52x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Ripgrep (Rust)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;"ThreadIndependentReaderWriterLock"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;142.3 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1.23x&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Going toe-to-toe with optimized Rust in a managed language requires treating memory like hot lava. Here is exactly how it's engineered under the hood.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Zero-Allocation Stack Traversal
&lt;/h2&gt;

&lt;p&gt;The first place search engines lose time is evaluating filesystem metadata and parsing &lt;code&gt;.gitignore&lt;/code&gt; rules. If you instantiate &lt;code&gt;DirectoryInfo&lt;/code&gt; or materialize file paths as managed strings just to skip a hidden directory, you've already lost.&lt;/p&gt;

&lt;p&gt;Glacier.Grep uses &lt;code&gt;System.IO.Enumeration.FileSystemEnumerable&amp;lt;T&amp;gt;&lt;/code&gt;. We intercept the OS file handles and evaluate exclusion criteria entirely on the stack using custom &lt;code&gt;ref struct&lt;/code&gt; rules before a single path string is allocated.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;.gitignore&lt;/code&gt; hierarchy is compiled at startup into a lightweight prefix-tree (Trie). Path matching becomes an instant $O(L)$ operation, pruning folders like &lt;code&gt;bin/&lt;/code&gt;, &lt;code&gt;obj/&lt;/code&gt;, and &lt;code&gt;node_modules&lt;/code&gt; before they ever touch the processing queue.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. The Hybrid I/O Dispatcher
&lt;/h2&gt;

&lt;p&gt;There is no "one-size-fits-all" for disk I/O. Memory-mapped files are amazing for massive datasets, but forcing the OS to map page tables for thousands of tiny 4KB source files introduces major kernel overhead.&lt;/p&gt;

&lt;p&gt;We use a dynamic dispatcher that checks the file length directly from the stack-allocated filesystem entry:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Small files (&amp;lt; 1MB):&lt;/strong&gt; Handled via &lt;code&gt;RandomAccess.Read&lt;/code&gt; straight into a chunk of memory rented from &lt;code&gt;ArrayPool&amp;lt;byte&amp;gt;.Shared&lt;/code&gt;. This keeps data purely in the buffer pool and avoids virtual memory mapping overhead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Large files (&amp;gt; 1MB):&lt;/strong&gt; Handled via &lt;code&gt;MemoryMappedFile.CreateFromFile&lt;/code&gt;. We grab an unsafe &lt;code&gt;byte*&lt;/code&gt; pointer directly to the OS page cache, wrap it in a &lt;code&gt;ReadOnlySpan&amp;lt;byte&amp;gt;&lt;/code&gt;, and feed it to the execution engine.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  3. Hardware Acceleration via .NET 10 &lt;code&gt;SearchValues&amp;lt;byte&amp;gt;&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;We don't convert bytes to characters, and we never split text into an array of strings. The entire search happens on raw UTF-8 bytes.&lt;/p&gt;

&lt;p&gt;To find the needle in the haystack, we lean heavily on .NET 10's upgraded &lt;code&gt;SearchValues&amp;lt;byte&amp;gt;&lt;/code&gt;. The JIT compiler automatically emits vectorized instructions—utilizing &lt;strong&gt;AVX-512&lt;/strong&gt; or &lt;strong&gt;AVX2&lt;/strong&gt; depending on the hardware—to scan 32 or 64 bytes of text in a &lt;em&gt;single CPU clock cycle&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;When a match byte sequence is triggered, the engine avoids line-splitting allocations by scanning backwards and forwards to the nearest &lt;code&gt;\n&lt;/code&gt; byte boundaries, producing a &lt;code&gt;ReadOnlySpan&amp;lt;byte&amp;gt;&lt;/code&gt; slice of the line instantly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The .NET 10 hot loop&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;offset&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;fileData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Hardware-accelerated SIMD scan&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;matchIndex&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fileData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;IndexOfAny&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_searchValues&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;matchIndex&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;offset&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;matchIndex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Zero-allocation line slicing via byte boundaries&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;lineStart&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fileData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;LastIndexOf&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="sc"&gt;'\n'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;lineEnd&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fileData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;IndexOf&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="sc"&gt;'\n'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Process match slice...&lt;/span&gt;
    &lt;span class="n"&gt;offset&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;_searchValues&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  4. Built for Agentic Loops (MCP Native)
&lt;/h2&gt;

&lt;p&gt;This isn’t just a CLI utility. Glacier.Grep is built specifically to serve as a Model Context Protocol (MCP) server for AI coding agents.&lt;/p&gt;

&lt;p&gt;When an LLM agent needs to find code patterns, it can invoke the tool directly over standard I/O JSON-RPC. Because the engine runs continuously as a persistent process, there's zero process-spawning penalty. Matches are streamed immediately to the agent's context window via a zero-allocation &lt;code&gt;System.IO.Pipelines&lt;/code&gt; stream using &lt;code&gt;Utf8JsonWriter&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Managed languages aren't slow—heavy, framework-obsessed architectures are. When you drop the abstractions, bypass the heap, and write code with mechanical sympathy for the underlying CPU registers, .NET 10 is an absolute speed demon.&lt;/p&gt;

&lt;p&gt;Glacier.Grep is open-source and part of the Glacier high-performance storage suite.&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;Check out the repo here:&lt;/strong&gt; &lt;a href="https://github.com/ian-cowley/Glacier.Grep" rel="noopener noreferrer"&gt;github.com/ian-cowley/Glacier.Grep&lt;/a&gt;&lt;/p&gt;

</description>
      <category>csharp</category>
      <category>dotnet</category>
      <category>performance</category>
      <category>showdev</category>
    </item>
    <item>
      <title>I built a C# Knowledge Layer to solve the AI Agent Memory Crisis</title>
      <dc:creator>Ian Cowley</dc:creator>
      <pubDate>Mon, 18 May 2026 17:34:40 +0000</pubDate>
      <link>https://dev.to/iancowley/i-built-a-c-knowledge-layer-to-solve-the-ai-agent-memory-crisis-34mj</link>
      <guid>https://dev.to/iancowley/i-built-a-c-knowledge-layer-to-solve-the-ai-agent-memory-crisis-34mj</guid>
      <description>&lt;p&gt;If you are building AI Agents today, you’ve probably noticed a glaring problem: &lt;strong&gt;The Memory Wall&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;When you give an agent a task (like resolving a complex customer support escalation), it doesn't just need to read a single document. It needs to look at CRM data, trace fraud relationships, read strict legal policies, and review historical tickets.&lt;/p&gt;

&lt;p&gt;The industry's current solution is to give the Agent a bunch of separate tools (APIs) and let it figure it out. The result? The Agent burns up 80% of your token budget and 5 seconds of latency just looping through databases, trying to assemble the context itself. Often, it gets confused and hallucinates.&lt;/p&gt;

&lt;p&gt;As a software engineer of 40 years, I prefer deterministic logic and bare-metal performance. Agents shouldn't assemble data; our backend architecture should assemble it for them.&lt;/p&gt;

&lt;p&gt;Over the past few weeks, I’ve built a suite of zero-dependency, hyper-optimized C# storage engines to handle the four distinct "shapes" of enterprise data. Today, I am releasing the final piece: &lt;a href="https://github.com/ian-cowley/Glacier.Bundle" rel="noopener noreferrer"&gt;&lt;strong&gt;Glacier.Bundle&lt;/strong&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It is a unified semantic orchestration engine that compiles relational, hierarchical, tabular, and vector data into a single, high-density prompt bundle in &lt;strong&gt;under 20 milliseconds&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Four Pillars of AI Memory
&lt;/h3&gt;

&lt;p&gt;Before building the orchestrator, I had to build the engines. If you missed the previous articles, &lt;code&gt;Glacier.Bundle&lt;/code&gt; sits on top of these four native C# libraries:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;📊 &lt;strong&gt;The Tabular Layer:&lt;/strong&gt; &lt;a href="https://dev.to/iancowley/i-built-a-native-c-dataframe-engine-to-rival-python-polars-its-actually-faster-on-some-things-opg"&gt;I built a native C# DataFrame engine to rival Python Polars&lt;/a&gt; (&lt;code&gt;Glacier.Polaris&lt;/code&gt; / &lt;code&gt;PolarsPlus&lt;/code&gt;) for structured CRM and metric data.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;🧠 &lt;strong&gt;The Semantic Layer:&lt;/strong&gt; &lt;a href="https://dev.to/iancowley/i-built-a-zero-dependency-c-vector-database-that-saturates-ddr5-ram-bandwidth-5f9a"&gt;I built a zero-dependency C# Vector Database that saturates DDR5 RAM&lt;/a&gt; (&lt;code&gt;Glacier.Vector&lt;/code&gt;) for fuzzy, historical similarity matching using AVX-512 SIMD.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;🌐 &lt;strong&gt;The Relational Layer:&lt;/strong&gt; &lt;a href="https://dev.to/iancowley/i-built-a-zero-allocation-c-knowledge-graph-because-jvm-graphs-are-too-bloated-4pej"&gt;I built a zero-allocation C# Knowledge Graph&lt;/a&gt; (&lt;code&gt;Glacier.Graph&lt;/code&gt;) to instantly map fraud rings and entity neighborhoods.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;📂 &lt;strong&gt;The Hierarchical Layer:&lt;/strong&gt; &lt;a href="https://dev.to/iancowley/why-naive-rag-is-dead-i-built-a-zero-dependency-c-semantic-tree-parser-2ph8"&gt;I built a Semantic Tree Parser because Naive RAG is dead&lt;/a&gt; (&lt;code&gt;Glacier.DocTree&lt;/code&gt;) to extract deterministic document policies without chunking them into oblivion.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The Problem: Bridging the Islands
&lt;/h3&gt;

&lt;p&gt;Having four incredibly fast databases is useless if your AI Agent has to make four separate HTTP REST calls to query them.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Glacier.Bundle&lt;/code&gt; acts as the single central broker. It runs in-memory alongside your Agent. You register your tabular data, your vector indices, your graph store, and your parsed markdown documents into a thread-safe &lt;code&gt;BundleContext&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Then, instead of asking the AI to &lt;em&gt;"go find the data,"&lt;/em&gt; you use the &lt;code&gt;BundleBuilder&lt;/code&gt; to programmatically compile the absolute truth.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Code: Compiling Context in C
&lt;/h3&gt;

&lt;p&gt;Here is how you compile a massive, multi-dimensional prompt for a customer escalation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;queryVector&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;GetEmbedding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Customer requested API rate expansion."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// The fluent, zero-allocation context compiler&lt;/span&gt;
&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;contextPrompt&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;BundleBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bundleCtx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BeginBundle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Customer Escalation Resolution"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;// 1. Instantly pull structured CRM data (Polaris)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AppendTabularRow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Client CRM Profile"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sb&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;sb&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AppendLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"  ID: User_9021"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AppendLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"  Name: DeltaCorp Ltd"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AppendLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"  Tier: Enterprise"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="c1"&gt;// 2. Traverse the Knowledge Graph for suspicious IP links (Graph)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AppendGraphTopology&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Graph_Topology"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"User_9021"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;maxHops&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Relational Fraud Mapping"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;// 3. Extract the exact SLA section without chunking errors (DocTree)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AppendDocumentTreeSection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"DocTree_SLA"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Enterprise SLAs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Guaranteed SLA Policies"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;// 4. Do a SIMD-accelerated math search for past tickets (Vector)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AppendVectorContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Vector_Tickets"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;queryVector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;topK&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Semantic Ticket History"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Output: 17 Milliseconds
&lt;/h3&gt;

&lt;p&gt;When we run this code, the C# runtime queries all four engines—extracting CRM details, tracing 2-hop relational fraud paths, pulling strict SLA Markdown structures, and executing a brute-force vector search.&lt;/p&gt;

&lt;p&gt;Here is the benchmark output from the console:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[5] Executing high-speed Bundle compilation...

Bundle compiled in 17.4316 ms!

======================================================================
 SYSTEM RESOLVED CONTEXT BUNDLE: CUSTOMER ESCALATION: DELTACORP LTD
 GENERATED AT: 2026-05-18 17:14:55 UTC
======================================================================

--- [DATA LAYER] CLIENT CRM PROFILE ---
  ID: User_9021
  Name: DeltaCorp Ltd
  Tier: Enterprise
  Value: £85,200.00
  Status: Active

--- [RELATIONAL LAYER] RELATIONAL FRAUD MAPPING (2 HOPS FROM User_9021) ---
Target Entity: User_9021
Connected Network Entities (3):
  SubAccount_B, SubAccount_A, Suspicious_IP_88

--- [HIERARCHICAL LAYER] GUARANTEED SLA POLICIES ---
Document Path: Document Root &amp;gt; Service Level Agreements &amp;gt; Enterprise SLAs
Content Frame:
Enterprise SLAs
Enterprise accounts enjoy a guaranteed 99.99% uptime with immediate 15-minute response times.
No hardware throttling is applied.

--- [SEMANTIC LAYER] SEMANTIC TICKET HISTORY ---
[Rank 1 | Match Score: 1.0000] Historical Ticket #4823: Customer requested custom API access rate expansion. Granted temporary bypass.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Result: Zero Hallucinations
&lt;/h3&gt;

&lt;p&gt;Look at that output block. If you hand that string to an LLM, &lt;strong&gt;it cannot hallucinate&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It doesn't have to guess what tier the customer is on. It doesn't have to guess if the cancellation policy applies to them. It doesn't have to guess if they are connected to a suspicious IP. The deterministic C# application code proved it all &lt;em&gt;before&lt;/em&gt; the LLM was even invoked.&lt;/p&gt;

&lt;p&gt;And it did it in &lt;strong&gt;17.4 milliseconds&lt;/strong&gt;—faster than a standard web API takes to negotiate a TLS handshake.&lt;/p&gt;

&lt;h3&gt;
  
  
  Try the Full Suite
&lt;/h3&gt;

&lt;p&gt;If you are a .NET developer building AI infrastructure, and you are tired of relying on bloated JVM containers and Python scripts, you can now run the entire AI data stack natively in C#.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/ian-cowley/Glacier.Bundle" rel="noopener noreferrer"&gt;ian-cowley/Glacier.Bundle&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NuGet:&lt;/strong&gt; &lt;code&gt;dotnet add package Glacier.Bundle&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This completes the &lt;strong&gt;Glacier High-Performance Storage Suite&lt;/strong&gt;. Let's prove C# is the ultimate backend language for the AI era!&lt;/p&gt;

</description>
      <category>ai</category>
      <category>dotnet</category>
      <category>architecture</category>
      <category>rag</category>
    </item>
    <item>
      <title>Why Naive RAG is Dead: I built a zero-dependency C# Semantic Tree Parser</title>
      <dc:creator>Ian Cowley</dc:creator>
      <pubDate>Mon, 18 May 2026 17:07:05 +0000</pubDate>
      <link>https://dev.to/iancowley/why-naive-rag-is-dead-i-built-a-zero-dependency-c-semantic-tree-parser-2ph8</link>
      <guid>https://dev.to/iancowley/why-naive-rag-is-dead-i-built-a-zero-dependency-c-semantic-tree-parser-2ph8</guid>
      <description>&lt;p&gt;If you have built an AI Agent or a RAG (Retrieval-Augmented Generation) pipeline in the last year, you’ve almost certainly run into the exact same problem: &lt;strong&gt;Hallucinations caused by Naive Chunking.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The standard industry advice for feeding documents to an AI is to take a 50-page API manual, blindly chop it up into 500-token chunks, vectorize them, and throw them into a database. &lt;/p&gt;

&lt;p&gt;This completely destroys the document's structure. &lt;/p&gt;

&lt;p&gt;If you have a paragraph that says, &lt;em&gt;"If initiated after 30 days, a 15% fee will be deducted"&lt;/em&gt;, and it gets chunked away from its &lt;code&gt;## Enterprise Cancellations&lt;/code&gt; header, the AI has no idea who that rule applies to. When a user asks about &lt;em&gt;Free Tier&lt;/em&gt; cancellations, the Vector DB might return that enterprise paragraph just because the words matched. Boom. Hallucination.&lt;/p&gt;

&lt;p&gt;As a software engineer who hates bloat and relies on deterministic logic, I needed a better way. I didn't want to use massive Python libraries to solve this.&lt;/p&gt;

&lt;p&gt;So, I built &lt;strong&gt;&lt;a href="https://github.com/ian-cowley/Glacier.DocTree" rel="noopener noreferrer"&gt;Glacier.DocTree&lt;/a&gt;&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;It is a zero-dependency, bare-metal C# library that parses documents into a &lt;strong&gt;Semantic Tree&lt;/strong&gt; instead of flattening them into dumb chunks. &lt;/p&gt;




&lt;h3&gt;
  
  
  The Fix: Hierarchical Parsing
&lt;/h3&gt;

&lt;p&gt;Instead of chopping text blindly by character count, &lt;code&gt;Glacier.DocTree&lt;/code&gt; reads Markdown and builds a strongly-typed parent-child object graph. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;#&lt;/code&gt; (H1) becomes a root node.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;##&lt;/code&gt; (H2) becomes a child of the last active H1.&lt;/li&gt;
&lt;li&gt;Standard text and code blocks become children of their most recent header.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When you feed it a messy API document, it instantly compiles this beautiful, queryable hierarchy in memory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;==========================================
 Glacier.DocTree | Semantic Parser Engine
==========================================

[1] Parsing Markdown into Semantic Tree...

[2] Visualizing the Document Structure:
└─ [Root] Document Root
  └─ [Header1] Glacier Enterprise API
    └─ [Paragraph] Welcome to the Glacier API. This docu...
    └─ [Header2] Authentication
      └─ [Paragraph] All requests to the API must be crypt...
      └─ [Header3] OAuth 2.0
        └─ [Paragraph] To authenticate via OAuth2, you must ...
        └─ [CodeBlock] ```

json {    "Authorization": "Bearer...
    └─ [Header2] Usage Policies
      └─ [Paragraph] Please adhere to the following usage ...
      └─ [Header3] Rate Limits
        └─ [Paragraph] Free tier users are limited to 100 re...
      └─ [Header3] Acceptable Use
        └─ [Paragraph] Do not use the API to train competing...

[3] Simulating Agent Query: 'Extract Rate Limits context'

--- SEMANTIC CONTEXT ---
LOCATION: Document Root &amp;gt; Glacier Enterprise API &amp;gt; Usage Policies &amp;gt; Rate Limits
--- BEGIN TEXT ---
Rate Limits
Free tier users are limited to 100 requests per minute.
Enterprise users have unlimited access.
If you exceed the limit, you will receive an HTTP 429 status code.
--- END TEXT ---


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look at that &lt;code&gt;LOCATION&lt;/code&gt; string. &lt;br&gt;
If an LLM reads that, it &lt;strong&gt;cannot&lt;/strong&gt; hallucinate. It knows exactly what document it's looking at, what section it is in, and who the policy applies to. The structure &lt;em&gt;is&lt;/em&gt; the meaning.&lt;/p&gt;

&lt;h3&gt;
  
  
  Try it out
&lt;/h3&gt;

&lt;p&gt;If you are a .NET developer building AI infrastructure, and you are tired of your Vector DB returning paragraphs completely devoid of context, you need a Semantic Layer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/ian-cowley/Glacier.DocTree" rel="noopener noreferrer"&gt;ian-cowley/Glacier.DocTree&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It is purely native C#. No heavy frameworks, no Python interop, no API keys required. It just parses documents at blistering speeds and gives your agents the context they actually need. &lt;/p&gt;

&lt;p&gt;Let's prove C# belongs in the modern AI ecosystem!&lt;/p&gt;

</description>
      <category>ai</category>
      <category>architecture</category>
      <category>rag</category>
      <category>dotnet</category>
    </item>
    <item>
      <title>I built a Zero-Allocation C# Knowledge Graph (because JVM graphs are too bloated)</title>
      <dc:creator>Ian Cowley</dc:creator>
      <pubDate>Mon, 18 May 2026 11:53:55 +0000</pubDate>
      <link>https://dev.to/iancowley/i-built-a-zero-allocation-c-knowledge-graph-because-jvm-graphs-are-too-bloated-4pej</link>
      <guid>https://dev.to/iancowley/i-built-a-zero-allocation-c-knowledge-graph-because-jvm-graphs-are-too-bloated-4pej</guid>
      <description>&lt;p&gt;If you are building AI agents, you eventually hit the "Memory Wall".&lt;/p&gt;

&lt;p&gt;Your agent doesn't just need semantic text chunks (Vector Search) or structured tables (SQL). It often needs to trace &lt;strong&gt;relationships&lt;/strong&gt;. For example: &lt;em&gt;Find all suppliers connected to this failing part,&lt;/em&gt; or &lt;em&gt;Find the common connection between User_A and User_B.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;To solve this, the industry tells you to spin up a massive Java-based Graph Database container.&lt;/p&gt;

&lt;p&gt;I've been writing C# and SQL for decades, and I despise unnecessary bloat. I didn't want to run a 2GB JVM container locally just to traverse a few hundred thousand relationships.&lt;/p&gt;

&lt;p&gt;So, I built &lt;a href="https://github.com/ian-cowley/Glacier.Graph" rel="noopener noreferrer"&gt;&lt;strong&gt;Glacier.Graph&lt;/strong&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It is a zero-dependency, bare-metal C# Knowledge Graph. It completely bypasses the .NET Garbage Collector and can persist 230,000 edges to disk in 30 milliseconds.&lt;/p&gt;

&lt;p&gt;Here is how I architected the engine to achieve memory-bandwidth speeds.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Problem with "Object-Oriented" Graphs
&lt;/h3&gt;

&lt;p&gt;When most developers try to build a graph in memory, they immediately reach for Object-Oriented Programming:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Node&lt;/span&gt; 
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Edge&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Edges&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Edge&lt;/span&gt; 
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Node&lt;/span&gt; &lt;span class="n"&gt;Target&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;RelationType&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you have 100,000 nodes and 500,000 relationships, the .NET Garbage Collector now has to track &lt;strong&gt;600,000 object headers&lt;/strong&gt; scattered randomly across the heap. Traversing this graph means chasing pointer references through RAM, missing the CPU cache every single time, and dealing with brutal GC pauses.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Fix: The "Forward Star" Representation
&lt;/h3&gt;

&lt;p&gt;Instead of objects, &lt;code&gt;Glacier.Graph&lt;/code&gt; uses the &lt;strong&gt;Forward Star&lt;/strong&gt; (or Compressed Sparse Row) technique.&lt;/p&gt;

&lt;p&gt;All edges are stored in flat, primitive integer arrays. When you add a relationship, the engine doesn't allocate an object; it just increments an index and writes to &lt;code&gt;int[]&lt;/code&gt; arrays.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The core of the Graph Store&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;_head&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;// Starting edge index for a node&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;_to&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;        &lt;span class="c1"&gt;// Target node ID&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;_relation&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// Type of relation (e.g. "KNOWS")&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;_next&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;// Index of the next edge for this node&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you traverse the graph, you aren't doing heap allocations. You are just doing sequential array index lookups. The CPU cache prefetcher loves this, resulting in near-instantaneous traversal speeds.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Benchmark: Traversing 150,000 Nodes
&lt;/h3&gt;

&lt;p&gt;I generated a complex synthetic graph with &lt;strong&gt;150,000 nodes&lt;/strong&gt; and &lt;strong&gt;233,000 edges&lt;/strong&gt; (including chains, branches, and back-references).&lt;/p&gt;

&lt;p&gt;I then ran a Breadth-First Search (BFS) to find the shortest path between &lt;code&gt;User_1&lt;/code&gt; and &lt;code&gt;User_99999&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here is the raw console output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;==========================================
 Glacier.Graph | High-Performance Graph DB
==========================================

[1] Initializing Graph Engine and generating data...
    Total Nodes: 150,001 | Total Edges: 233,333

[3] Executing BFS Shortest Path (User_1 -&amp;gt; User_99999)...
    Path found in 5.3066 ms!
    Hops: 25
    Route: User_1 -&amp;gt; ... (24 intermediate nodes) ... -&amp;gt; User_99999

[4] Executing 4-Hop Neighborhood Search around User_1...
    Neighborhood scanned in 0.4416 ms!
    Discovered 11 connected entities within 4 hops.

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finding a 25-hop path through 150,000 nodes took &lt;strong&gt;5.3 milliseconds&lt;/strong&gt;. Mapping an entire 4-hop neighborhood took &lt;strong&gt;0.44 milliseconds&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;You can't even complete the HTTP handshake to a standard database in the time it takes this engine to traverse the entire network.&lt;/p&gt;

&lt;h3&gt;
  
  
  Blazing Fast Persistence
&lt;/h3&gt;

&lt;p&gt;Because the graph is just a collection of primitive &lt;code&gt;int[]&lt;/code&gt; arrays, saving the graph to disk is incredibly fast. We don't use JSON or heavy serialization.&lt;/p&gt;

&lt;p&gt;Using &lt;code&gt;MemoryMarshal.AsBytes()&lt;/code&gt;, the engine grabs the raw bytes of the arrays directly out of RAM and blasts them to the SSD.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[2] Saving raw memory arrays to disk...
    Saved 27.46 MB to 'graph_database.bin' in 30 ms.

[3] Destroying graph in memory and reloading from disk...
    Graph revived from disk in 41 ms!

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;30 milliseconds to save.&lt;/strong&gt; &lt;strong&gt;41 milliseconds to revive.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Built for AI Agents
&lt;/h3&gt;

&lt;p&gt;Like my other libraries, I didn't want to build a bloated REST API.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Glacier.Graph&lt;/code&gt; includes a built-in &lt;strong&gt;Model Context Protocol (MCP)&lt;/strong&gt; server over standard I/O. You can point your AI agents (via AgentDevKit, Claude Desktop, or Cursor) directly at the &lt;code&gt;.dll&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The AI instantly gets JSON-RPC access to &lt;code&gt;add_node&lt;/code&gt;, &lt;code&gt;add_edge&lt;/code&gt;, &lt;code&gt;find_shortest_path&lt;/code&gt;, and &lt;code&gt;find_neighborhood&lt;/code&gt;. It allows LLMs to autonomously traverse a high-speed Knowledge Graph without any Python dependencies.&lt;/p&gt;

&lt;h3&gt;
  
  
  Try it out
&lt;/h3&gt;

&lt;p&gt;If you are tired of massive containerized databases and want bare-metal C# performance for your relational AI data, give it a shot.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/ian-cowley/Glacier.Graph" rel="noopener noreferrer"&gt;ian-cowley/Glacier.Graph&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let me know what your traversal times look like!&lt;/p&gt;

</description>
      <category>ai</category>
      <category>dotnet</category>
      <category>performance</category>
      <category>database</category>
    </item>
    <item>
      <title>I built a zero-dependency C# Vector Database that saturates DDR5 RAM bandwidth</title>
      <dc:creator>Ian Cowley</dc:creator>
      <pubDate>Sat, 16 May 2026 13:39:40 +0000</pubDate>
      <link>https://dev.to/iancowley/i-built-a-zero-dependency-c-vector-database-that-saturates-ddr5-ram-bandwidth-5f9a</link>
      <guid>https://dev.to/iancowley/i-built-a-zero-dependency-c-vector-database-that-saturates-ddr5-ram-bandwidth-5f9a</guid>
      <description>&lt;p&gt;If you’re building AI apps today, you eventually need a Vector Database for RAG (Retrieval-Augmented Generation) or giving your agents long-term memory. &lt;/p&gt;

&lt;p&gt;The current ecosystem’s answer to this problem usually involves one of three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pay for a cloud service.&lt;/li&gt;
&lt;li&gt;Spin up a massive Rust, Go, or Python Docker container locally (like Qdrant, Chroma, or Milvus).&lt;/li&gt;
&lt;li&gt;Use a bloated wrapper library that pulls in 50MB of dependencies just to do math.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I’ve been a software engineer for 40 years. I despise bloat. I don't use heavy data-access frameworks or massive client-side libraries. At its core, a vector database is just a massive 2D array of floats and a tight math loop. I didn't want to spin up a Docker container or rely on Python interop just to do math.&lt;/p&gt;

&lt;p&gt;So, I built &lt;strong&gt;&lt;a href="https://github.com/ian-cowley/Glacier.Vector" rel="noopener noreferrer"&gt;Glacier.Vector&lt;/a&gt;&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;It is a purely native, zero-dependency, hardware-accelerated Vector Database for .NET 10. And it is fast enough to literally hit the physical limits of my motherboard.&lt;/p&gt;

&lt;p&gt;Here is how I squeezed every drop of performance out of the .NET runtime.&lt;/p&gt;




&lt;h3&gt;
  
  
  1. The Goal: Zero Allocations
&lt;/h3&gt;

&lt;p&gt;If you have 100,000 documents, and each has a 1536-dimensional embedding (the OpenAI &lt;code&gt;text-embedding-3-small&lt;/code&gt; standard), you are looking at about 153.6 million floats (~600 MB of RAM). &lt;/p&gt;

&lt;p&gt;If you store this as a &lt;code&gt;float[][]&lt;/code&gt; (an array of arrays), the .NET Garbage Collector will create 100,000 object headers on the heap. Your cache locality is ruined, and your GC pause times will be brutal.&lt;/p&gt;

&lt;p&gt;Instead, &lt;code&gt;Glacier.Vector&lt;/code&gt; uses a zero-copy memory model. It allocates flat arrays in massive chunks, or uses Memory-Mapped files, completely bypassing GC pauses. &lt;/p&gt;

&lt;p&gt;When searching, the engine pins the memory and uses &lt;code&gt;fixed&lt;/code&gt; pointers and &lt;code&gt;ReadOnlySpan&amp;lt;float&amp;gt;&lt;/code&gt;. No bounds checking. No object allocations in the hot path.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Pushing the CPU: 4-Way SIMD Unrolling
&lt;/h3&gt;

&lt;p&gt;Searching a vector database requires comparing the user's query against every single document using Cosine Similarity (which, for normalized vectors, is just the Dot Product).&lt;/p&gt;

&lt;p&gt;In standard C#, a scalar &lt;code&gt;for&lt;/code&gt; loop over 150 million floats takes forever. &lt;/p&gt;

&lt;p&gt;To fix this, I wrote a custom compute kernel using .NET hardware intrinsics (&lt;code&gt;System.Runtime.Intrinsics&lt;/code&gt;). But just using &lt;code&gt;Vector256&lt;/code&gt; wasn't enough. I unrolled the loop 4-ways to feed the CPU's out-of-order execution pipeline with simultaneous Fused-Multiply-Add (FMA) instructions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Unrolled AVX2 fast path (processing 32 floats per cycle)&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;acc0&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Vector256&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;.&lt;/span&gt;&lt;span class="n"&gt;Zero&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;acc1&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Vector256&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;.&lt;/span&gt;&lt;span class="n"&gt;Zero&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="m"&gt;32&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="m"&gt;32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;acc0&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Fma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MultiplyAdd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Vector256&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pTarget&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;Vector256&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pDb&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;acc0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;acc1&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Fma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MultiplyAdd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Vector256&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pTarget&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="m"&gt;8&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;Vector256&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pDb&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="m"&gt;8&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;acc1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Benchmark: Hitting the Memory Wall&lt;br&gt;
I ran a benchmark pumping 100,000 vectors (1536 dimensions each) into the engine. Here is the raw console output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;==========================================
 Glacier.Vector | SIMD Performance Engine
==========================================

[1] Initializing In-Memory Storage...
    Dimensions: 1536
    Target Count: 100,000

[2] Generating and loading synthetic vectors...
    Done! Loaded 100,000 vectors in 547 ms.

[3] Preparing search query...
[4] Executing SIMD brute-force search...

==========================================
 SEARCH COMPLETED IN: 7.152 ms
 Vectors scanned:     100,000
 Operations/sec:      13,982,298
==========================================

Top 5 Results:
  Rank 1 | Score: 0.1056 | ID: 51030 | Meta: Document_Chunk_51030
  Rank 2 | Score: 0.1019 | ID: 87632 | Meta: Document_Chunk_87632
  Rank 3 | Score: 0.1003 | ID: 52591 | Meta: Document_Chunk_52591
  Rank 4 | Score: 0.0994 | ID: 96139 | Meta: Document_Chunk_96139
  Rank 5 | Score: 0.0990 | ID: 29879 | Meta: Document_Chunk_29879
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To search the entire database—scanning 153.6 million floats—it took exactly 7.15 milliseconds.&lt;/p&gt;

&lt;p&gt;At that speed, the engine is demanding roughly 85 Gigabytes per second of memory bandwidth. The dual-channel DDR5 RAM on my motherboard physically maxes out right around 85-90 GB/s.&lt;/p&gt;

&lt;p&gt;I literally cannot make this C# code any faster without buying faster RAM. We hit the physical memory wall.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Built-in AI Integration (MCP)
A vector database isn't useful if AI agents can't talk to it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Instead of building a bloated REST API or requiring gRPC, I built a native Model Context Protocol (MCP) server directly into the engine. It runs entirely over standard I/O (stdio).&lt;/p&gt;

&lt;p&gt;You can configure Claude Desktop, Cursor, or your own autonomous agents to point directly at the Glacier.Vector.Host.dll. The AI instantly understands how to call the add_vector and search_vectors tools via JSON-RPC. Zero Python, zero external API keys, zero network latency.&lt;/p&gt;

&lt;p&gt;Try it out&lt;br&gt;
If you are a .NET developer building RAG pipelines, AI agents, or just dealing with heavy data, and you hate bloated frameworks as much as I do, try it out.&lt;/p&gt;

&lt;p&gt;NuGet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package Glacier.Vector
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GitHub: &lt;a href="https://github.com/ian-cowley/Glacier.Vector" rel="noopener noreferrer"&gt;ian-cowley/Glacier.Vector&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It pairs perfectly with my other recent project, &lt;a href="https://github.com/ian-cowley/AgentDevKit" rel="noopener noreferrer"&gt;AgentDevKit&lt;/a&gt; (a native C# LLM orchestration library).&lt;/p&gt;

&lt;p&gt;Drop a star on the repo, throw a few million vectors at the memory storage, and let me know how many milliseconds it takes to saturate your RAM! Let's prove C# belongs in the AI ecosystem.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>dotnet</category>
      <category>csharp</category>
      <category>performance</category>
    </item>
    <item>
      <title>C# got left behind in the AI Agent hype. So I fixed it! AgentDevKit</title>
      <dc:creator>Ian Cowley</dc:creator>
      <pubDate>Thu, 14 May 2026 17:14:04 +0000</pubDate>
      <link>https://dev.to/iancowley/c-got-left-behind-in-the-ai-agent-hype-so-i-fixed-it-agentdevkit-4b40</link>
      <guid>https://dev.to/iancowley/c-got-left-behind-in-the-ai-agent-hype-so-i-fixed-it-agentdevkit-4b40</guid>
      <description>&lt;p&gt;If you’re a .NET developer watching the current AI landscape, you probably know the feeling.&lt;/p&gt;

&lt;p&gt;A massive new paradigm drops—in this case, autonomous AI agents—and overnight, Python and TypeScript are flooded with official SDKs, shiny frameworks like LangChain or crewAI, and endless tutorials.&lt;/p&gt;

&lt;p&gt;Meanwhile, us C# backend devs are left staring at the wall, waiting for an enterprise-grade port that will inevitably be bloated, overly abstracted, and arrive 18 months late.&lt;/p&gt;

&lt;p&gt;I’ve been writing backend software and integrations for 40 years. I don’t like waiting, and I don't like bloated frameworks. I just wanted a native, high-performance way to build AI agents in C# that can actually &lt;em&gt;do&lt;/em&gt; things—read files, query databases, and use the new Model Context Protocol (MCP) without a mountain of boilerplate.&lt;/p&gt;

&lt;p&gt;Since it didn't exist, I built it. &lt;/p&gt;

&lt;p&gt;Meet &lt;strong&gt;&lt;a href="https://github.com/ian-cowley/AgentDevKit" rel="noopener noreferrer"&gt;AgentDevKit (ADK)&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is it?
&lt;/h3&gt;

&lt;p&gt;It’s a native C# Agent Development Kit designed to help you create "Agents"—AI personalities powered by Google Gemini that don't just talk, but &lt;strong&gt;think, plan, and act&lt;/strong&gt; using real-world tools.&lt;/p&gt;

&lt;p&gt;Here is a quick look at what I built into it to make it actually useful for backend environments.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The Brain &amp;amp; The Hands (Tools)
&lt;/h3&gt;

&lt;p&gt;An LLM is just a brain. Without tools, it's just a chatbot. ADK lets you easily attach "hands" to your agent so it can interact with your systems.&lt;/p&gt;

&lt;p&gt;When you ask the agent a question, it pauses, realizes it needs more info, executes your C# tool, and uses the result to finish its answer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Setup the connection&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;llm&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;GeminiService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Create the Agent&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;LlmAgent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"SysAdminBot"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;instructions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"You are an expert system administrator."&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Give it hands (a tool you define in C#)&lt;/span&gt;
&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tools&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;FileReadTool&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="c1"&gt;// Run it&lt;/span&gt;
&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RunAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"What is in the server.log file?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Model Context Protocol (MCP) Integration
&lt;/h3&gt;

&lt;p&gt;This is the real game-changer. MCP is becoming the industry standard for letting AI securely plug into local environments.&lt;/p&gt;

&lt;p&gt;Instead of writing a custom C# wrapper for every single database or file system you want your AI to touch, ADK supports MCP out of the box.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Auto-connect to a local SQLite database using MCP&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;mcp&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;McpService&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;databaseTools&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;mcp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;InitializeFromConfigAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tools&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddRange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;databaseTools&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Delegation (Building Teams)
&lt;/h3&gt;

&lt;p&gt;One agent is cool. A team of agents is dangerous.&lt;br&gt;
ADK supports orchestration patterns like Pipelines and Parallel workflows. You can literally give a "Manager" agent a "Researcher" agent to use as a tool.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;researcher&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;LlmAgent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Researcher"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Find facts..."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;manager&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;LlmAgent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Manager"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Guide the project..."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// The Manager can now delegate tasks to the Researcher&lt;/span&gt;
&lt;span class="n"&gt;manager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tools&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;DelegationTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;researcher&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Guardrails (Because I trust no one, especially AI)
&lt;/h3&gt;

&lt;p&gt;If you've managed backend integrations as long as I have, you know that giving an AI blind access to tools is a terrible idea.&lt;/p&gt;

&lt;p&gt;ADK includes &lt;strong&gt;Human-in-the-Loop (HITL)&lt;/strong&gt; approvals and interceptors. If a tool is dangerous, wrap it in a &lt;code&gt;SensitiveTool&lt;/code&gt;. The agent &lt;em&gt;cannot&lt;/em&gt; execute it without an explicit green light from your &lt;code&gt;IApprovalService&lt;/code&gt; (which you can hook up to a console prompt, a web UI, or an email).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Block the AI from breaking out of directories&lt;/span&gt;
&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BeforeToolCall&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;".."&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;SecurityException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Path breakout attempt blocked!"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5. Resilient Parsing
&lt;/h3&gt;

&lt;p&gt;Smaller local models (and even big ones sometimes) spit out malformed JSON when trying to call tools. ADK has a built-in Self-Correction Loop. If the LLM messes up the JSON structure, the SDK catches the error, feeds the error back to the model, and tells it to fix its formatting automatically based on a retry budget.&lt;/p&gt;

&lt;h3&gt;
  
  
  Try it out
&lt;/h3&gt;

&lt;p&gt;I built this to fill a massive gap in the .NET ecosystem, keeping the architecture pragmatic and heavily focused on orchestration and safety.&lt;/p&gt;

&lt;p&gt;If you are a C# dev wanting to mess around with autonomous agents, MCP, and Gemini without having to learn Python, check it out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/ian-cowley/AgentDevKit" rel="noopener noreferrer"&gt;ian-cowley/AgentDevKit&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Drop a star, try hooking it up to your database via MCP, and let me know how it goes. If you find any bugs, open an issue—I'd love to hear how other C# devs are using it.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>csharp</category>
      <category>agents</category>
      <category>google</category>
    </item>
    <item>
      <title>I built a minimal, zero-dependency PDF library for C# because I hate bloat</title>
      <dc:creator>Ian Cowley</dc:creator>
      <pubDate>Wed, 13 May 2026 19:12:26 +0000</pubDate>
      <link>https://dev.to/iancowley/i-built-a-minimal-zero-dependency-pdf-library-for-c-because-i-hate-bloat-58b1</link>
      <guid>https://dev.to/iancowley/i-built-a-minimal-zero-dependency-pdf-library-for-c-because-i-hate-bloat-58b1</guid>
      <description>&lt;h1&gt;
  
  
  I built a minimal, zero-dependency PDF library for C# because I hate bloat
&lt;/h1&gt;

&lt;p&gt;If you’ve been writing C# for a while, you know the drill. You need to generate a simple PDF—maybe an invoice, a quick report, or a receipt.&lt;/p&gt;

&lt;p&gt;So, you open up NuGet, search for "PDF", and suddenly you are staring down the barrel of a 50MB dependency, a labyrinth of complex object models, and licensing terms that require a law degree to understand.&lt;/p&gt;

&lt;p&gt;I’ve been a developer for 40 years. I mostly work on backend and integration software for the printing industry. I like keeping things close to the metal, and I generally despise pulling in massive frameworks when a pragmatic, lightweight solution will do the trick.&lt;/p&gt;

&lt;p&gt;I didn't want a bloated library. I just wanted to draw some text, a few shapes, and maybe throw an image on a page.&lt;/p&gt;

&lt;p&gt;So, I ported a minimal PDF generator to .NET 10. Meet &lt;strong&gt;&lt;a href="https://github.com/ian-cowley/tinypdf-csharp" rel="noopener noreferrer"&gt;tinypdf-csharp&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is it?
&lt;/h3&gt;

&lt;p&gt;It is a C# port of the original TypeScript &lt;code&gt;tinypdf&lt;/code&gt; by Lulzx. But I didn't just translate it; I added a few quality-of-life features that make it significantly more useful for day-to-day backend development.&lt;/p&gt;

&lt;p&gt;The entire library is &lt;strong&gt;just over 1,000 lines of code&lt;/strong&gt;. It has &lt;strong&gt;zero external dependencies&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The "Batteries Included" Additions
&lt;/h3&gt;

&lt;p&gt;While the original architecture was beautifully simple, I needed it to do a bit more heavy lifting for real-world tasks. Here is what I added over the original:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Markdown to PDF:&lt;/strong&gt; This is my favorite addition. You can pass a raw Markdown string into the library, and it handles the layout and spits out a formatted PDF.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flate (Deflate) Compression:&lt;/strong&gt; Automatically compresses the PDF streams so your file sizes stay tiny. (You can toggle it off if you need raw streams).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clickable Links:&lt;/strong&gt; Full support for clickable hyperlinks, complete with optional underline styling.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Plus, it still has all the basics: text rendering (Helvetica, Times, Courier), shapes (rectangles, circles, wedges, lines), and JPEG image embedding.&lt;/p&gt;

&lt;h3&gt;
  
  
  Show me the code
&lt;/h3&gt;

&lt;p&gt;It is designed to be ridiculously simple to use. Here is how you generate a PDF with some text and a red box:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;TinyPdf&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TinyPdfCreate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Page&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Hello World"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;700&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;24&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Rect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;650&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"#FF0000"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;pdf&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteAllBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"output.pdf"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or, if you want to use the Markdown converter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;TinyPdf&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;md&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"# Header\n\nThis is a paragraph.\n\n- List item"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;pdf&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TinyPdfCreate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Markdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;md&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteAllBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"markdown.pdf"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Is it fast?
&lt;/h3&gt;

&lt;p&gt;Because there's no massive object tree or bloated memory footprint, it flies. I ran a benchmark generating 1,000 multi-page invoice PDFs concurrently, and it finished in &lt;strong&gt;~270 milliseconds&lt;/strong&gt;. That’s 0.27ms per PDF. If you are generating receipts or pie charts, it drops to around &lt;strong&gt;0.03ms&lt;/strong&gt; per PDF.&lt;/p&gt;

&lt;h3&gt;
  
  
  Try it out
&lt;/h3&gt;

&lt;p&gt;If you are building a microservice, an AWS Lambda, or a simple backend integration and you just need to spit out PDFs without sacrificing your app's payload size, give it a try.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NuGet:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package TinyPdf

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/ian-cowley/tinypdf-csharp" rel="noopener noreferrer"&gt;ian-cowley/tinypdf-csharp&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Drop by the repo, take a look at the code (again, it's only ~1000 lines, so you can read the whole thing on your lunch break), and let me know what you think. If it saves you from installing a massive legacy PDF framework today, my job is done.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>pdf</category>
      <category>dotnetcore</category>
      <category>markdown</category>
    </item>
    <item>
      <title>I built a native C# DataFrame engine to rival Python Polars (it's actually faster on some things)</title>
      <dc:creator>Ian Cowley</dc:creator>
      <pubDate>Wed, 13 May 2026 10:32:44 +0000</pubDate>
      <link>https://dev.to/iancowley/i-built-a-native-c-dataframe-engine-to-rival-python-polars-its-actually-faster-on-some-things-opg</link>
      <guid>https://dev.to/iancowley/i-built-a-native-c-dataframe-engine-to-rival-python-polars-its-actually-faster-on-some-things-opg</guid>
      <description>&lt;h1&gt;
  
  
  I built a native C# DataFrame engine to rival Python Polars (it's actually faster on some things)
&lt;/h1&gt;

&lt;p&gt;If you work in data science or heavy data engineering, you already know about &lt;a href="https://pola.rs/" rel="noopener noreferrer"&gt;Polars&lt;/a&gt;. It’s the Rust-backed powerhouse that took the Python ecosystem by storm, leaving Pandas in the dust.&lt;/p&gt;

&lt;p&gt;But if you’re a .NET developer, the data manipulation story has always been a bit… frustrating. We have &lt;code&gt;Microsoft.Data.Analysis&lt;/code&gt;, but it lacks the expressive lazy API and raw speed we crave. We often end up exporting data to Python just to process it, only to bring it back to C#.&lt;/p&gt;

&lt;p&gt;I got tired of waiting for a native .NET solution. So, I decided to build one from scratch.&lt;/p&gt;

&lt;p&gt;Meet &lt;strong&gt;&lt;a href="https://github.com/ian-cowley/Glacier.Polaris" rel="noopener noreferrer"&gt;Glacier.Polaris&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It is a high-performance, strongly-typed DataFrame library for C# (.NET 10). It features SIMD-accelerated compute kernels, a lazy execution engine, native nullability (Kleene logic), and it currently passes 135/135 golden-file parity tests against Python Polars.&lt;/p&gt;

&lt;p&gt;And after weeks of fighting the .NET JIT compiler and CPU caches, it is actually beating Polars in several key benchmarks.&lt;/p&gt;

&lt;p&gt;Here is how I pushed C# to its physical limits to pull this off.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Zero-Allocation and SIMD String Filtering
&lt;/h2&gt;

&lt;p&gt;In standard C#, string operations are heavy. If you filter a DataFrame with &lt;code&gt;df.Filter(Expr.Col("Status") == "Completed")&lt;/code&gt;, checking materialized &lt;code&gt;.NET&lt;/code&gt; string objects one by one will instantly ruin your performance due to pointer-chasing and heap allocations.&lt;/p&gt;

&lt;p&gt;To beat Polars (which uses Arrow's contiguous memory format), I couldn't use C# strings.&lt;/p&gt;

&lt;p&gt;Instead, Glacier.Polaris stores strings as flat UTF-8 byte arrays. When you execute an equality filter, the engine loads your target string into a &lt;code&gt;Vector256&amp;lt;byte&amp;gt;&lt;/code&gt; register. As it scans the 10-million row DataFrame, it fires a single AVX2 instruction (&lt;code&gt;Vector256.Equals&lt;/code&gt;) that compares entire words simultaneously against the target bytes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Result:&lt;/strong&gt; String exact-match filtering in Glacier runs in &lt;strong&gt;3.62 ms&lt;/strong&gt; for 1 million rows, beating Polars (&lt;strong&gt;~4.2ms&lt;/strong&gt;) with zero string allocations.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Breaking the 4ms Barrier: The Float64 Sorting War
&lt;/h2&gt;

&lt;p&gt;The hardest fight I had was with &lt;code&gt;ArgSort&lt;/code&gt; on Float64 data.&lt;/p&gt;

&lt;p&gt;Initially, I wrote a highly optimized, single-threaded Radix sort. I managed to drop the sorting time for 1 million floats to &lt;strong&gt;13.11 ms&lt;/strong&gt;—a massive 5.4x speedup over the standard .NET &lt;code&gt;Array.Sort&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;But Polars was doing it in &lt;strong&gt;4.21 ms&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;At 13ms, my C# code had officially maxed out the physical capabilities of a single CPU core. Moving 200MB of data (keys and indices) over 8 radix passes requires about 47 GB/s of memory bandwidth. A single core physically taps out around 15-20 GB/s.&lt;/p&gt;

&lt;p&gt;I needed to parallelize it. But .NET's &lt;code&gt;Parallel.For&lt;/code&gt; has too much overhead; spinning up the ThreadPool state machine takes 1-2ms alone, which is a death sentence when your target is 4ms.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Fix: The Parallel Block Tournament Merge&lt;/strong&gt;&lt;br&gt;
Instead of using standard .NET parallel loops, I built a custom generic parallel block merge engine:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The engine slices the 1M array into isolated chunks and hands them to raw &lt;code&gt;Task&lt;/code&gt; objects.&lt;/li&gt;
&lt;li&gt;Each core executes a single-threaded Radix sort entirely inside its &lt;strong&gt;L2 Cache&lt;/strong&gt;, meaning it never talks to system RAM, avoiding Translation Lookaside Buffer (TLB) thrashing.&lt;/li&gt;
&lt;li&gt;The engine merges the sorted chunks using a stable, parallel pairwise tournament merge.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;The Result:&lt;/strong&gt; Float64 sorting dropped to &lt;strong&gt;12.05 ms&lt;/strong&gt; for 1M rows, and successfully scaled to sort 10 Million rows in just &lt;strong&gt;84.71 ms&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  3. The Benchmarks (C# vs Polars)
&lt;/h2&gt;

&lt;p&gt;I ran these benchmarks on the same machine, comparing Glacier.Polaris (.NET 10 Release build) against Polars 1.40.1.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(Note: Times are in milliseconds. Lower is better).&lt;/em&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation (1M Rows)&lt;/th&gt;
&lt;th&gt;Glacier.Polaris (C#)&lt;/th&gt;
&lt;th&gt;Python Polars&lt;/th&gt;
&lt;th&gt;Winner&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DataFrame Creation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0.02 ms&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;5.33 ms&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;🟢 C# (~266x)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Sum (Int32)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0.14 ms&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0.45 ms&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;🟢 C# (3.2x)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Standard Deviation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0.33 ms&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0.55 ms&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;🟢 C# (1.7x)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;GroupBy Sum (Int32)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1.56 ms&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;5.20 ms&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;🟢 C# (3.3x)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Inner Join (Small Right)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;2.29 ms&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;4.61 ms&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;🟢 C# (2.0x)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Rolling StdDev&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;3.15 ms&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;12.92 ms&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;🟢 C# (4.1x)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;By utilizing single-pass Welford algorithms for variance, contiguous memory, and a custom Fibonacci-hashing hash map for joins, C# absolutely flies.&lt;/p&gt;
&lt;h2&gt;
  
  
  Try it out
&lt;/h2&gt;

&lt;p&gt;Glacier.Polaris covers ~98% of the Python Polars core surface area, including LazyFrames, query optimization (predicate/projection pushdowns), and full temporal operations.&lt;/p&gt;

&lt;p&gt;If you are building high-performance data pipelines, backtesting financial algorithms, or doing ML preprocessing in .NET, I’d love for you to try it out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/ian-cowley/Glacier.Polaris" rel="noopener noreferrer"&gt;https://github.com/ian-cowley/Glacier.Polaris&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NuGet:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package Glacier.Polaris

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Star the repo, try to break the lazy execution engine, and let me know what features you want to see next! Let's bring world-class data engineering to .NET.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>polars</category>
      <category>datascience</category>
    </item>
  </channel>
</rss>
