Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C# ReadOnlySpan<char> vs Substring for string dissection

Tags:

string

c#

I have a fairly simple string extension method that gets called very frequently in a system I have that is doing a lot of string manipulations. I read this post (String.Substring() seems to bottleneck this code) and thought I would try the same method to see if I could find some performance by changing how I'm reading the string. My results are not quite what I was expecting (I was expecting ReadOnlySpan to provide a significant perf boost), and I'm wondering why that is. In my production code on a real run I found a very slight loss of performance.

I generated a file with ~1.15 million rows of strings with the character I care about, called the method on each, and dumped the results to console.

My results (runtime in milliseconds) are:

ReadOnlySpan.IndexOf Framework 4.7.1: 68538
ReadOnlySpan.IndexOf Core 2.1: 64486

ReadOnlySpan.SequenceEqual Framework 4.7.1: 63650
ReadOnlySpan.SequenceEqual Core 2.1: 65071

substring Framework 4.7.1: 63508
substring Core 2.1: 64125


The code (all identical from Full Framework to Core 2.1):

The calling code:

static void Main(string[] args)
{
    Stopwatch sw = new Stopwatch();
    sw.Start();

    var f = File.ReadAllLines("periods.CSV");

    foreach (string s in f)
    { Console.WriteLine(s.CountOccurrences(".")); }

    sw.Stop();
    Console.WriteLine("Done in " + sw.ElapsedMilliseconds + " ms");
    Console.ReadKey();
}


The original substring form of my method:

public static int CountOccurrencesSub(this string val, string searchFor)
{
    if (string.IsNullOrEmpty(val) || string.IsNullOrEmpty(searchFor))
    { return 0; }

    int count = 0;

    for (int x = 0; x <= val.Length - searchFor.Length; x++)
    {
        if (val.Substring(x, searchFor.Length) == searchFor)
        { count++; }
    }

    return count;
}


