<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xml:base="https://goblinreactor.com/" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    
    <title>Goblin Reactor Field Notes</title>
    <link>https://goblinreactor.com/</link>
    <atom:link href="https://goblinreactor.com/feed.xml" rel="self" type="application/rss+xml" />
    <description>Systems performance notes on memory, latency, data layout, SIMD, and performance-critical infrastructure.</description>
    <language>en</language>
    <item>
      <title>Kernel TCP Was 14× Slower Than Goblin Core’s Fastest Path</title>
      <link>https://goblinreactor.com/blog/kernel-tcp-14x-slower-than-goblin-core-fastest-local-path/</link><description>&lt;p&gt;I am building low-latency AWS support for Goblin Core.&lt;/p&gt;
&lt;p&gt;AWS EFA uses libfabric, but renting expensive cloud hardware is a bad time to discover that the client, server, completion queues, receive path, or small-message handling is broken. Before taking Goblin Core to a native EFA instance, I wanted to qualify the complete libfabric code path locally.&lt;/p&gt;
&lt;p&gt;So I connected two 14-year-old, four-socket Intel Xeon systems with a direct 100 Gb/s Mellanox ConnectX-5 link and tested the combinations that mattered:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ordinary kernel TCP;&lt;/li&gt;
&lt;li&gt;libfabric’s software &lt;code&gt;tcp&lt;/code&gt; provider;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;verbs;ofi_rxm&lt;/code&gt; over the Mellanox hardware;&lt;/li&gt;
&lt;li&gt;Redis RESP2 and Goblin Core’s compact SBE protocol;&lt;/li&gt;
&lt;li&gt;automatic small-message injection and forced &lt;code&gt;fi_send&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Every workload used pipeline depth one: send one operation, wait for its complete response, then send the next. This was a latency test, not an attempt to fill a 100 Gb/s pipe. (&lt;a href=&quot;https://goblin-core.dev/EFA-LATENCY&quot; title=&quot;Libfabric provider matrix over 100 Gb/s — Goblin Core&quot;&gt;Goblin Core&lt;/a&gt;)&lt;/p&gt;
&lt;h2&gt;The result&lt;/h2&gt;
&lt;p&gt;Across eight Redis-shaped operations, the fastest configuration completed the round trip in &lt;strong&gt;6.39 microseconds at the arithmetic-average median&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The ordinary Redis-compatible baseline—RESP2 over kernel TCP—took &lt;strong&gt;90.33 microseconds&lt;/strong&gt;.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;th style=&quot;text-align:right&quot;&gt;Wire format&lt;/th&gt;
&lt;th style=&quot;text-align:right&quot;&gt;Average p50&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Kernel TCP&lt;/td&gt;
&lt;td style=&quot;text-align:right&quot;&gt;RESP2&lt;/td&gt;
&lt;td style=&quot;text-align:right&quot;&gt;90.33 µs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Libfabric &lt;code&gt;tcp&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:right&quot;&gt;RESP2&lt;/td&gt;
&lt;td style=&quot;text-align:right&quot;&gt;39.59 µs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;verbs;ofi_rxm&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:right&quot;&gt;RESP2&lt;/td&gt;
&lt;td style=&quot;text-align:right&quot;&gt;7.38 µs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;verbs;ofi_rxm&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:right&quot;&gt;SBE&lt;/td&gt;
&lt;td style=&quot;text-align:right&quot;&gt;&lt;strong&gt;6.39 µs&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;That makes the fastest path &lt;strong&gt;14.1 times lower latency&lt;/strong&gt; than ordinary kernel TCP with RESP2. It also supports roughly 156,000 sequential round trips per second across the tested operations, compared with about 11,000 for the baseline. (&lt;a href=&quot;https://goblin-core.dev/EFA-LATENCY&quot; title=&quot;Libfabric provider matrix over 100 Gb/s — Goblin Core&quot;&gt;Goblin Core&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;The eight operations were:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;PING&lt;/code&gt;, &lt;code&gt;SET&lt;/code&gt;, &lt;code&gt;GET&lt;/code&gt;, &lt;code&gt;HSET&lt;/code&gt;, &lt;code&gt;HGET&lt;/code&gt;, &lt;code&gt;ZADD&lt;/code&gt;, &lt;code&gt;ZSCORE&lt;/code&gt;, and Pub/Sub.&lt;/p&gt;
&lt;p&gt;The Pub/Sub number includes more than acknowledging &lt;code&gt;PUBLISH&lt;/code&gt;: timing ends only after a subscriber on a separate connection receives and validates the message. Each distribution contained 20,000 warmups followed by 200,000 measured operations, with every response checked for correctness. (&lt;a href=&quot;https://goblin-core.dev/EFA-LATENCY&quot; title=&quot;Libfabric provider matrix over 100 Gb/s — Goblin Core&quot;&gt;Goblin Core&lt;/a&gt;)&lt;/p&gt;
&lt;h2&gt;Most of the win did not require abandoning RESP&lt;/h2&gt;
&lt;p&gt;The fastest result used SBE, Goblin Core’s compact binary protocol. But RESP2 over the same &lt;code&gt;verbs;ofi_rxm&lt;/code&gt; transport still averaged &lt;strong&gt;7.38 microseconds&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;That is important.&lt;/p&gt;
&lt;p&gt;The binary wire format helped, but the larger result came from changing the transport and software path. In this benchmark, a Redis-shaped RESP operation over the libfabric verbs path was more than twelve times faster than RESP2 over Goblin Core’s ordinary kernel TCP path. (&lt;a href=&quot;https://goblin-core.dev/EFA-LATENCY&quot; title=&quot;Libfabric provider matrix over 100 Gb/s — Goblin Core&quot;&gt;Goblin Core&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;The software &lt;code&gt;tcp&lt;/code&gt; provider was also interesting. RESP2 over libfabric &lt;code&gt;tcp&lt;/code&gt; averaged 39.59 microseconds—2.28 times lower than the ordinary socket implementation.&lt;/p&gt;
&lt;p&gt;That does not establish a universal law that libfabric TCP is always faster than sockets. It says something narrower and more useful: &lt;strong&gt;inside this real server, the surrounding software path consumed a large fraction of the latency.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A 100 Gb/s card does not automatically give an application low latency. The protocol parser, buffer ownership, syscall pattern, polling model, completion handling, NUMA placement, and application architecture still decide how quickly one request becomes one validated response.&lt;/p&gt;
&lt;h2&gt;Then the tail latency went bad&lt;/h2&gt;
&lt;p&gt;The median result was only part of the work.&lt;/p&gt;
&lt;p&gt;An earlier qualification run had an ugly, repeatable step in the tail. The average SBE/RDM p99.99 was roughly &lt;strong&gt;857 microseconds&lt;/strong&gt;—wildly out of proportion to a single-digit-microsecond median.&lt;/p&gt;
&lt;p&gt;The obvious suspect was message reordering.&lt;/p&gt;
&lt;p&gt;So I instrumented it.&lt;/p&gt;
&lt;p&gt;Across nearly two million publisher replies and 220,001 subscriber replies, the client observed no missing sequence, duplicate, reorder-window overflow, stranded frame, or pending reordered message. Server request counters were clean too.&lt;/p&gt;
&lt;p&gt;The transport was not losing control of the messages.&lt;/p&gt;
&lt;p&gt;The stall came from a completely different place: Goblin Core’s bootstrap TCP listener.&lt;/p&gt;
&lt;p&gt;The polled server called nonblocking &lt;code&gt;accept()&lt;/code&gt; on every pass through its spin loop, whether or not a connection was waiting. On Linux, even an unsuccessful &lt;code&gt;accept()&lt;/code&gt; may create and retire socket, file, and inode-related objects before returning &lt;code&gt;EAGAIN&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Those objects eventually have to be reclaimed.&lt;/p&gt;
&lt;p&gt;Tracing showed repeated RCU softirq batches, some lasting roughly 0.4 to 1.1 milliseconds. A listener intended only for bootstrap connections was periodically stalling the low-latency data path. (&lt;a href=&quot;https://goblin-core.dev/EFA-LATENCY&quot; title=&quot;Libfabric provider matrix over 100 Gb/s — Goblin Core&quot;&gt;Goblin Core&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;The fix was conceptually simple: check whether the listener is ready before calling &lt;code&gt;accept()&lt;/code&gt;, and drain it only when a connection is actually waiting.&lt;/p&gt;
&lt;p&gt;Before the fix, one trace contained 4,119 RCU softirq runs longer than 100 microseconds.&lt;/p&gt;
&lt;p&gt;During a two-second active trace after the fix, every softirq run remained below 100 microseconds; the longest was 20.6 microseconds.&lt;/p&gt;
&lt;p&gt;The immediate control run dropped average SBE/RDM p99.99 from &lt;strong&gt;857.47 to 29.47 microseconds&lt;/strong&gt;. The independent full provider matrix reproduced the improvement at &lt;strong&gt;30.83 microseconds&lt;/strong&gt;. (&lt;a href=&quot;https://goblin-core.dev/EFA-LATENCY&quot; title=&quot;Libfabric provider matrix over 100 Gb/s — Goblin Core&quot;&gt;Goblin Core&lt;/a&gt;)&lt;/p&gt;
&lt;h2&gt;The boring code can own the latency&lt;/h2&gt;
&lt;p&gt;The listener was not part of the exciting design.&lt;/p&gt;
&lt;p&gt;It was not RDMA. It was not the compact protocol. It was not the Redis data structure. It was not the 100 Gb/s NIC.&lt;/p&gt;
&lt;p&gt;It was a small bootstrap path doing unnecessary work inside the wrong loop.&lt;/p&gt;
&lt;p&gt;That is a recurring lesson in low-latency systems: the headline mechanism may be correct while something apparently harmless nearby destroys the tail.&lt;/p&gt;
&lt;p&gt;The only reliable response is to measure the complete system, preserve anomalous results, and trace the mechanism instead of explaining the anomaly away.&lt;/p&gt;
&lt;h2&gt;What this result proves&lt;/h2&gt;
&lt;p&gt;It proves that Goblin Core’s libfabric RDM implementation works across two physical hosts with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;real request and response validation;&lt;/li&gt;
&lt;li&gt;both RESP2 and SBE;&lt;/li&gt;
&lt;li&gt;small-message injection;&lt;/li&gt;
&lt;li&gt;explicit &lt;code&gt;fi_send&lt;/code&gt; with transmit completion handling;&lt;/li&gt;
&lt;li&gt;posted receives and receive completion queues;&lt;/li&gt;
&lt;li&gt;Pub/Sub over separate publisher and subscriber connections;&lt;/li&gt;
&lt;li&gt;bounded message reordering;&lt;/li&gt;
&lt;li&gt;stable low-microsecond medians;&lt;/li&gt;
&lt;li&gt;p99.99 behavior in the tens of microseconds after fixing the listener.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It also shows that the transport and surrounding software architecture can matter far more than the nominal speed of the network card.&lt;/p&gt;
&lt;h2&gt;What it does not prove&lt;/h2&gt;
&lt;p&gt;This is &lt;strong&gt;not yet an AWS EFA hardware benchmark&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The native &lt;code&gt;efa&lt;/code&gt; provider was unavailable on these local machines. The measurements used AWS’s libfabric build with the &lt;code&gt;tcp&lt;/code&gt; and &lt;code&gt;verbs;ofi_rxm&lt;/code&gt; providers over a direct ConnectX-5 Ethernet link.&lt;/p&gt;
&lt;p&gt;That distinction matters. EFA still needs its own run.&lt;/p&gt;
&lt;p&gt;The purpose of this work was to reach AWS with a qualified implementation, a reproducible benchmark harness, validated send and receive paths, and known-good local baselines—not to rename a local verbs result as an EFA result. (&lt;a href=&quot;https://goblin-core.dev/EFA-LATENCY&quot; title=&quot;Libfabric provider matrix over 100 Gb/s — Goblin Core&quot;&gt;Goblin Core&lt;/a&gt;)&lt;/p&gt;
&lt;h2&gt;Why this belongs on Goblin Reactor&lt;/h2&gt;
&lt;p&gt;This experiment is a small example of how I approach difficult systems work:&lt;/p&gt;
&lt;p&gt;Establish the real measurement.&lt;/p&gt;
&lt;p&gt;Test the complete matrix rather than the favorite configuration.&lt;/p&gt;
&lt;p&gt;Keep the strange result instead of smoothing it away.&lt;/p&gt;
&lt;p&gt;Instrument the suspected mechanism.&lt;/p&gt;
&lt;p&gt;Change the implementation.&lt;/p&gt;
&lt;p&gt;Run the entire experiment again.&lt;/p&gt;
&lt;p&gt;The outcome was not simply a fast number. It was a validated network path, a repaired tail-latency failure, and a reproducible artifact ready for the next machine.&lt;/p&gt;
&lt;p&gt;That is the work Goblin Reactor is built to do: take a hard systems problem through architecture, implementation, hostile measurement, and a result that survives contact with the machine.&lt;/p&gt;
&lt;p&gt;The next stop is native AWS EFA.&lt;/p&gt;
</description><pubDate>Mon, 27 Jul 2026 16:00:00 +0000</pubDate>
      <dc:creator>Adam DePrince</dc:creator>
      <guid>https://goblinreactor.com/blog/kernel-tcp-14x-slower-than-goblin-core-fastest-local-path/</guid>
    </item>
    <item>
      <title>What Goblin Core proves about performance work</title>
      <link>https://goblinreactor.com/blog/what-goblin-core-proves/</link><description>&lt;p&gt;Performance work should leave evidence behind. Goblin Core is one example of what that evidence can look like: not a vague claim that a system is “fast,” but measurements tied to a workload, a transport, and a machine.&lt;/p&gt;
&lt;p&gt;The current headline numbers are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;51 bytes of RSS per sorted-set member at four million members.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;14.3 billion real Lichess rating updates replayed.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;220 nanoseconds for a single-element shared-memory request and reply.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;392,000 ZADD operations per second on one core.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Those numbers describe different parts of the system. They should not be compressed into one universal speed claim.&lt;/p&gt;
&lt;h2&gt;Memory is a data-structure decision&lt;/h2&gt;
&lt;p&gt;At four million sorted-set members, Goblin Core uses 51 bytes of resident memory per member. That result comes from packed arenas and compact Swiss-table indexes instead of a graph of separately allocated objects.&lt;/p&gt;
&lt;p&gt;The useful lesson is broader than Redis compatibility: when memory cost grows with millions of tiny objects, allocation strategy and data layout are often the product architecture. Treating them as implementation details can become very expensive.&lt;/p&gt;
&lt;h2&gt;Throughput needs a realistic history&lt;/h2&gt;
&lt;p&gt;The Lichess replay runs 14.3 billion real rating updates in chronological order and checks the result against Redis member for member. A long, real history exercises behavior that a small synthetic loop can miss: changing set sizes, repeat updates, ranking churn, and the cumulative effects of the storage design.&lt;/p&gt;
&lt;h2&gt;Latency includes the round trip&lt;/h2&gt;
&lt;p&gt;The 220 ns result is a synchronous, unpipelined request and reply for a one-field &lt;code&gt;HSET&lt;/code&gt; or one-member &lt;code&gt;ZADD&lt;/code&gt; over shared-memory SBE rings. The client and server are pinned to separate cores on a Threadripper PRO 5995WX, and the server busy-polls.&lt;/p&gt;
&lt;p&gt;That context matters. The result shows what is possible when the kernel is removed from the hot path and a core is deliberately owned by the service. It is not a promise that an ordinary networked deployment will respond in 220 ns.&lt;/p&gt;
&lt;h2&gt;A benchmark should make the trade visible&lt;/h2&gt;
&lt;p&gt;The single-core ZADD result measures 392,000 operations per second. Together with the latency and memory numbers, it gives a more useful picture than any one result alone: how much work a core completes, how quickly one request returns, and how much memory the data consumes.&lt;/p&gt;
&lt;p&gt;That is the standard Goblin Reactor brings to client work. First define the workload and the outcome that matters. Then measure the existing system, isolate the cost, build the smallest credible optimization, and state the tradeoffs alongside the result.&lt;/p&gt;
&lt;p&gt;The full benchmark methodology and current results live at &lt;a href=&quot;https://goblin-core.dev/&quot;&gt;goblin-core.dev&lt;/a&gt;.&lt;/p&gt;
</description><pubDate>Thu, 09 Jul 2026 16:00:00 +0000</pubDate>
      <dc:creator>Adam DePrince</dc:creator>
      <guid>https://goblinreactor.com/blog/what-goblin-core-proves/</guid>
    </item>
  </channel>
</rss>