runtime: String.IndexOfAny is slower than hand-written naive implementation on x86
See: https://github.com/NuGet/NuGet.Client/pull/1590#discussion_r130262373 and https://github.com/aspnet/Mvc/issues/5362.
On short strings and only two char arrays, String.IndexOfAny is significantly slower than a hard-coded version, and on x86 (non-RyuJIT) even slower than a hand-written naive implementation where there is no matches.
| Method | Job | Jit | Platform | Input | Mean | Error | StdDev | Scaled | ScaledSD | Faster? |
|---|---|---|---|---|---|---|---|---|---|---|
| IndexOfAny_Baseline | LegacyJitX64 | LegacyJit | X64 | This is a short string. | 3,690.3 us | 43.6378 us | 36.4396 us | 1.00 | 0.00 | |
| IndexOfAny_HandWrittenLoop_HardcodedChars | LegacyJitX64 | LegacyJit | X64 | This is a short string. | 2,276.0 us | 27.5134 us | 25.7360 us | 0.62 | 0.01 | ✓ |
| IndexOfAny_HandWrittenLoop | LegacyJitX64 | LegacyJit | X64 | This is a short string. | 6,550.1 us | 123.8535 us | 96.6966 us | 1.78 | 0.03 | |
| IndexOfAny_Baseline | LegacyJitX86 | LegacyJit | X86 | This is a short string. | 6,552.9 us | 65.9870 us | 61.7243 us | 1.00 | 0.00 | |
| IndexOfAny_HandWrittenLoop_HardcodedChars | LegacyJitX86 | LegacyJit | X86 | This is a short string. | 2,314.6 us | 48.8886 us | 71.6603 us | 0.35 | 0.01 | ✓ |
| IndexOfAny_HandWrittenLoop | LegacyJitX86 | LegacyJit | X86 | This is a short string. | 5,143.8 us | 51.0904 us | 47.7900 us | 0.79 | 0.01 | ✓ |
| IndexOfAny_Baseline | RyuJitX64 | RyuJit | X64 | This is a short string. | 3,666.7 us | 61.5387 us | 57.5634 us | 1.00 | 0.00 | |
| IndexOfAny_HandWrittenLoop_HardcodedChars | RyuJitX64 | RyuJit | X64 | This is a short string. | 2,400.2 us | 22.9174 us | 21.4370 us | 0.65 | 0.01 | ✓ |
| IndexOfAny_HandWrittenLoop | RyuJitX64 | RyuJit | X64 | This is a short string. | 6,126.5 us | 118.1990 us | 116.0872 us | 1.67 | 0.04 | |
| IndexOfAny_Baseline | LegacyJitX64 | LegacyJit | X64 | /This is a short string. | 1,196.7 us | 8.0287 us | 7.1172 us | 1.00 | 0.00 | |
| IndexOfAny_HandWrittenLoop_HardcodedChars | LegacyJitX64 | LegacyJit | X64 | /This is a short string. | 206.3 us | 1.3898 us | 1.3000 us | 0.17 | 0.00 | ✓ |
| IndexOfAny_HandWrittenLoop | LegacyJitX64 | LegacyJit | X64 | /This is a short string. | 464.2 us | 8.2176 us | 7.2847 us | 0.39 | 0.01 | ✓ |
| IndexOfAny_Baseline | LegacyJitX86 | LegacyJit | X86 | /This is a short string. | 1,668.2 us | 11.5627 us | 10.8158 us | 1.00 | 0.00 | |
| IndexOfAny_HandWrittenLoop_HardcodedChars | LegacyJitX86 | LegacyJit | X86 | /This is a short string. | 233.1 us | 0.7199 us | 0.6734 us | 0.14 | 0.00 | ✓ |
| IndexOfAny_HandWrittenLoop | LegacyJitX86 | LegacyJit | X86 | /This is a short string. | 350.4 us | 1.5306 us | 1.3568 us | 0.21 | 0.00 | ✓ |
| IndexOfAny_Baseline | RyuJitX64 | RyuJit | X64 | /This is a short string. | 1,344.7 us | 35.3044 us | 49.4918 us | 1.00 | 0.00 | |
| IndexOfAny_HandWrittenLoop_HardcodedChars | RyuJitX64 | RyuJit | X64 | /This is a short string. | 236.6 us | 1.6766 us | 1.4001 us | 0.18 | 0.01 | ✓ |
| IndexOfAny_HandWrittenLoop | RyuJitX64 | RyuJit | X64 | /This is a short string. | 384.6 us | 5.8540 us | 5.1894 us | 0.29 | 0.01 | ✓ |
| IndexOfAny_Baseline | LegacyJitX64 | LegacyJit | X64 | This is a/short string. | 2,074.6 us | 14.7860 us | 13.1074 us | 1.00 | 0.00 | |
| IndexOfAny_HandWrittenLoop_HardcodedChars | LegacyJitX64 | LegacyJit | X64 | This is a/short string. | 920.1 us | 14.5393 us | 12.1410 us | 0.44 | 0.01 | ✓ |
| IndexOfAny_HandWrittenLoop | LegacyJitX64 | LegacyJit | X64 | This is a/short string. | 3,059.0 us | 26.8631 us | 20.9729 us | 1.47 | 0.01 | |
| IndexOfAny_Baseline | LegacyJitX86 | LegacyJit | X86 | This is a/short string. | 3,821.6 us | 22.9191 us | 20.3171 us | 1.00 | 0.00 | |
| IndexOfAny_HandWrittenLoop_HardcodedChars | LegacyJitX86 | LegacyJit | X86 | This is a/short string. | 1,256.0 us | 7.6582 us | 7.1635 us | 0.33 | 0.00 | ✓ |
| IndexOfAny_HandWrittenLoop | LegacyJitX86 | LegacyJit | X86 | This is a/short string. | 3,824.8 us | 36.8087 us | 34.4309 us | 1.00 | 0.01 | |
| IndexOfAny_Baseline | RyuJitX64 | RyuJit | X64 | This is a/short string. | 2,096.2 us | 15.8659 us | 14.8410 us | 1.00 | 0.00 | |
| IndexOfAny_HandWrittenLoop_HardcodedChars | RyuJitX64 | RyuJit | X64 | This is a/short string. | 1,045.3 us | 12.6123 us | 11.7975 us | 0.50 | 0.01 | ✓ |
| IndexOfAny_HandWrittenLoop | RyuJitX64 | RyuJit | X64 | This is a/short string. | 3,116.7 us | 62.2240 us | 103.9623 us | 1.49 | 0.05 |
[MethodImpl(MethodImplOptions.NoInlining)]
public static int IndexOfAny(string value, char[] array)
{
return value.IndexOfAny(array);
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static int IndexOfAny_HandWrittenLoop_HardcodedChars(string value)
{
for (int i = 0; i < value.Length; i++)
{
char c = value[i];
if (c == '/' || c == '\\')
{
return i;
}
}
return -1;
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static int IndexOfAny_HandWrittenLoop(string value, char[] array)
{
if (array.Length == 0)
return 0;
for (int i = 0; i < value.Length; i++)
{
char c = value[i];
foreach (var ch in array)
{
if (c == ch)
return i;
}
}
return -1;
}
Benchmark is here (using BenchmarkDotNet): https://gist.github.com/davkean/dab5c068e3f907709f57a757a7e7fb3a.
@benaadams Seems to think that the code creates a probabilistic map is over kill for a two char array.
Updated: Add a few more test cases, and updated handwritten loop to handle empty chars.
About this issue
- Original URL
- State: closed
- Created 7 years ago
- Reactions: 1
- Comments: 22 (22 by maintainers)
The problem is none of these changes actually affect where we are - .NET Framework.