Nexus: High-Performance Go Concurrency Primitives for Extreme Performance (SPSC, Sharded MPMC Mailbox)
▲ 1 r/HiLoad

Nexus: High-Performance Go Concurrency Primitives for Extreme Performance (SPSC, Sharded MPMC Mailbox)

Hey HiLoad community!

TL;DR: I've poured my heart into building a lock-free Go library that absolutely flies – it's up to 10 times faster than standard channels under heavy load (think 32+ cores). Dive in for the benchmarks and the "why" behind the architecture!

I'm super excited to share my latest project, Nexus! It's a Go library packed with high-performance, lock-free concurrency primitives. If you've ever found yourself hitting the limits of standard Go channels or mutexes when you need really high throughput and ultra-low latency, then Nexus might just be your new best friend.

So, what's the big deal? When you're pushing systems to their absolute maximum (especially on those beefy multi-core processors, 32+ cores and beyond!), standard Go synchronization tools like mutexes and channels can actually become the bottleneck. All those atomic operations on shared head and tail counters can lead to a ton of contention for cache lines (what we call "false sharing"), which just grinds performance down and kills scalability.

My Solution: Nexus Nexus is built with a deep understanding of "mechanical sympathy" – meaning it's designed to work with the hardware, not against it. Here's what it brings to the table:

  • Truly Lock-Free Design: We're talking pure atomic operations here, which means minimal kernel context switches. It's all about speed!
  • Decentralized Architecture: Especially cool in the ShardedMailbox, there are no central counters or "hint" variables, which eliminates bottlenecks and lets performance scale beautifully with every extra core you throw at it.
  • Cache-Friendly: I've carefully padded the data structures to prevent false sharing, keeping your CPU caches happy and efficient.

What's inside the box?

  1. spsc: This is a rock-solid, lock-free Single-Producer-Single-Consumer (SPSC) queue. Perfect for those tight pipelines where one goroutine is churning out data and another is gobbling it up.
  2. sharded: Meet the Sharded Mailbox! This isn't your grandma's queue. It's a collection of single-element "mailboxes" (shards). It's specifically engineered for super high-throughput message exchange between many producers and many consumers (MPMC).
    • How does the ShardedMailbox work its magic? Each shard is like its own little independent "mailbox" with a simple "empty" or "full" state. Producers and consumers play a clever "casino" strategy: they start by checking their "home" shard, and if that's busy, they gracefully move to adjacent ones. This smartly distributes the load and avoids those nasty "hot spots." The Go runtime then naturally balances everything out: faster goroutines snatch up free/full shards quicker, while slower ones just wait a bit longer. The result? A beautiful self-balancing act where your most performant cores get to do the most work!

Let's talk numbers! Benchmarks (on an AMD Ryzen 9 7950X3D 16-Core Processor)

ShardedMailbox (MPMC) vs. a standard channel:

  • At 4 cores: The ShardedMailbox is already ~2.4 times faster than a standard channel.
  • At 32 cores: This is where Nexus really shines! The ShardedMailbox is an incredible ~10.5 times faster than a standard channel. Talk about scalability!

SPSCQueue (SPSC) vs. a standard channel:

  • At 4 cores: Even in a simple single-producer-single-consumer setup, SPSCQueue is ~10.6 times faster than a standard channel.

So, when should you reach for Nexus?

  • Latency is your enemy: If 50-100 ns of channel latency feels like an eternity.
  • Concurrency is off the charts: When you're dealing with 32, 64, or even 128 cores, and standard mutexes are just fighting each other for cache lines.
  • Predictability is key: You want to avoid those annoying GC "freezes" caused by unnecessary allocations.

Real-world Use Cases (where Nexus truly shines):

  • High-Frequency Trading (HFT): Every microsecond counts when you're transferring market quotes or order execution signals.
  • Real-time Game Event Processing: Imagine updating a game world with thousands of players' actions – Nexus keeps things smooth.
  • Database and Storage Engines: Perfect for implementing things like Write-Ahead Logs (WAL) or LSM-tree structures.
  • Network Proxies and API Gateways: Handling tens of thousands of requests per second? Nexus can build lock-free pipelines that handle immense loads.
  • AI/ML Inference Pipelines: Feeding data to your models without CPU-side bottlenecks. If your model is fast but data transport is slow, Nexus is your answer.

