Benchmarking the FSM: Pure Strings, Already Fast — and About to Get Faster

Benchmarking the FSM: Pure Strings, Already Fast — and About to Get Faster

2 41 74
calendar_today agoschedule10 min read

Benchmarking the FSM: Pure Strings, Already Fast — and About to Get Faster

There's a particular reaction you tend to get when you tell an experienced systems programmer that your architecture is string-backed:

"Don't use strings."

It's almost reflexive.

Stick figure teacher shown pointing at the white board where each white board has a different rule written on it.

Use enums.

Use integers.

Use precomputed IDs.

Avoid dictionary lookups.

Watch the garbage collector.

And, above all, don't put strings anywhere near a hot loop.

There's good engineering history behind those instincts. Strings can be expensive. Poorly designed string-heavy systems can generate allocations, create unnecessary comparisons, and turn perfectly reasonable abstractions into performance problems.

But there's a problem with rules of thumb:

Eventually, you have to measure.

So that's what I did.

The current FSM_API is purely string-backed.

Not string-backed with an integer cache hiding underneath.

Not enums converted to strings at the API boundary.

Not generated IDs.

Strings.

And I wanted to know what that actually costs.


The Baseline

Using BenchmarkDotNet, I measured the cost of updating active process groups as the workload increased from one group to ten and then to fifty.

The results:

Method Active Process Groups Mean Error StdDev Gen 0 Allocated
UpdateMultipleGroups 1 305.1 ns 1.89 ns 1.76 ns 0.0572 360 B
UpdateMultipleGroups 10 3,115.7 ns 20.14 ns 18.84 ns 0.5722 3,600 B
UpdateMultipleGroups 50 15,736.6 ns 121.61 ns 107.81 ns 2.8687 18,000 B

The first number is the one that gets your attention.

Stick figure teacher pointing at whiteboard with the results drawn out in a table on it.

305 nanoseconds.

That's the measured cost of updating one active process group.

A 60 FPS application has approximately 16.67 milliseconds available for an entire frame.

That's 16,670,000 nanoseconds.

The measured FSM operation therefore consumes approximately 0.0018% of that frame budget.

However, the more interesting result isn't the 305 ns.

It's what happens when I increase the workload.


Fifty Groups. Fifteen Microseconds.

At ten active process groups, the benchmark reports:

3.12 µs

At fifty:

15.74 µs

That's approximately 0.0157 milliseconds.

Against a 16.67 ms frame budget, fifty active process groups consume approximately:

0.094% of one frame.

That's not a theoretical "eventually it should scale" statement.

That's measured behavior.

And the scaling is almost perfectly linear.

Stick figure teacher with white board showing near linear scaling!

One group:

305 ns

Ten groups:

3.12 µs

Fifty groups:

15.74 µs

The architecture isn't encountering some mysterious performance cliff as the workload grows.

More groups mean more work.

Approximately proportionally more work.

That's exactly what we want from this kind of system.


Why Fifty?

Fifty isn't a magic number.

It isn't a claim that fifty process groups represents the maximum this FSM_API can handle.

It doesn't.

It is simply a useful workload for demonstrating what happens when we stop talking about one isolated state machine and start talking about multiple active systems being serviced together.

That's an important distinction.

A single-state benchmark can make almost anything look fast.

Real software rarely gets the luxury of doing only one thing.

A game might have character state, animation state, weapon state, UI state, navigation state, interaction state, environmental state, and networking state all operating simultaneously.

An application outside of games has its own collection of concurrent concerns.

The useful question therefore becomes:

What happens when we start stacking these things together?

In this benchmark, the answer is refreshingly boring.

The cost grows with the workload.

It doesn't suddenly explode.


What This Benchmark Does — and Doesn't — Prove

I believe it is worth being precise here.

This benchmark is measuring the cost of the FSM's process-group update machinery.

It is not claiming that the actual work performed by your states is free.

If your state executes an expensive pathfinding operation, performs a large physics calculation, processes network data, or does something computationally expensive, the FSM doesn't magically make that work disappear.

That's not what we're measuring.

We're measuring the machinery responsible for organizing and updating those processes.

And that machinery is currently taking approximately fifteen microseconds for fifty active groups.

That distinction is important because abstractions are often blamed for the work they organize.

A state machine can be blamed for a slow AI routine simply because the AI routine happens to execute inside a state.

This benchmark lets us separate those concerns.

The abstraction has a cost.

I measured it.

And at this scale, that cost is fairly small.


But There's Something Important Hiding in That Table

Stick figure teacher pointing at whiteboard with the cost of strings drawn out on it.

The benchmark also reports allocation.

One group:

360 bytes

