Katabench
Start free
← All docs

Writing fast C#

3 min read

The hidden performance tests aren't there to punish you — they're the part of the grade that separates "it works on the sample" from "it works at scale." This page is how to clear them.

Complexity first, everything else second

Most timeouts are not slow code; they're the wrong algorithm. A hidden test that feeds you n = 200,000 is decided by your big-O long before any micro-optimization matters. An O(n²) loop won't be rescued by a faster inner body — it needs to become O(n log n) or O(n).

Before you tune anything, ask the three questions that fix 90% of timeouts:

  • Am I re-scanning? A nested loop that searches a list on every step is the classic O(n²). A HashSet<T> or Dictionary<TKey,TValue> usually turns the inner scan into an O(1) lookup.
  • Am I re-computing? Work that doesn't change between iterations belongs outside the loop. Prefix sums, memoization, and a single sort up front routinely collapse a whole dimension.
  • Am I using the right shape? Two pointers, a sliding window, a sorted structure, a heap — the Algorithms track is designed so you reach for these by needing them.

Read the budget bar

Every test reports its server-measured time against a per-test time budget, drawn as a bar. A pass that fills 95% of the bar is a warning, not a victory — the next, larger hidden case will likely tip it over. Treat anything above ~80% as "still too slow for the size class this test represents" and look for a complexity win, not a constant-factor one.

Timings come from the grading servers, so they're stable and comparable. Your machine, your browser, and your network never enter into it — see How grading works.

Allocations are the silent tax

Each test also reports bytes allocated on the managed heap, and some puzzles enforce an allocation budget. Even when it isn't enforced, watch it: allocation pressure is what turns a theoretically-fast solution into a slow one, because every megabyte you allocate is a megabyte the garbage collector has to chase.

The usual sources, and what to do instead:

Source of garbage Cheaper approach
Building strings with + in a loop StringBuilder, or write into a Span<char>
string.Substring / Split in hot paths ReadOnlySpan<char> slices — no copy
LINQ chains over large sequences A single explicit loop when it's on the hot path
Intermediate ToList() / ToArray() Iterate the sequence once; don't materialize it
Boxing a value type (e.g. into object) Keep it generic, or use the typed API
A fresh array per iteration Allocate once outside the loop and reuse it

None of this means "never use LINQ." Use it freely where it's clear and the input is small; reach for the loop only when a hidden test says the allocation matters.

Spans and the stack

For the tight inner loops the hardest puzzles care about, Span<T> and ReadOnlySpan<T> let you slice and rewrite buffers without allocating, and stackalloc gives you a small scratch buffer that never touches the heap at all:

Span<char> buffer = stackalloc char[16];
// fill and use buffer — zero heap allocation

Keep stack buffers small and fixed-size; for anything large or variable, an array you allocate once and reuse is the right tool.

Don't optimize what you can't measure

The grader is your profiler. Make the algorithm correct, Submit, then read the two numbers it gives you — elapsed and bytes — and let them tell you whether the problem is your complexity or your allocations. Guessing wastes submissions; the panel doesn't.

Get new puzzles and .NET tips in your inbox

A short note when fresh kata land, plus the C# and performance tricks behind the grading. No spam, unsubscribe anytime.