When might you stick with standard Go channels?

  • Low-load applications: For a simple web service with, say, 10 requests per second, standard channels are perfectly fine and often simpler to maintain.
  • Complex select logic: Go channels are unique for their select statement, letting you wait on multiple events. Nexus is more of a "super-fast pipe"; it's all about direct data exchange, not complex selection logic.

Quick Code Example (Sharded Mailbox):

package main

import (
	"fmt"
	"sync"

	"github.com/GenshIv/nexus/sharded"
)

func main() {
	mailbox := sharded.NewShardedMailbox[int]()

	var wg sync.WaitGroup
	numMessages := 100

	wg.Add(numMessages * 2)

	fmt.Printf("Launching %d producer-consumer pairs on %d shards...\n", numMessages, mailbox.ShardCount())

	for i := 0; i < numMessages; i++ {
		go func(consumerID int) {
			defer wg.Done()
			item, err := mailbox.Dequeue(uint64(consumerID))
			if err != nil {
				fmt.Printf("Consumer %d failed: %v\n", consumerID, err)
				return
			}
			fmt.Printf("Consumer %d received: %d\n", consumerID, item)
		}(i)
	}

	for i := 0; i < numMessages; i++ {
		go func(producerID int) {
			defer wg.Done()
			item := 1000 + producerID
			err := mailbox.Enqueue(uint64(producerID), item)
			if err != nil {
				fmt.Printf("Producer %d failed: %v\n", producerID, err)
				return
			}
			fmt.Printf("Producer %d sent: %d\n", producerID, item)
		}(i)
	}

	wg.Wait()
	fmt.Println("\nAll messages have been successfully exchanged.")
	mailbox.Close()
}

Installation is a breeze: go get github.com/GenshIv/nexus

I'd absolutely love to hear your thoughts, questions, and any suggestions you might have! Feel free to give Nexus a spin in your own projects and definitely share your results.

Check out the GitHub repo here: https://github.com/GenshIv/nexus

u/No-Job-5616 — 10 hours ago
▲ 3 r/HiLoad

silentjson v2.0.0: Hitting the hardware limits, or how we squeezed the maximum out of JSON parsing in Go

Hello community! We are excited to announce the major update of our JSON parser without code generation and allocations -- silentjson v2.0.0.

First of all, a huge thank you to everyone who supported the project and provided feedback. We wouldn't have come this far without your involvement.

What is new in v2.0.0?

In this release, we focused on ultimate efficiency and specific high-load use cases:

  • Scalar parsing optimization (non-AVX2). We have significantly improved the scalar fallback. Now, even on architectures without modern vector instruction support or where they are disabled, the parser shows excellent results.
  • hft-ipc. Those who know, know.

The icing on the cake: hitting the physical limits

The main news of the release -- the speed has accelerated so much that we simply stopped being able to catch up. We have practically eliminated software overhead and are now simply hitting the raw hardware and memory bandwidth.

It is cool, yes. But we have to be honest: an extremely heavy load has an impact on us as well. To be fair, in a stress scenario simulation under heavy load, the parsing speed dropped by 60%. But even with this drop, silentjson continues to outperform all competitors by an insurmountable margin. The safety margin turned out to be colossal.

Here is a small teaser with tests on AMD Ryzen 9 7950X3D (parsing 100,000 objects):

Mode Throughput
Standard (encoding/json) 110 MB/s
Sonic (JIT) 644 MB/s
SilentJSON (Scalar) 810 MB/s
SilentJSON (AVX2) 24 670 MB/s

(Details later or on GitHub)

Do not take our word for it

Do not forget about our contribution and go check if we have deceived you. Just in case.

Update to the latest version:

go get github.com/GenshIv/silentjson@v2.0.0

Post your benchmarks in the comments to this article. Who can do more!

Project Repository: github.com/GenshIv/silentjson

reddit.com
u/No-Job-5616 — 3 days ago
▲ 2 r/HiLoad

How to make money on the CPU. Why do we sell features instead of solutions?

Optimization is always good, of course. But let's be honest: the business needs a product. Here and now. The business rarely thinks about what comes "later". That's why they hire a team of developers and demand features. They need it written faster, shipped faster, and sold faster.

But you don't have to sell just products or features. You can sell solutions.

Imagine a classic scenario: the business has grown, and traffic has skyrocketed (or we started selling 5 times more cupcakes). I had a real case in my practice. They come to me and say: "We urgently need to buy 3 more servers, we can't handle the load". And I look at the metrics and reply: "What for? Our current servers are only at 20% capacity".