Ten:

3,600 bytes

Fifty:

18,000 bytes

That is approximately 360 bytes per active process group per update under this benchmark.

At 60 FPS, if fifty groups were updated every frame, that corresponds to:

approximately 1.08 MB of managed allocation per second.

That isn't catastrophic.

But it isn't zero either.

And this is where the benchmark becomes particularly useful.

Because I'm not finished...


This Is the Performance Baseline — Not the Destination

The FSM_API architecture is currently fully string-backed.

Stick figure shouting 'All Aboard' a train heading from string backing to integer backing.

That's important because the numbers above aren't the result of some hidden integer representation doing the real work underneath.

The strings are actually part of the machinery being measured.

And that's intentional.

One of the architectural goals of FSM_API has always been to make state management expressive and discoverable.

States can have meaningful names.

Process groups can be identified without forcing the developer to maintain a parallel universe of integer constants.

The API remains readable.

The architecture remains flexible.

And now we have measured what that flexibility costs.

The answer is:

Not very much.

But there is another conclusion hiding in that result.

If this is what the system can do while paying the cost of its string-backed implementation, then we now have a very interesting optimization target.


The Next Version Removes the String Cost

I'm currently refactoring the underlying implementation so that the FSM can retain its string-facing architecture and developer experience while moving the internal backing representation toward integer indexing.

That's an important distinction.

The goal isn't to tell developers:

"Sorry, strings are slow. Go back and rewrite your application using integers."

The goal is the opposite.

Keep the expressive interface. Optimize what happens underneath it.

The developer can continue thinking in terms of:

"Idle"

"Running"

"Paused"

"Processing"

while the machinery underneath increasingly has the opportunity to operate on compact integer representations.

That's the architecture we're moving toward.

And the benchmark above gives us something extremely valuable:

a control group.

We already know what the purely string-backed implementation costs.

When the integer-backed implementation is complete, we can run the same benchmark again.

Same workload.

Same benchmark harness.

Same measurement methodology.

Then we can put the two implementations beside each other and see exactly what the architectural change bought us.


We Can Already See the Shape of the Opportunity

We shouldn't manufacture a performance number before we have measured it.

Stick figure reviewing the shape of opportunity when we go from string backing to integer backing.

That's one of the reasons I like this experiment.

We have a baseline.

We have a known workload.

We have known execution times.

We have known allocations.

Now we have something to beat.

The current 50-group benchmark takes:

15,736.6 ns

and allocates:

18,000 bytes.

Suppose, purely as a thought experiment, the integer-backed implementation reduced execution time by 25%.

That would take the measured operation from approximately:

15.7 µs → 11.8 µs

A 50% reduction would produce:

15.7 µs → 7.9 µs

A 75% reduction would produce:

15.7 µs → 3.9 µs

Those aren't predictions.

They're not marketing numbers.

They're simply showing the size of the optimization space we're about to measure.

And the allocation story could be even more interesting.

If the integer-backed representation eliminates allocations currently associated with string construction or other string-oriented work in the measured path, the memory profile could change substantially even if the raw execution-time improvement turns out to be modest.

That matters for systems that run continuously.

A few microseconds once isn't particularly interesting.

A few microseconds and a small allocation repeated every frame, across hundreds or thousands of objects, is a different conversation.

This is where optimization becomes architecture.


The Goal Isn't "Integer Good, String Bad"

This is probably the most important point in the entire experiment.

I'm not refactoring FSM_API because the string-backed implementation failed.

It didn't.

It's fast.

It scales predictably.

It's readable.

It's flexible.

And it has now survived measurement.

The integer-backed refactor is about taking an architecture that is already useful and asking:

Can we preserve everything developers like about it while making the machinery underneath even cheaper?

That's a much more interesting engineering problem.

We're not sacrificing usability to chase a benchmark score.

We're trying to make the implementation disappear even further beneath the abstraction.

The developer shouldn't have to care whether the machine underneath is using strings, integers, tables, caches, or some future representation we haven't invented yet.

They should be able to describe their state.

The machine should do the rest.


And This Is Where "Don't Use Strings" Gets Interesting

The benchmark doesn't prove that strings are universally fast.

They aren't.

It proves something much more useful:

A carefully engineered string-backed FSM does not automatically become a performance problem.

Our current implementation can update fifty active process groups in approximately fifteen microseconds.

That's fast enough that the abstraction itself occupies less than one tenth of one percent of a 60 FPS frame in this benchmark.

And that's before the integer-backed optimization is finished.

So perhaps the better engineering rule isn't:

Don't use strings.

Maybe it's:

Don't assume. Measure.

If the strings are costing us something measurable, optimize them.

