Performance isn’t just about making users happy, though. It makes our lives as developers way better too. Think about your typical day, you’re constantly running your code, debugging, testing, and then doing it all over again. When your code runs faster, you spend less time waiting and more time actually coding. Nobody enjoys staring at a spinning wheel while your tests run or the debugger loads up. Those small delays mess with your flow and make development less fun.
I was thinking of using semgrep to catch a lot of small easy to fix performance improvements. So I want to just share the rules I make, so maybe somebody else can use them to.
These rules will be covering the small cases, but sometimes the performance issues can be a death of a thousand cuts. Garbage Collection can be a real killer for performance, so a lot of the rules will try to cover things where there is some alternative that requires less or no allocations.
Stop Converting Strings Just to Compare Them
Strings are everywhere in our code, and the way we compare them can make a surprising difference in performance. Here’s our first rule that catches a really common mistake.
We’ve all done this at some point:
// The slow way
if (someString.ToLower().Equals(otherString))
{
// Do something
}
Or maybe this version:
// Also slow
if (someString.ToUpper().Equals(otherString))
{
// Do something
}
What’s wrong with this? A few things:
- It creates a whole new string just for the comparison
- It wastes memory for this temporary string
- It has to convert every character before it even starts comparing
The Better Way
There’s a much faster way to do the same thing in C#:
if (String.Equals(someString, otherString, StringComparison.OrdinalIgnoreCase))
{
// Do something
}
This skips creating new strings completely and just does the comparison directly.
The Semgrep Rule
Here’s the rule I made to catch this in your code:
rules:
- id: csharp-inefficient-string-comparison
patterns:
- pattern-either:
- pattern: $STR.ToLower().Equals($OTHER)
- pattern: $STR.ToLowerInvariant().Equals($OTHER)
- pattern: $STR.ToUpper().Equals($OTHER)
- pattern: $STR.ToUpperInvariant().Equals($OTHER)
- pattern-not: String.Equals($STR, $OTHER, StringComparison.OrdinalIgnoreCase)
message: >
Inefficient string comparison. Use String.Equals(s1, s2, StringComparison.OrdinalIgnoreCase)
instead of ToLower()/ToUpper().Equals() for better performance and clarity.
languages: [csharp]
severity: WARNING
metadata:
category: performance
subcategory:
- easyfix
- strings
This catches all four ways people typically do the slow comparison, but it won’t bug you if you’re already doing it the right way.
How Much Faster Is It Really?
I ran some benchmarks to see exactly how big the difference is:
| Method | Mean | Allocated |
|---------------------------------------------- |-----------:|----------:|
| StringEquals_OrdinalIgnoreCase_SameIgnoreCase | 0.0138 ns | - |
| StringEquals_OrdinalIgnoreCase_Different | 0.0327 ns | - |
| ToUpperInvariant_Different | 16.4798 ns | 56 B |
| ToLowerInvariant_Different | 16.6340 ns | 56 B |
| ToLowerInvariant_SameIgnoreCase | 17.4380 ns | 56 B |
| ToUpper_Different | 18.7223 ns | 56 B |
| ToLower_Different | 19.5512 ns | 56 B |
| ToLower_SameIgnoreCase | 21.0037 ns | 56 B |
| ToUpperInvariant_SameIgnoreCase | 29.8586 ns | 112 B |
| ToUpper_SameIgnoreCase | 34.5027 ns | 112 B |
Why Should You Care?
“But it’s just nanoseconds,” you might say. True, but:
- In a busy app, you might do these comparisons millions of times
- Every little memory allocation makes the garbage collector work harder
- These tiny slowdowns add up across your whole codebase
This is just the first of several performance-boosting rules I’m working on. If you add these to your workflow, you’ll catch these speed bumps before they slow down your code.
Want to see all the rules and benchmarks? Check out csharp-semgrep-performance on GitHub.
Leave a Reply