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)

Most upvoted comments

The problem is none of these changes actually affect where we are - .NET Framework.