更新履歴: StringComparison.OrdinalIgnoreCase と ToLowerInvariant/ToUpperInvariant の比較
文字列の比較に string.Equals を使うのって昔はテクニックとして有効だったのかもしれないけど、今(.NET Standard 2.1)ではもう使う意味なくね? って感じだったので色々と調べました。
【テスト環境】コンソールアプリ @ BenchmarkDotNet v0.13.12
- テストコード
- .NET Core 3.0(Unity 想定)
- .NET 8.0
Span.SequenceEqual?- 追記)OrdinalIgnoreCase と ToUpper/ToLower
- 参考)
string.Equalsのソースコード - おわりに
テストコード
BenchmarkDotNet v0.13.12
[MemoryDiagnoser]
public class Benchmark
{
string Irohani1st = "いろはにほへと ちりぬるを わかよたれそ つねならむ うゐのおくやま けふこえて あさきゆめみし ゑひもせす";
string Irohani2nd = "いろはにほへと ちりぬるを わかよたれそ つねならむ うゐのおくやま けふこえて あさきゆめみし ゑひもせす";
string TheQuickBrown = "The quick brown fox jumps over the lazy dog";
[Benchmark]
public void StringComparisonOp()
{
if (Irohani1st == TheQuickBrown)
goto EXIT;
else if (Irohani1st == Irohani2nd)
goto EXIT;
EXIT:
;
}
[Benchmark]
public void StringComparisonEqualsOrdinal()
{
if (Irohani1st.Equals(TheQuickBrown, StringComparison.Ordinal))
goto EXIT;
else if (Irohani1st.Equals(Irohani2nd, StringComparison.Ordinal))
goto EXIT;
EXIT:
;
}
[Benchmark]
public void StringComparisonEquals()
{
if (Irohani1st.Equals(TheQuickBrown))
goto EXIT;
else if (Irohani1st.Equals(Irohani2nd))
goto EXIT;
EXIT:
;
}
}
凡例
- StringComparisonOp
== - StringComparisonEqualsOrdinal
string.Equals(..., StringComparison.Ordinal) - StringComparisonEquals
string.Equals(...)
.NET Core 3.0(Unity 想定)
Core 3.0 は .NET Standard 2.1 なので Unity 想定です。Ordinal 無しの方が速いのは意外。
NET Core 3.1.32 でのベンチマーク結果
| Method | Mean | Error | StdDev | Allocated |
|---|---|---|---|---|
| StringComparisonOp | 2.644 ns | 0.0136 ns | 0.0121 ns | - |
| StringComparisonEqualsOrdinal | 3.866 ns | 0.0305 ns | 0.0254 ns | - |
| StringComparisonEquals | 2.665 ns | 0.0263 ns | 0.0233 ns | - |
[MethodImpl(MethodImplOptions.NoOptimization)] アリ
なんの意味もないですが純粋なスピード勝負? てことで。== は Equals にたどり着くまでのインライン化がなくなったとかそういう感じ?
| Method | Mean | Error | StdDev | Allocated |
|---|---|---|---|---|
| StringComparisonOp | 3.609 ns | 0.0384 ns | 0.0359 ns | - |
| StringComparisonEqualsOrdinal | 4.811 ns | 0.0619 ns | 0.0483 ns | - |
| StringComparisonEquals | 2.304 ns | 0.0115 ns | 0.0096 ns | - |
.NET 8.0
全体的に .Net Core 3 よりもかなり速くなってますね。ナノ秒レベルですが。こっちは StringComparison.Ordinal アリの Equals が速い結果に。
.NET 8.0.5 (8.0.524.21615) でのベンチマーク結果
| Method | Mean | Error | StdDev | Allocated |
|---|---|---|---|---|
| StringComparisonOp | 0.6416 ns | 0.0047 ns | 0.0040 ns | - |
| StringComparisonEqualsOrdinal | 0.7478 ns | 0.0057 ns | 0.0048 ns | - |
| StringComparisonEquals | 1.3397 ns | 0.0072 ns | 0.0067 ns | - |
MethodImplOptions.NoOptimization アリ
最適化ナシだと Ordinal アリが遅くなるのは謎。
| Method | Mean | Error | StdDev | Allocated |
|---|---|---|---|---|
| StringComparisonOp | 2.605 ns | 0.0085 ns | 0.0079 ns | - |
| StringComparisonEqualsOrdinal | 3.099 ns | 0.0229 ns | 0.0179 ns | - |
| StringComparisonEquals | 2.126 ns | 0.0152 ns | 0.0118 ns | - |
Span.SequenceEqual?
.Net 8 が速いのは SequenceEqual 使ってるからじゃね? と思いましたが、純粋に実行速度が速くなってるみたいです。CoreCLR を使うように変わったとか??
[Benchmark]
public void StringComparisonSequenceEqual()
{
if (Irohani1st.AsSpan().SequenceEqual(TheQuickBrown))
goto EXIT;
else if (Irohani1st.AsSpan().SequenceEqual(Irohani2nd))
goto EXIT;
EXIT:
;
}
.Net Core 3.0 でのベンチマーク結果
| Method | Mean | Error | StdDev | Allocated |
|---|---|---|---|---|
| StringComparisonOp | 2.594 ns | 0.0089 ns | 0.0079 ns | - |
| StringComparisonEqualsOrdinal | 4.454 ns | 0.0033 ns | 0.0030 ns | - |
| StringComparisonEquals | 3.077 ns | 0.0082 ns | 0.0068 ns | - |
| StringComparisonSequenceEqual | 2.876 ns | 0.0035 ns | 0.0028 ns | - |
2回目
| Method | Mean | Error | StdDev | Allocated |
|---|---|---|---|---|
| StringComparisonOp | 2.470 ns | 0.0201 ns | 0.0188 ns | - |
| StringComparisonEqualsOrdinal | 4.493 ns | 0.0379 ns | 0.0296 ns | - |
| StringComparisonEquals | 2.609 ns | 0.0052 ns | 0.0041 ns | - |
| StringComparisonSequenceEqual | 3.314 ns | 0.0204 ns | 0.0181 ns | - |
追記)OrdinalIgnoreCase と ToUpper/ToLower
文字列比較に Equals を使うのは元々は Java 由来の慣例らしく、C# で単純な文字列比較を行う手段としては全くの意味なし(なんならちょっと遅い)ですが、OrdinalIgnoreCase な比較であれば話は別です。
以下は何のテストになっているのか微妙な内容のベンチマークですが、string.Equals(..., StringComparison.OrdinalIgnoreCase は使える場面があったら積極的に使いたいという感じのパフォーマンスが出ています。
ToLower/ToUpper の実行頻度を低く抑えられるのであれば/可能な限り実行頻度を下げられるロジックにして
==による比較を行った方が良い結果を得られますが、それが無理なこともあるでしょう。
テストコード
string TheQuickBrownUpper = "the quick brown fox jumps over the lazy dog";
string TheQuickBrownLower = "THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG";
[Benchmark]
public void EqualsIgnoreCase()
{
if (TheQuickBrownLower.Equals(Irohani1st, StringComparison.OrdinalIgnoreCase))
goto EXIT;
else if (TheQuickBrownLower.Equals(TheQuickBrownUpper, StringComparison.OrdinalIgnoreCase))
goto EXIT;
EXIT:
;
}
[Benchmark]
public void ToLowerInvariant()
{
if (TheQuickBrownLower == Irohani1st.ToLowerInvariant())
goto EXIT;
else if (TheQuickBrownLower == TheQuickBrownUpper.ToLowerInvariant())
goto EXIT;
EXIT:
;
}
[Benchmark]
public void ToUpperInvariant()
{
if (TheQuickBrownUpper == Irohani1st.ToLowerInvariant())
goto EXIT;
else if (TheQuickBrownUpper == TheQuickBrownLower.ToUpperInvariant())
goto EXIT;
EXIT:
;
}
凡例
- EqualsIgnoreCase
string.Equals(..., StringComparison.OrdinalIgnoreCase) - ToLowerInvariant
string == other.ToLowerInvariant() - ToUpperInvariant
string == other.ToUpperInvariant()
.NET Core 3.1.32
| Method | Mean | Error | StdDev | Gen0 | Allocated |
|---|---|---|---|---|---|
| EqualsIgnoreCase | 21.534 ns | 0.0266 ns | 0.0208 ns | - | - |
| ToLowerInvariant | 154.355 ns | 1.1894 ns | 1.1126 ns | 0.0162 | 136 B |
| ToUpperInvariant | 154.377 ns | 1.0553 ns | 0.9355 ns | 0.0162 | 136 B |
| -- | |||||
| StringComparisonOp | 2.535 ns | 0.0252 ns | 0.0236 ns | - | - |
| StringComparisonEqualsOrdinal | 3.875 ns | 0.0347 ns | 0.0290 ns | - | - |
| StringComparisonEquals | 2.943 ns | 0.0692 ns | 0.0578 ns | - | - |
| StringComparisonSequenceEqual | 2.902 ns | 0.0510 ns | 0.0452 ns | - | - |
.NET 8.0.5 (8.0.524.21615)
| Method | Mean | Error | StdDev | Gen0 | Allocated |
|---|---|---|---|---|---|
| EqualsIgnoreCase | 12.3164 ns | 0.1092 ns | 0.0912 ns | - | - |
| ToLowerInvariant | 254.3058 ns | 2.3199 ns | 2.1701 ns | 0.0162 | 136 B |
| ToUpperInvariant | 255.7412 ns | 1.0547 ns | 0.9866 ns | 0.0162 | 136 B |
| -- | |||||
| StringComparisonOp | 1.0586 ns | 0.0245 ns | 0.0217 ns | - | - |
| StringComparisonEqualsOrdinal | 0.8159 ns | 0.0052 ns | 0.0040 ns | - | - |
| StringComparisonEquals | 1.1643 ns | 0.0294 ns | 0.0260 ns | - | - |
| StringComparisonSequenceEqual | 2.9543 ns | 0.0271 ns | 0.0254 ns | - | - |
参考)string.Equals のソースコード
おわりに
マイクロベンチマークなんて参考程度にとらえるべき、ですが実際のところ .NET Standard 2.1 以降(Unity 2021 以降?)なら string.Equals を単純な文字列比較を行うだけのケースで使う必要はないと思います。
Unity の場合は IL2CPP の結果も見ないとですが、まあ劇的には変わらないと思うので使わなくて良いでしょう。
以上です。お疲れ様でした。