Obsidio
## Inspiration
The challenge placed a financial analytics API inside a container limited to two CPUs and 2 GiB of memory. Most requests were inexpensive price lookups or statistics, but /risk required a chain of 50,000 dependent SHA-256 operations. A server could use both cores and still fail because heavy requests would block everything behind them.
That made the project interesting to us. We were not simply trying to build the fastest SHA-256 benchmark. We wanted to build a service that remained responsive while completely saturated; a system that behaved well when it had no spare capacity left.
## What Obsidio does
Obsidio is a C++17 analytics API with four endpoints:
- /health reports whether the service is running.
- /price returns an in-memory price.
- /stats calculates statistics over 500 data points on every request.
- /risk performs 50,000 rounds of SHA-256, feeding each hexadecimal digest into the next round.
The published workload chooses requests in a fixed 60/30/10 ratio and assigns weights of 1, 3, and
- That gave us the first important insight:
[ \mathbb{E}[\text{points per request}] = 0.6(1) + 0.3(3) + 0.1(10) = 2.5 ]
Therefore, for (N) completed requests,
[ \text{work score} \approx 2.5N ]
The score was fundamentally a throughput problem. Profiling showed that /risk consumed roughly 91% of the server’s CPU time, despite representing only 10% of requests. This separated the project into two different engineering problems:
- Keep inexpensive requests responsive enough to qualify.
- Make the risk computation dramatically cheaper to win.
## How we built it
We built the service without a web framework. Linux epoll threads own the network connections and answer /health, /price, and /stats directly. Expensive /risk work moves into a bounded worker queue.
The risk workers run at low scheduler priority, allowing the kernel to pre-empt them whenever an I/ O thread needs CPU. Once a hash completes, an eventfd wakes the connection’s owning I/O loop. This keeps socket ownership in one place and avoids cross-thread connection races.
We explicitly configure two I/O threads and two risk workers. A CPU-limited container can still see every processor on its host, so automatically sizing the thread pool would create far more runnable threads than the two-CPU quota could support.
After the first SHA-256 operation, every subsequent input is exactly 64 hexadecimal characters. That means every steady-state hash always has two blocks, and the second block is fixed padding. We precompute that block’s entire message schedule.
Obsidio then selects a hardware backend at runtime:
- ARMv8 SHA2 with a four-lane interleave
- x86 SHA-NI optimized for AMD Ryzen and other compatible x86 processors
- A portable, specialized fallback
The surrounding system remains portable; only the compute kernels are architecture-specific.
### From two lanes to eight
A single risk chain is inherently sequential: round (n+1) cannot begin until round (n) produces its digest. We found parallelism between requests instead. While one chain waits on an instruction dependency, another independent chain can use the processor.
Our first successful x86 kernel kept two chains entirely inside the 16-register XMM file. Their SHA rounds and hexadecimal conversion were interleaved instruction by instruction, eliminating the repeated memory round trips of the original implementation.
Two lanes still used only about half of the Ryzen SHA unit’s sustainable throughput. Going wider appeared impossible because each chain’s live state and message schedule needed too many registers.
The eventual solution was an eight-lane, pipelined phase-split kernel. We separated message- schedule generation from the SHA round state, stored expanded schedules in L1 cache, and divided eight chains into two groups. While one four-chain group executes SHA rounds, the processor simultaneously prepares schedules for the other:
[ \text{rounds}(A)\parallel\text{schedule}(B) \quad\longrightarrow\quad \text{rounds}(B)\parallel\text{schedule}(A') ]
The final version schedules two streams during each SHA block, allowing every lane’s scheduling work to ride inside otherwise unused instruction latency.
## Challenges we faced
The hardest part was not writing intrinsics. It was learning which measurements deserved to be believed.
Some of our most useful experiments made the program slower:
- Widening only the constant SHA block produced a 23% regression.
A sequential phase-split kernel made the round phase 1.78× faster, but exposed schedule work that had previously been hidden. Its total gain was only 6.2%, below our shipping threshold.
Mixing AVX2 with legacy SHA-NI instructions caused an approximately 98.5% collapse because of the AVX/SSE transition penalty.
Three-job x86 batches were 39% slower than two-job batches because the third chain fell back to the single-lane path.
Using one I/O thread, which had helped on ARM, lost 15.5% on the Ryzen environment.
Changing scheduler classes recovered no meaningful performance because both allocated CPUs were already fully occupied.
We also discovered that our tools could lie. One clock probe reported 1.49 GHz and led us to blame thermal throttling; later investigation proved the reading was an instrumentation artifact. Another load-test harness silently failed to forward an environment variable, producing results for a different workload than its label claimed. We learned to prefer in-process A/B ratios, repeated runs, independent controls, and generated-assembly inspection over a single impressive number.
Correctness was another constant challenge. A wrong SHA digest still looks like a perfectly valid 64-character hexadecimal string. It can be fast, plausible, and worth zero points. We therefore kept a deliberately plain implementation as an independent oracle. Every accelerated backend verifies itself at startup, and the Docker build tests automatic selection, the reference path, and the ARM and x86 backends independently. Cross-lane tests ensure that a digest cannot change depending on the batch width used to calculate it.
Hardening the network path uncovered similarly subtle bugs. Partial non-blocking writes could silently drop responses, and pipelined requests already read from the socket could stall forever after backpressure cleared. Raw-socket regression tests now force those normally rare paths. The suite also covers malformed framing, oversized inputs, conflicting Content-Length headers, non- finite prices, sanitizers, and backend correctness.
## What we learned
The biggest lesson was that performance engineering is as much about disproving ideas as implementing them. We became comfortable recording negative results and explicitly retracting conclusions when better evidence contradicted them.
We also learned that latency and throughput can behave counterintuitively. Moving from two lanes to eight increased the amount of work performed in one batch, yet /risk p95 fell because the queue drained faster. The batch itself became longer, but requests spent much less time waiting for a worker.
Most importantly, portability and specialization do not have to oppose each other. Obsidio has a portable server architecture, portable correctness oracle, and runtime feature detection, while still using aggressively specialized kernels where the workload spends nearly all its CPU time.
## Results
Metric Baseline Final ━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━ ━━━━━━━━━━━ Work score 5,818,877 7,365,605 ──────────────────── ─────────── ─────────── HTTP failures 0.00% 0.00% ──────────────────── ─────────── ─────────── Correctness checks 100% 100%
The final score is a 26.5% improvement over the clean Phase 0 baseline. A second complete 4-minute- measurement, preserve correctness, keep failed ideas in the record, and only ship the changes that survived all four.
Log in or sign up for Devpost to join the conversation.