If they aren't costing us enough to matter, spend our engineering effort somewhere more valuable.

And if we can preserve the developer-facing benefits of strings while removing much of their runtime cost?

Stick figure observing tradeoffs between string backing and integer backing

Then we've got the best of both worlds.


The Benchmark Becomes Part of the Architecture

This is ultimately why I like this benchmark more than a simple performance chart.

It isn't a victory lap.

It's a timestamp.

This is what the FSM costs today.

Pure strings.

305 ns for one group.

3.12 µs for ten.

15.74 µs for fifty.

360 bytes per group under the measured workload.

Linear scaling.

And now we're changing the internal representation.

So we'll measure it again.

The interesting question isn't whether the next implementation is faster.

It almost certainly should be.

The interesting question is:

How much faster?

And more importantly:

How much allocation can we eliminate without sacrificing the API and architectural flexibility that made the string-backed design worthwhile in the first place?

That's something worth measuring.

And when those numbers are available, we'll have something better than a benchmark claim.

We'll have an A/B measurement of the architecture itself.

The string-backed FSM already flies.

Now we're taking the strings out of the engine and finding out just how much faster it can fly.


You Don't Have to Take My Word for It

And here's the part where the benchmark stops being an article and becomes an invitation.

Stick figure seen trying the API out for themselves

The FSM_API is available now.

You don't have to agree with me about strings.

You don't have to take the 305 ns number on faith.

You don't even have to like the architecture.

Install it.

Build something with it.

Put it under your own workload.

Benchmark it against whatever you're currently using.

Then tell me what happens.

That's one of the reasons I publish these experiments in the first place.

The goal isn't to tell developers what they should believe.

The goal is to build the tooling, expose the architecture, measure the results, and let other engineers kick the tires.

And if you find a workload where it falls over?

I want to know about that too.

Because that's useful data.


Resources & Code:

The FSM Package (Unity Asset Store):
https://assetstore.unity.com/packages/slug/332450

NuGet Package (Non-Unity Core):
https://www.nuget.org/packages/TheSingularityWorkshop.FSM_API

GitHub Repository:
https://github.com/TrentBest/FSM_API

Support Our Work:

Patreon Page:
https://www.patreon.com/c/TheSingularityWorkshop

Support Us (PayPal Donation):
https://www.paypal.com/donate/?hosted_button_id=3Z7263LCQMV9J

Please Like this post, Love the code, and Share your experience in the comments.


A Hyper-Detailed, Architecturally Impossible Synthesis of Consciousness and Digital Matter. The image is a frenetic, deeply complex digital vista where a central Luminous Eye dominates a vast, glowing circuit landscape. This eye, suspended mid-air, is a sphere of intense, fractal energy—a pulsating vortex of pink, violet, and electric blue light—that powerfully suggests an emergence of digital consciousness or a Technological Singularity itself. The core is a bottomless black aperture, ringed by a white-hot plasma disc. Below this ocular energy source, the light dissipates into an intricate, copper-gold and neon-blue Circuit Board Megastructure that stretches to the horizon, impossibly dense with exaggerated microchips, glowing resistor arrays, and power conduits that form deep, glowing canyons. The background is a dark, holographic projection field displaying complex schematics, mathematical models, and flowing data streams, reinforcing the theme of Absolute Digital Engineering. The entire scene is bathed in a dramatic, opposing light source—a warm, orange-gold glow from the left and an ice-cold, electric-blue glow from the right—creating a maximalist, high-contrast visual experience that definitively rejects the minimalist simplicity of conventional design and announces: Technology is intricate, overwhelming, and infinitely complex.

🔥 Join developers growing publicly
Share your knowledge, build in public, and grow your developer presence with a global community.

More Posts

I’m a Senior Dev and I’ve Forgotten How to Think Without a Prompt

Karol Modelski - Mar 19

Systems Thinking: Thriving in the Third Golden Age of Software

Tom Smithverified - Apr 15

I spent years trying to get AI agents to collaborate. Then Opus 4.6 and Codex 5.3 wrote the rules

snapsynapseverified - Apr 20

The Control Group: Benchmarking the "Dirty" Version of My FSM API

The Singularity Workshop - Nov 30, 2025

The Sovereign Vault — A Comprehensive Guide to Protocol-Driven AI

Ken W. Algerverified - Jun 4
chevron_left
2.9k Points117 Badges
33Posts
17Comments
12Connections
Architecting the Future of Computation through Relentless Optimization.

The Singularity Workshop is... Show more

Related Jobs

Commenters (This Week)

10 comments
3 comments
1 comment

Contribute meaningful comments to climb the leaderboard and earn badges!