The ReadOnlySpan version (which I've tested with both IndexOf and SequenceEqual for equality checks):

public static int CountOccurrences(this string val, string searchFor)
{
    if (string.IsNullOrEmpty(val) || string.IsNullOrEmpty(searchFor))
    { return 0; }

    int count = 0;

    ReadOnlySpan<char> vSpan = val.AsSpan();
    ReadOnlySpan<char> searchSpan = searchFor.AsSpan();

    for (int x = 0; x <= vSpan.Length - searchSpan.Length; x++)
    {
        if (vSpan.Slice(x, searchSpan.Length).SequenceEqual(searchSpan))
        { count++; }
    }

    return count;
}


Does the equality comparison do an allocation in the methods I'm calling, and therefor no boost? Is this just not a good application for ReadOnlySpan? Am I just plain old missing something?

like image 504
Mardoch Avatar asked Aug 15 '18 18:08

Mardoch


People also ask

What C is used for?

C programming language is a machine-independent programming language that is mainly used to create many types of applications and operating systems such as Windows, and other complicated programs such as the Oracle database, Git, Python interpreter, and games and is considered a programming foundation in the process of ...

What is the full name of C?

In the real sense it has no meaning or full form. It was developed by Dennis Ritchie and Ken Thompson at AT&T bell Lab. First, they used to call it as B language then later they made some improvement into it and renamed it as C and its superscript as C++ which was invented by Dr.

Is C language easy?

C is a general-purpose language that most programmers learn before moving on to more complex languages. From Unix and Windows to Tic Tac Toe and Photoshop, several of the most commonly used applications today have been built on C. It is easy to learn because: A simple syntax with only 32 keywords.

What is C in C language?

What is C? C is a general-purpose programming language created by Dennis Ritchie at the Bell Laboratories in 1972. It is a very popular language, despite being old. C is strongly associated with UNIX, as it was developed to write the UNIX operating system.


1 Answers

Although I'm a bit late to the party but I think I can still add relevant information to this topic.

First of all, some words about the other posters' measurements.

OP's results are clearly incorrect. As it was pointed out in the comments, the I/O operations completely distorts the stats.

The poster of the accepted answer is on the right track. His method eliminates the slow I/O operations and focuses clearly on the subject of the benchmark. However, he doesn't mention the environment (especially the .NET runtime) used and his "warm-up method" is rather debatable.

Performance measurement is a really tricky business, it's very hard to get it right. I wouldn't even try to code it myself if I wanted to get valid results. So I decided to check out this issue using the widely-adopted Benchmark.NET library. To make this all more interesting I added a third candidate to the mix. This implementation uses String.CompareOrdinal for occurence counting and I expected pretty good results from it.

The benchmark

Before the measurement starts (at the global setup stage), I generate 1,000,000 lines of lorem ipsum text. This data is used throughout the measurement.

Each method is exercised with 1,000 and 1,000,000 lines and with a shorter (5 characters long) and a longer (39 characters long) search text.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

namespace MyBenchmarks
{
#if NETCOREAPP2_1
    [CoreJob]
#else
    [ClrJob]
#endif
    [RankColumn, MarkdownExporterAttribute.StackOverflow]
    public class Benchmark
    {
        static readonly string[] words = new[]
        {
            "lorem", "ipsum", "dolor", "sit", "amet", "consectetuer",
            "adipiscing", "elit", "sed", "diam", "nonummy", "nibh", "euismod",
            "tincidunt", "ut", "laoreet", "dolore", "magna", "aliquam", "erat"
        };

        // borrowed from greg (https://stackoverflow.com/questions/4286487/is-there-any-lorem-ipsum-generator-in-c)
        static IEnumerable<string> LoremIpsum(Random random, int minWords, int maxWords, int minSentences, int maxSentences, int numLines)
        {
            var line = new StringBuilder();
            for (int l = 0; l < numLines; l++)
            {
                line.Clear();
                var numSentences = random.Next(maxSentences - minSentences) + minSentences + 1;
                for (int s = 0; s < numSentences; s++)
                {
                    var numWords = random.Next(maxWords - minWords) + minWords + 1;
                    line.Append(words[random.Next(words.Length)]);
                    for (int w = 1; w < numWords; w++)
                    {
                        line.Append(" ");
                        line.Append(words[random.Next(words.Length)]);
                    }
                    line.Append(". ");
                }
                yield return line.ToString();
            }
        }

        string[] lines;

        [Params(1000, 1_000_000)]
        public int N;

        [Params("lorem", "lorem ipsum dolor sit amet consectetuer")]
        public string SearchValue;

        [GlobalSetup]
        public void GlobalSetup()
        {
            lines = LoremIpsum(new Random(), 6, 8, 2, 3, 1_000_000).ToArray();
        }

        public static int CountOccurrencesSub(string val, string searchFor)
        {
            if (string.IsNullOrEmpty(val) || string.IsNullOrEmpty(searchFor))
            { return 0; }

            int count = 0;

            for (int x = 0; x <= val.Length - searchFor.Length; x++)
            {
                if (val.Substring(x, searchFor.Length) == searchFor)
                { count++; }
            }

            return count;
        }

        public static int CountOccurrences(string val, string searchFor)
        {
            if (string.IsNullOrEmpty(val) || string.IsNullOrEmpty(searchFor))
            { return 0; }

            int count = 0;

            ReadOnlySpan<char> vSpan = val.AsSpan();
            ReadOnlySpan<char> searchSpan = searchFor.AsSpan();

            for (int x = 0; x <= vSpan.Length - searchSpan.Length; x++)
            {
                if (vSpan.Slice(x, searchSpan.Length).SequenceEqual(searchSpan))
                { count++; }
            }

            return count;
        }

        public static int CountOccurrencesCmp(string val, string searchFor)
        {
            if (string.IsNullOrEmpty(val) || string.IsNullOrEmpty(searchFor))
            { return 0; }

            int count = 0;

            for (int x = 0; x <= val.Length - searchFor.Length; x++)
            {
                if (string.CompareOrdinal(val, x, searchFor, 0, searchFor.Length) == 0)
                { count++; }
            }

            return count;
        }


        [Benchmark(Baseline = true)]
        public int Substring()
        {
            int occurences = 0;
            for (var i = 0; i < N; i++)
                occurences += CountOccurrencesSub(lines[i], SearchValue);
            return occurences;
        }

        [Benchmark]
        public int Span()
        {
            int occurences = 0;
            for (var i = 0; i < N; i++)
                occurences += CountOccurrences(lines[i], SearchValue);
            return occurences;
        }

        [Benchmark]
        public int Compare()
        {
            int occurences = 0;
            for (var i = 0; i < N; i++)
                occurences += CountOccurrencesCmp(lines[i], SearchValue);
            return occurences;
        }
    }

    public class Program
    {
        public static void Main(string[] args)
        {
            BenchmarkRunner.Run<Benchmark>();
        }
    }
}

The results

NET Core 2.1

BenchmarkDotNet=v0.11.0, OS=Windows 7 SP1 (6.1.7601.0)
Intel Core i3-4360 CPU 3.70GHz (Haswell), 1 CPU, 4 logical and 2 physical cores
Frequency=3604970 Hz, Resolution=277.3948 ns, Timer=TSC
.NET Core SDK=2.1.400
  [Host] : .NET Core 2.1.2 (CoreCLR 4.6.26628.05, CoreFX 4.6.26629.01), 64bit RyuJIT
  Core   : .NET Core 2.1.2 (CoreCLR 4.6.26628.05, CoreFX 4.6.26629.01), 64bit RyuJIT

Job=Core  Runtime=Core  

    Method |       N |          SearchValue |           Mean |           Error |          StdDev |         Median | Scaled | ScaledSD | Rank |
---------- |-------- |--------------------- |---------------:|----------------:|----------------:|---------------:|-------:|---------:|-----:|
 Substring |    1000 |                lorem |     2,149.4 us |       2.2763 us |       2.1293 us |     2,149.4 us |   1.00 |     0.00 |    3 |
      Span |    1000 |                lorem |       555.5 us |       0.2786 us |       0.2470 us |       555.5 us |   0.26 |     0.00 |    1 |
   Compare |    1000 |                lorem |     1,471.8 us |       0.2133 us |       0.1891 us |     1,471.8 us |   0.68 |     0.00 |    2 |
           |         |                      |                |                 |                 |                |        |          |      |
 Substring |    1000 | lorem(...)etuer [39] |     2,128.7 us |       1.0414 us |       0.9741 us |     2,128.6 us |   1.00 |     0.00 |    3 |
      Span |    1000 | lorem(...)etuer [39] |       388.9 us |       0.0440 us |       0.0412 us |       388.9 us |   0.18 |     0.00 |    1 |
   Compare |    1000 | lorem(...)etuer [39] |     1,215.6 us |       0.7016 us |       0.6220 us |     1,215.5 us |   0.57 |     0.00 |    2 |
           |         |                      |                |                 |                 |                |        |          |      |
 Substring | 1000000 |                lorem | 2,239,510.8 us | 241,887.0796 us | 214,426.5747 us | 2,176,083.7 us |   1.00 |     0.00 |    3 |
      Span | 1000000 |                lorem |   558,317.4 us |     447.3105 us |     418.4144 us |   558,338.9 us |   0.25 |     0.02 |    1 |
   Compare | 1000000 |                lorem | 1,471,941.2 us |     190.7533 us |     148.9276 us | 1,471,955.8 us |   0.66 |     0.05 |    2 |
           |         |                      |                |                 |                 |                |        |          |      |
 Substring | 1000000 | lorem(...)etuer [39] | 2,350,820.3 us |  46,974.4500 us | 115,229.1264 us | 2,327,187.2 us |   1.00 |     0.00 |    3 |
      Span | 1000000 | lorem(...)etuer [39] |   433,567.7 us |  14,445.7191 us |  42,593.5286 us |   417,333.4 us |   0.18 |     0.02 |    1 |
   Compare | 1000000 | lorem(...)etuer [39] | 1,299,065.2 us |  25,474.8504 us |  46,582.2045 us | 1,296,892.8 us |   0.55 |     0.03 |    2 |  

NET Framework 4.7.2

BenchmarkDotNet=v0.11.0, OS=Windows 7 SP1 (6.1.7601.0)
Intel Core i3-4360 CPU 3.70GHz (Haswell), 1 CPU, 4 logical and 2 physical cores
Frequency=3604960 Hz, Resolution=277.3956 ns, Timer=TSC
  [Host] : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 64bit RyuJIT-v4.7.3062.0
  Clr    : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 64bit RyuJIT-v4.7.3062.0

Job=Clr  Runtime=Clr  

    Method |       N |          SearchValue |           Mean |          Error |          StdDev |         Median | Scaled | ScaledSD | Rank |
---------- |-------- |--------------------- |---------------:|---------------:|----------------:|---------------:|-------:|---------:|-----:|
 Substring |    1000 |                lorem |     2,025.8 us |      2.4639 us |       1.9237 us |     2,025.4 us |   1.00 |     0.00 |    3 |
      Span |    1000 |                lorem |     1,216.6 us |      4.2994 us |       4.0217 us |     1,217.8 us |   0.60 |     0.00 |    1 |
   Compare |    1000 |                lorem |     1,295.5 us |      5.2427 us |       4.6475 us |     1,293.1 us |   0.64 |     0.00 |    2 |
           |         |                      |                |                |                 |                |        |          |      |
 Substring |    1000 | lorem(...)etuer [39] |     1,939.5 us |      0.4428 us |       0.4142 us |     1,939.3 us |   1.00 |     0.00 |    3 |
      Span |    1000 | lorem(...)etuer [39] |       944.9 us |      2.6648 us |       2.3622 us |       944.7 us |   0.49 |     0.00 |    1 |
   Compare |    1000 | lorem(...)etuer [39] |     1,002.0 us |      0.2475 us |       0.2067 us |     1,002.1 us |   0.52 |     0.00 |    2 |
           |         |                      |                |                |                 |                |        |          |      |
 Substring | 1000000 |                lorem | 2,065,805.7 us |  2,009.2139 us |   1,568.6619 us | 2,065,555.1 us |   1.00 |     0.00 |    3 |
      Span | 1000000 |                lorem | 1,209,976.4 us |  6,238.6091 us |   5,835.5982 us | 1,206,554.3 us |   0.59 |     0.00 |    1 |
   Compare | 1000000 |                lorem | 1,303,321.8 us |  1,257.7418 us |   1,114.9552 us | 1,303,330.1 us |   0.63 |     0.00 |    2 |
           |         |                      |                |                |                 |                |        |          |      |
 Substring | 1000000 | lorem(...)etuer [39] | 2,085,652.9 us | 62,651.7471 us | 168,309.8501 us | 1,973,522.2 us |   1.00 |     0.00 |    3 |
      Span | 1000000 | lorem(...)etuer [39] |   958,421.2 us |  3,703.5508 us |   3,464.3034 us |   958,324.9 us |   0.46 |     0.03 |    1 |
   Compare | 1000000 | lorem(...)etuer [39] | 1,007,936.8 us |    802.1730 us |     750.3531 us | 1,007,680.3 us |   0.49 |     0.04 |    2 |

Conclusion

It's clear that there is a solid performance gain using Span<T>. What's somewhat surprising that it's 4-5x on .NET Core and only 2x on .NET Framework. What reasons could be behind that? Does anyone have a clue?

String.CompareOrdinal performs pretty good as well. I expected somewhat better results because theoretically it's just the same byte by byte compare but it's not bad at all. On .NET Framework it's a viable option by all means.

The length of the search string (except for extremities, of course) doesn't seem to have too much impact on the results.

like image 178
Adam Simon Avatar answered Nov 15 '22 18:11

Adam Simon