Has this ever happened to you? Usually, it's the exact opposite. Developers themselves are begging: "Give us more hardware, our microservices are suffocating".

Now imagine a different dialogue: Business: -- Why did we even buy such an expensive server if it's sitting practically idle right now? You: -- Because the conditions and architecture were different back then. Now, we've made the code run efficiently.

Do you know what the main problem is? The business buys "features", even though it actually wants to buy a solution. But very few people actually sell those solutions. Most often, developers just sell "delayed headaches" with the condition of buying new hardware and endlessly scaling horizontally.

It's hard to blame the programmers here. We all live in a certain engineering culture where abstractions take center stage, and understanding how the hardware actually works is considered "low-level" and unnecessary. Let's look at how we got here through three famous quotes.

>"Software is getting slower more rapidly than hardware becomes faster." -- Niklaus Wirth, creator of Pascal (Wirth's Law, 1995).

In the race for development speed, we started hiding the hardware behind thick layers of frameworks, virtual machines, and Garbage Collectors. We are "burning" all the massive computing power that modern multi-core processors give us just to maintain these abstractions. Hardware gets more powerful every year, but programs run slower and slower for the end user.

>"We broke monoliths into microservices to solve management problems, not performance problems." -- Kelsey Hightower, one of the main evangelists of Kubernetes.

Many young developers believe that microservices are synonymous with HighLoad and speed. In reality, we took a lightning-fast local memory call (which took nanoseconds) and replaced it with a slow network request with endless JSON serialization (which takes milliseconds). The industry voluntarily traded pure CPU speed for insane network latency.

And finally, we got way too carried away with trendy design patterns:

>"I made up the term ‘object-oriented’, and I can tell you I did not have C++ in mind." -- Alan Kay, one of the creators of Smalltalk (OOPSLA '97).

A bit later, he added:

>"I’m sorry that I long ago coined the term “objects” for this topic... The big idea is messaging."

We somehow unnoticeably got carried away with beautiful objects, abstract factories, and layers, completely forgetting how the system actually communicates internally and how this code will eventually be executed in silicon.

But there is another way. We can "sell" servers to the business without physically selling them. Even servers that haven't been bought yet!

The point is, if you deeply study your domain, you can strip away all the architectural fluff. You can implement your idea so that it eats microscopic CPU ticks, rather than entire server racks. Essentially accelerating the system and reducing resource consumption not just by a factor of ten, but by hundreds of times.

Engineering is when your code works in synergy with the processor, not fights against it.

Join our community at r/HiLoad. Let's find elegant solutions, not just "ship features".

And now, a question for you: How often have you managed to convince the business when they asked to "just buy more hardware", instead of giving you time to find and optimize the bottleneck in the architecture? Share your stories!

reddit.com
u/No-Job-5616 — 5 days ago
▲ 1 r/HiLoad

Ditching ML for Math: How we packed a catalog of millions of products into an int64

Hey r/HiLoad,

I wanted to share a war story from a past project. We were dealing with a classic nightmare: receiving millions of products from suppliers and having to classify them instantly as their price lists came in.

Because it was the trendy thing to do, we initially threw a neural network at the problem. In production, it was actually pretty snappy. But the training phase? An absolute dumpster fire. The human factor completely ruined it because the team responsible for managing the catalog and keywords was frankly just phoning it in.

They kept feeding absolute garbage into the training set. Naturally, the model choked on this chaos and started hallucinating wildly. My personal favorite was when it confidently classified a massive batch of toilet paper into the "Baby Car Seats" category. You can imagine the collective groan from the dev team when we had to stay up half the night manually unscrewing thousands of misclassified items after a "successful" release.

To make it all worse, retraining this beast took up to two weeks. And the punchline? Because the catalog team never stopped making mistakes, we had to kick off this miserable two-week process almost every single month. Two weeks of training, maybe two weeks of peace, then someone uploads garbage again, paper towels suddenly belong in Auto Parts, and we're back in hell.

We desperately needed a solution that was dead-simple, rock-solid, easy to debug, and -- most importantly -- didn't need two weeks to learn what a car seat is. The fix turned out to be surprisingly elegant: we ripped out the ML magic entirely and replaced it with pure math.

From Words to Numbers: The Magic of Overflow

The core idea was to turn any product description into a tiny, numerical "fingerprint."

The pipeline was stupidly simple:

  1. Take the raw product name.
  2. Strip out all the garbage (stop words, prepositions).
  3. Run it through a stemmer to just get the root words.
  4. Hash each meaningful root into an int64.

Here’s the fun part. We didn't store an array of hashes. We just added them all together using standard binary addition. The trick here is that when you add large numbers in int64, you get a natural overflow, which just drops the most significant bits. In Go (and C/C++), this is completely legal, costs zero CPU cycles, and happens instantly.

The code looks almost too simple:

import "hash/fnv"

// Hash a single cleaned root word
func hashWord(word string) int64 {
	h := fnv.New64a()
	h.Write([]byte(word))
	return int64(h.Sum64())
}

// Get the numerical fingerprint for the whole product name
func getProductFingerprint(tokens []string) (hashSum int64, wordCount int) {
	for _, token := range tokens {
		// Stop-word filtering and stemming happens around here
		if isStopWord(token) {
			continue
		}
		
		wordHash := hashWord(token)
		
		// Standard binary addition. 
		// The int64 overflows naturally—no panics, no worries.
		hashSum += wordHash 
		wordCount++
	}

	return hashSum, wordCount
}

Just like that, we packed the "meaning" of an entire product name into exactly one int64 -- regardless of whether it had three words or ten. We'd also generate a few of these fingerprints per product to cover permutations and weird supplier spellings.

The Index and Foolproofing

That int64 became our key. For lookups, we just threw it into a standard in-memory Map. The value was a tiny struct:

  1. ID of the target category or product model.
  2. Word Count -- the exact number of tokens that built this hash (that wordCount variable from the code).

Why keep the word count? That was our bulletproof vest against collisions and our secret weapon for smart ranking.

When a new item came in from a price list, we'd run its name through the pipeline and hit the Map. If the int64 matched, we absolutely had to check if the word counts matched too. This took the chance of a false collision (two different sets of words accidentally summing to the same hash) down to mathematical zero.

During a lookup, we'd generate fingerprints for various combinations of words in the name. If we got multiple hits, the category that matched the highest number of words won. Longer phrase = more accurate match.

Trust, but Verify (The Dev-Tool)

Given our PTSD from the catalog team (never forget the toilet paper car seats), we refused to do any more blind debugging. If an item went to the wrong category, we wanted receipts.

So, we built an internal Dev-Tool. You paste a product name in, and it spits out the exact thought process: how it was tokenized, the stemmed roots, the int64 hashes, and crucially -- which exact words matched the database and which were ignored.

You can't argue with transparent math. Whenever someone complained about a misclassification, we could find the culprit (usually a horribly written keyword rule) in two clicks, rather than spending hours trying to decipher neural network logs.

Performance

The numbers were great. Even with 5 million unique products (which balloons to tens of millions of records when you add the hash variations), the whole index (int64 Key + Category ID + Word Count) barely took up a couple hundred megabytes.

The entire thing lived comfortably in RAM. Classifying a single item meant a couple of fast hashes and an O(1) map lookup. We were chewing through incoming price lists at the speed of the network interface while the CPU basically sat there twiddling its thumbs.

Scaling it out

Since it was just a Map, it scaled horizontally like a dream. We just distributed the heavy price list streams across worker goroutines. No DB transactions, no table locks, no massive SQL queries -- just pure arithmetic and memory reads.

We also reused this for validation. If a supplier sent a product and its fingerprint matched a model we already knew, the system instantly recognized it as a duplicate. We just attached the new price to the existing item and kept the catalog completely clean.

TL;DR: ML isn't always the answer. Sometimes, adding a bunch of hashes together and letting the integer naturally overflow gives you an instantly searchable, O(1) in-memory index that takes up almost no RAM and doesn't take two weeks to train.

Curious to hear from you guys -- how would you handle cache invalidation for an in-memory setup like this if the business decides to change the stop-word dictionary or stemming rules on the fly? Would love to hear your thoughts.

reddit.com
u/No-Job-5616 — 5 days ago
▲ 2 r/HiLoad+2 crossposts

Accelerating Go: How we beat GNU Grep and reached 7.3 GB/s without a single line of Assembly

Many people think Go is a typical language for enterprise, form building, and boring microservices. And they are somewhat right. But unlike their perspective, we understand what hides under the hood if you dig a little deeper.

A very common task: exact search of multiple strings in a huge file. Who hasn't done this? We constantly use it for logging and analysis. For example, GNU grep is a great program. It has regex, word variations, and speed. So, what am I talking about? Ah yes... You haven't seen real speed yet.

Today, we are going to search for a dictionary of 50,000 unique keys in a log file of exactly 1 Gigabyte. And we will do it in Go.

1. Naive approach: Writing the file loop

First, let's sketch out the core. We will read the file not line-by-line (which kills the garbage collector), but in fat 10 Megabyte chunks using bufio.Scanner.

func (m *Matcher) MatchReader(reader io.Reader) {
    scanner := bufio.NewScanner(reader)
    buf := make([]byte, 10*1024*1024)
    scanner.Buffer(buf, 10*1024*1024)

    for scanner.Scan() {
        lineBytes := scanner.Bytes()
        // ... substring search
    }
}

2. Setting up output and callbacks

To make our library universal, we won't write fmt.Println directly in the core. We will make an elegant Callback:

type MatchCallback func(lineNum int, bytePos int, pattern []byte, lineContent []byte)

Now, upon every match, we will pass the line number and the found word to the outside.

3. Setting up parameters (Two-byte hash)

How do we search for 50,000 words simultaneously? Checking each word in a loop is death (we'd get 50 Terabytes of scanning). Write an Aho-Corasick tree? Long, complicated, and, running ahead—unnecessary.

We take the first 2 bytes of each search word, convert them into a uint16, and create an array of 65,536 buckets.

type Matcher struct {
    buckets [65536][][]byte
}

Upon initialization (which takes a laughable 2 ms), we distribute the 50,000 words into these buckets. On average, there is less than one word per bucket!

4. Measuring the first version (Single thread, no unsafe)

In the loop, we simply take a 2-byte window and look into the buckets[idx] bucket. If it's not empty, we compare the remaining word via bytes.Equal.

Running the benchmark:

BenchmarkMatcher-32      100      10927238 ns/op       959.60 MB/s

~1 GB/s. Good, but not enough. We want to squeeze all the juice out of the hardware!

5. The dark side of Go: Adding unsafe and multithreading

Now buckle up. We remove the safety checks of the Go compiler.

Instead of idx := uint16(line[i]) | uint16(line[i+1])<<8, which causes an array bounds check on every byte, we write:

ptr := unsafe.Pointer(&data[0])
idx := *(*uint16)(unsafe.Pointer(uintptr(ptr) + i))

What happens from the Assembly (ASM) perspective? The Go compiler takes the hint and collapses this piece into one single assembly memory read instruction (like MOVZX), executed in one clock cycle!

Next—CPU Cache. We add hasPattern [65536]bool to the structure. This array weighs exactly 64 Kilobytes—it perfectly, byte-for-byte, fits into the ultra-fast L1 cache of modern processors. We no longer touch RAM at all!

And finally—Multithreading with smart queues. We stream the file, slice it by the last newline \n (so as not to cut keys in half at chunk boundaries), and throw the pieces into taskChan. Workers process them in parallel, incrementing the result via atomic counters: atomic.AddInt64(&matchesCount, 1).

6. Measuring and staying in shock

Running our new pure in-memory benchmark (go test -bench . -benchmem):

goos: windows
goarch: amd64
pkg: grep/deanon
cpu: AMD Ryzen 9 7950X3D 16-Core Processor          
BenchmarkMatcher-32              100      10927238 ns/op       959.60 MB/s      10547080 B/op        126 allocs/op
BenchmarkMatcherParallel-32       97      14240700 ns/op      7363.23 MB/s      62197060 B/op       1169 allocs/op

7.36 Gigabytes per second! At the same time, the algorithm practically doesn't touch the garbage collector.

Out of curiosity, we take the native C GNU grep (v3.0 with Aho-Corasick support) and sic it on our 1GB log file (NVMe SSD).

7. Final comparison

Below are the results of a real scan of the target.log file of 1 Gigabyte (search with -F -f flag for grep and MatchReaderParallel for our Go code).

Results Table (Dictionary of 10,000 keys)

Tool Threads Scan Time (1 GB) Throughput
GNU Grep 3.0 1 (Single) ~0.62 sec ~1.6 GB/s
Our Go (Base) 1 (Single) ~0.95 sec ~1.0 GB/s
Our Go (Unsafe) 32 (Multi) 0.29 sec ~3.4 GB/s 🏆

Results Table (Dictionary of 50,000 keys)

The larger the dictionary, the more classic search suffers due to the growth of data structures. But our hash array has a fixed size (64 KB), so the speed hardly drops!

Tool Threads Scan Time (1 GB) Throughput
GNU Grep 3.0 1 (Single) 1.39 sec ~0.7 GB/s
Our Go (Base) 1 (Single) 1.10 sec ~0.9 GB/s
Our Go (Unsafe) 32 (Multi) 0.32 sec ~3.1 GB/s 🏆

(Note: On a real file, we hit a bottleneck at 3.1 GB/s, as this is the physical read speed limit of our SSD drive. In RAM, as seen from the benchmarks, the algorithm is capable of 7.3 GB/s).

Reflections: Why do we need "Aho-Corasick"?

The standard Aho-Corasick algorithm for multi-search builds a huge transition graph in RAM. For 50k words, that's hundreds of thousands of pointers. Running through this graph, you constantly catch Cache Misses. Our "stupid" approach with a 2-byte index array and direct access via unsafe puts the filter directly into the CPU L1 cache and destroys any trees.

Conclusions

In our core, there is not a single line written manually in Assembly (we also didn't use full-fledged SIMD via C-bindings). We stayed within the standard Go tooling.

But if you study the tool you work with, you can work miracles. In C/C++ it would have been much harder and not necessarily faster: managing buffer pools, cross-platform work with channels and goroutines—in Go, this is done out of the box. We operate directly on the hardware (via unsafe), but at the same time completely and safely manage high-level queues and multithreading.

Don't be afraid to look under the hood!

Repo if anyone wants to poke around the assembly: https://github.com/GenshIv/grep

u/No-Job-5616 — 6 days ago
▲ 86 r/HiLoad+2 crossposts

How to split 10GB JSON files in seconds without hitting RAM limits

We had this classic pain point on our project: constantly chewing through massive JSON arrays. Catalogs, analytics dumps, ML datasets — files ranging from a couple hundred megabytes to tens of gigabytes.

The task was stupidly simple: split a giant JSON array into individual elements so we could chunk them or throw them into parallel processing. No data transformation, no querying by keys. We literally just needed to find where each chunk starts and ends.

Naturally, we started with the classic approach: json.Unmarshal -> slice -> json.Marshal. On a 10GB file, memory consumption went to the moon. We ended up spending more time fighting the Go garbage collector (GC) than doing actual work.

And then it clicked: to just move the data around, we don't need to understand what's inside it. We just need to find the boundaries.

Stop parsing, start scanning

Every parser out there (even the ultra-fast ones like sonic or simdjson) still builds a tree in memory. Instead, you can just treat the JSON as a raw byte stream. Look for structural markers, find the edges, and cut.

The entire logic boils down to a tiny state machine:

  1. Nesting counter: { and [ go +1, } and ] go -1.
  2. String tracking: keep track of when you enter "..." so you don't accidentally react to brackets inside a text field.
  3. Escapes: a \" inside a string is a trap, not the end of the string.
  4. The boundary: whenever your nesting depth is exactly 0, any comma , is where you split.

That’s it. We don't care about keys or values. We don't allocate a single byte, we just return memory views (slices) of the original buffer.

Here’s what the concept looks like in Go (oversimplified, ignoring string logic):

gofunc findElements(data []byte) []Chunk {
    var chunks []Chunk
    depth := 0
    start := 0
    for i, b := range data {
        switch b {
        case '{', '[':
            depth++
        case '}', ']':
            depth--
        case ',':
            if depth == 0 {
                chunks = append(chunks, Chunk{Start: start, End: i})
                start = i + 1
            }
        }
    }
    if start < len(data) {
        chunks = append(chunks, Chunk{Start: start, End: len(data)})
    }
    return chunks
}

Obviously, this naive code will break on the first tricky whitespace or string, but you get the point. We aren't parsing. We are scanning.

Why is this so damn fast?

  1. Zero allocations in the hot loop. You're just handing back data[start:end]. No new objects, no copying strings, no building hash maps.
  2. Hardware absolutely loves it. Your entire working state is basically two integers. It easily fits in L1 cache, and memory reads are strictly sequential.
  3. The branch predictor is happy. A simple state machine with highly predictable transitions is infinitely easier for the CPU to digest than a full parser juggling dozens of token types.

Look at how much work we are skipping:

Step Standard Parser Boundary Scanner
Read bytes
Classify tokens Only {}[]"\ and ,
Build hash maps
Allocate strings
Allocate slices
Type conversion
What you get back []MyStruct [][]byte (pointers to original buffer)

We are literally throwing away 80% of the overhead.

But how fast is it actually?

I got a bit carried away and polished this into a production-ready tool. I added proper string handling, escape tracking, and rewrote the hot loop in AVX2 assembly (chewing through 32 bytes per cycle using SIMD bitmasks).

Tbh, the results surprised even me:

Approach What it does Throughput Memory Overhead
encoding/json Full parse → Go structs ~107 MB/s 3-4x input size
sonic / simdjson-go Optimized parse → structs/AST ~400–700 MB/s ~1.1x
My AVX2 scanner Just finds boundaries ~4.1 GB/s ~1.0x (zero extra)

At 4.1 GB/s, the algorithm isn't even the bottleneck anymore. It's bottlenecked by the RAM's read bandwidth. The CPU is just sitting there waiting for the next cache line to arrive.

The catch (Tradeoffs)

Because I went the unsafe and raw assembly route for maximum speed, you have to pay the price:

  • Platform-specific: The AVX2 branch only works on amd64. For ARM (hello MacBooks), you need a pure Go fallback.
  • Memory lifecycle danger: You are getting slices that point directly to the original buffer. If that []byte gets overwritten or GC'd while you're still working with the chunks... it's going to hurt.
  • No validation: The scanner takes your word that the JSON is valid. Feed it garbage, and it will silently slice up garbage.

TL;DR

The biggest insight was stupidly simple: stop thinking "I need to parse this JSON" and start thinking "I need to find boundaries in a byte stream". Once I changed my perspective, the code wrote itself and the performance gap was massive.

Has anyone else suffered through this? How do you guys route or chunk massive JSON payloads in production when you simply can't fit them into RAM?

If anyone wants to poke around the assembly or run the benchmarks, the repo is here: https://github.com/GenshIv/silentjson

u/No-Job-5616 — 5 days ago
▲ 1 r/HiLoad

Welcome to r/HiLoad: Why we are here (and what to post)

If you’ve ever spent days hunting down a memory leak, fought the Garbage Collector in production, or rewritten a perfectly readable function into an ugly, unsafe assembly kernel just to squeeze out an extra 500 MB/s of throughput — welcome home.

I created r/HiLoad because I felt something was missing in the standard programming subreddits. Too often, deep technical posts about performance optimization or niche architectural choices are met with "just use the standard library," or worse, removed by moderators for "self-promotion."

Here, the rules are simple and built for engineers:

1. Performance over purity We respect beautiful code, but we respect production metrics more.

2. Bring the receipts (Data & Benchmarks) We love flame graphs, allocation metrics, and throughput numbers. We love data.

3. Show and Tell (Yes, you can share your projects!) If you built a crazy fast tool, library, or pipeline — share it! Self-promotion is absolutely allowed and encouraged here, as long as you provide technical context. Tell us the problem, how you solved it, the tradeoffs you made, and what didn't work.

What to post:

  • Deep dives into backend architecture at scale.
  • Optimizing CPU, Memory, or I/O.
  • Database scaling and query tuning.
  • "War stories" from production outages and how you fixed them.
  • Benchmarks and performance comparisons of languages/frameworks.

Make yourself at home. Drop a comment, share what you're currently working on (or struggling with), and let's build a community that actually cares about how things run under the hood.

reddit.com
u/No-Job-5616 — 6 days ago
▲ 43 r/Backend

How to split 10GB JSON files in seconds without hitting RAM limits

I had a real problem at work: we were constantly processing massive JSON array dumps — catalogs, analytics exports, ML datasets. The files ranged from hundreds of megabytes to tens of gigabytes.

The task was simple: split a giant JSON array into individual objects so they could be routed, chunked, or processed in parallel. That's it. No transformation, no querying by field name — just find where each {...} starts and ends.

And yet, we were doing json.Unmarshal → slice → json.Marshal. On a 10 GB file, the memory usage was absurd, and we spent more time fighting the GC than doing actual work.

At some point I realized: we don't need to understand the data to move it. We just need to find the boundaries.

The idea: boundary extraction

Instead of building an object tree (which is what every parser does — even fast ones like simdjson or sonic), you can treat JSON as a byte stream with structural markers and just scan for the edges of each object.

The logic boils down to a small state machine:

  1. Nesting counter: { increments, } decrements.
  2. String tracking: you need to know if you're inside "..." so you don't count braces inside string values.
  3. Escape handling: a \" inside a string is not a real quote.
  4. The boundary: when the nesting counter returns to 0 after being >0, you've found one complete object.

That's it. You don't look at keys. You don't look at values. You don't allocate anything. You return slices (memory views) into the original buffer.

Here's the conceptual core in Go (simplified, without the string-tracking):

gofunc findBoundaries(data []byte) []Chunk {
    var chunks []Chunk
    depth := 0
    start := -1
    for i, b := range data {
        switch b {
        case '{':
            if depth == 0 {
                start = i
            }
            depth++
        case '}':
            depth--
            if depth == 0 && start >= 0 {
                chunks = append(chunks, Chunk{Start: start, End: i + 1})
                start = -1
            }
        }
    }
    return chunks
}

Of course, this naive version is wrong — it doesn't handle strings, escapes, or nested arrays. But the principle is the key insight: you're not parsing, you're scanning.

Why this is fast

When you scan boundaries instead of parsing:

  • Zero allocations in the hot path. You return data[start:end] — a slice of the original buffer. No new objects, no string copies, no map construction.
  • Cache-friendly. Your working state is a couple of integers (depth counter, string flag). Everything fits in L1. The only memory access pattern is a linear sequential read.
  • Pipeline-friendly. A state machine with predictable transitions is much kinder to the CPU's branch predictor than a parser that dispatches on dozens of token types.

Compare this to what a "real" parser does:

Step Full parser Boundary scanner
Read bytes
Classify tokens Only {}[]"\
Build hash maps
Allocate strings
Allocate slices
Type conversion
Return objects []MyStruct [][]byte (slices into original buffer)

You're literally removing 80% of the work.

But how fast, really?

I got curious and decided to implement this properly — with a real string-aware state machine, correct escape handling, and a full AVX2 assembly kernel on amd64 for the hot scanning loop (processing 32 bytes per cycle, using SIMD bitmasks to classify structural characters in parallel).

Honestly, the results surprised even me:

Approach What it does Throughput Memory overhead
encoding/json Unmarshal Full parse → Go structs ~107 MB/s 3-4x input size
sonic / simdjson-go Optimized parse → structs/AST ~400–700 MB/s ~1.1x
Boundary scan (AVX2 asm) Just finds {...} edges ~4.1 GB/s ~1.0x (zero extra)

>

The ~4.1 GB/s number is essentially limited by memory read bandwidth on my machine, not by the scanner's logic. The AVX2 kernel spends most of its time waiting for the next cache line to arrive.

When this is (and isn't) useful

Good fit:

  • Splitting a huge JSON array into files/chunks for parallel processing
  • Streaming: extracting objects from an io.Reader on-the-fly without loading the whole thing into RAM
  • Proxying/routing: forwarding individual JSON objects from an array to different consumers
  • Pre-processing for a real parser: find boundaries first, then parse each chunk on a separate goroutine

Bad fit:

  • You need actual field values (you'll need a parser anyway)
  • Your JSON isn't arrays-of-objects (this technique targets [{...},{...},...])
  • Small files where the overhead of json.Unmarshal is negligible

The tradeoffs

Since I went the unsafe / raw assembly route for maximum performance, there are real costs:

  • Platform-specific. The AVX2 path only works on amd64. You need a scalar fallback for ARM/other architectures.
  • unsafe usage. Any time you avoid allocations by returning slices into a shared buffer, you need to be very careful about the buffer's lifetime. If the underlying []byte gets overwritten or GC'd while you still hold slices — boom.
  • Not a validator. The scanner trusts that the input is valid JSON. It won't give you nice error messages about malformed data.

Closing thought

The mental shift was surprisingly simple: stop thinking "I need to parse this JSON" and start thinking "I need to find the boundaries in this byte stream." Once you reframe the problem, the solution practically writes itself — and the performance gap compared to full parsing is enormous.

Has anyone else gone down this path? I'm curious how people handle large-scale JSON splitting/routing in production — especially the streaming case where you can't load the full file.

reddit.com
u/No-Job-5616 — 7 days ago