The PowerShell $N/$n bug counted zero primes; fixed pwsh was 2.5x faster
In this article
On 2026-07-30, the buggy prime count returned “0 3” under pwsh 7.6.4 and “0 4” under powershell.exe 5.1.
The measurement date was July 30, 2026.
There were zero primes and an execution time of 3ms or 4ms. I intended to count primes below 200,000, so the result clearly was not working. When I first saw it, it felt less like PowerShell's loop was fast and more like something had broken in the measurement code. Stopping to check at that point was fortunate.
The cause was simple: PowerShell variable names are case-insensitive, but I used $N for the upper limit and $n for the loop variable. It was a direct transfer of a C or Python habit. They look like different variables in C and Python, but PowerShell treats them as the same variable.
As soon as $n=2 was assigned, the upper limit $N also became 2. Therefore $n -lt $N was false on the first check, and the loop ended without running once. It took me a little while to notice. When a result is too fast, you should inspect whether the output is correct before celebrating. Reversing that order makes a benchmark easy to break.
The broken code and the corrected code
The first version was roughly this.
$N=200000
$sw=[Diagnostics.Stopwatch]::StartNew()
$c=0
for($n=2;$n -lt $N;$n++){
# primality test by trial division
}
Write-Output "$c $($sw.ElapsedMilliseconds)"
In PowerShell, $N and $n collide. In the corrected version I changed the upper limit to $limit. The algorithm did not change; only the variable name did.
$limit=200000
$sw=[Diagnostics.Stopwatch]::StartNew()
$c=0
for($n=2;$n -lt $limit;$n++){
# primality test by trial division
}
Write-Output "$c $($sw.ElapsedMilliseconds)"
The left side of the output is the number of primes, and the right side is the milliseconds returned by the script's Stopwatch. I also measured wall time from outside, but here I mainly looked at script-internal elapsed time. I had measured process startup separately, and including it would mix in a different question. This run focuses on execution speed after the loop actually starts.
The environment was a Surface Pro 11th Edition running Windows 11 Pro ARM64. The two targets were pwsh 7.6.4 and Windows PowerShell 5.1, both running the same operation.
The speed of zero is not speed
I kept the buggy and corrected versions separate because mixing a broken result that looks fast with a result that produces the right answer makes it unclear where comparison becomes valid. The failure itself is worth preserving too. I used only the numbers present in the measured JSON.
| Type | Runtime | Wall time | Output |
|---|---|---|---|
| Buggy | pwsh 7.6.4 | 1062ms | 0 3 |
| Buggy | powershell.exe 5.1 | 382ms | 0 4 |
| Corrected | pwsh 7.6.4 | 6221ms / 5857ms | 17984 5171 / 17984 5232 |
| Corrected | powershell.exe 5.1 | 13527ms / 15866ms | 17984 13051 / 17984 15077 |
The buggy 3ms and 4ms did not mean that the operation was fast; it meant that nothing was processed. External wall time was 1062ms for pwsh and 382ms for powershell.exe, so starting the process and running the script did take some time. Even so, the important loop ended zero times, before prime testing began. Looking only at startup made it seem as if something had run, while the script body had already exited. That mismatch was the most confusing part.
After the fix, both returned 17,984 primes, and comparison finally became valid. Check the answer first, then look at speed. That is the natural order. pwsh took 5171ms and 5232ms, while powershell.exe took 13051ms and 15077ms. For actual execution of the same script, pwsh was 2.5–2.9x faster. If you carry the startup-time impression into this comparison, you will misread it.
This was unexpected. When I previously measured startup alone, Windows PowerShell 5.1 was faster, so I thought there was a reason to favor powershell.exe for short automation. But for a computation that really runs a loop, the difference while executing becomes larger than the startup difference.
What caught me
The most dangerous part was that the buggy version looked overwhelmingly fast if you watched only time. Picking up the 3ms or 4ms figures alone makes it sound as if PowerShell finished counting primes instantly.
In reality, the answer was zero. The question should have been why the answer disappeared, not why it was fast. If I had missed that, I would have written a completely wrong article. When a benchmark produces a value that is “too fast,” question the output itself first. That is the whole lesson this time.
I had probably seen the fact that PowerShell variable names are case-insensitive before. I could not recall it when I casually put $N and $n next to each other. There is no error or warning, so the failure is quiet—too quiet to notice if you watch time alone.
The $N/$n collision is especially easy to hit with short shell-script variable names. In benchmarks, it is common to use N as the upper limit and n as the current value, but PowerShell should avoid that. A meaningful name such as $limit substantially reduces this class of bug.
Variation remains
The corrected powershell.exe runs were 13051ms and 15077ms, a fairly large spread of about 15%. There were only two runs, so I could not discuss anything like a confidence interval.
The pwsh runs, 5171ms and 5232ms, were close, but these were also only two records. They may simply have happened to align. It is safest to say only that pwsh was clearly faster in these two runs.
Personally, I had thought of PowerShell 7 as heavy to start. These results showed that extending that impression to execution speed would be sloppy. “powershell.exe for a short one-shot, pwsh for a longer calculation” fits the local measurements better.
How I would use them differently
I made the conclusion conditional. The original code did not work because of a bug, but after changing the variable name it reached 17,984 primes. The measurement had to be redone, and the corrected version exposed a difference in execution speed.
For future small PowerShell benchmarks, I will first compare the output with a fixed expected value. I will not create the speed table first. Check that results such as zero items or zero records did not appear, then move to time comparison.
Variable names should not be too short either. $N and $n are the same thing in PowerShell, so use $limit for the upper bound and something like $candidate for the current value. A slightly longer name is better than measurement code silently breaking.
For a stronger check, I would run at least 10 repetitions and look at the median. The 15% difference on powershell.exe may be ordinary variation or may be a fluctuation that appears more often on Windows PowerShell 5.1; these two runs are not enough to decide.