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>orDictionary<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.