C#: Fastest Way To Check Last Character of a String
In this article we’ll benchmark in C#: Fastest Way To Check Last Character of a string.
There are plenty of applications and systems out there which require the need to check last character of a string. The perfect example seen all the time is needing to check if a product is “47A” versus “47C”.
That’s when this curious consultant started wondering what’s the fastest technique? Hence a showdown in C#: Fastest Way To Check Last Character of a string.
Setting the Stage:
I wrote a C# Console application to test multiple methods.
The code is written in Visual Studio 2015 targeting .Net Framework version 4.5 x64. The source code is available at the end of this blog so you can benchmark it on your own system.
Summarized, the code does the following:
1) Creates an array of strings of various lengths. The first time we create an array of 1,000 strings, then 100,000, then we go for broke at 10,000,000.
2) Loops through the array, comparing the last character of every string, using one of the methods below:
Technique |
Code Snippet |
||
|
|||
Accessing the character directly |
|
||
|
|||
|
|||
|
|||
|
3) Every time a match is found, the total number of matches is incremented by 1. This verifies every loop ends up with the same total for each method.
The code assumes there will be no exceptions.
Turn’em loose!
Let’s see what happened on my machine.
All times are indicated in seconds.milliseconds format.
Lower numbers indicate faster performance. Winning times marked in green.
Run #1:
|
1,000 |
100,000 |
10,000,000 |
String.EndsWidth |
00.0001932 |
00.0142231 |
01.4651393 |
String[String.Length – 1] |
00.0000250 |
00.0012311 |
00.0976598 |
String.Substring |
00.0001312 |
00.0118785 |
00.2248687 |
Enumeration.Last |
00.0035442 |
00.0358329 |
01.4915060 |
String.LastIndexOf |
00.0006875 |
00.0202248 |
01.8502749 |
Regex.IsMatch |
00.0034079 |
00.0889541 |
05.3940095 |
Run #2:
|
1,000 |
100,000 |
10,000,000 |
String.EndsWidth |
00.0002469 |
00.0139121 |
01.4538031 |
String[String.Length – 1] |
00.0000258 |
00.0012385 |
00.1050509 |
String.Substring |
00.0001271 |
00.0113140 |
00.2488104 |
Enumeration.Last |
00.0037960 |
00.0462239 |
01.5123975 |
String.LastIndexOf |
00.0006806 |
00.0446781 |
01.8581174 |
Regex.IsMatch |
00.0033727 |
00.0898702 |
05.1968687 |
Run #3:
|
1,000 |
100,000 |
10,000,000 |
String.EndsWidth |
00.0002006 |
00.0145041 |
01.4786785 |
String[String.Length – 1] |
00.0000229 |
00.0012147 |
00.0846341 |
String.Substring |
00.0000849 |
00.0110756 |
00.2632133 |
Enumeration.Last |
00.0033727 |
00.0483466 |
01.4936676 |
String.LastIndexOf |
00.0006071 |
00.0499634 |
01.8748690 |
Regex.IsMatch |
00.0030038 |
00.1281857 |
05.3105023 |
One Method Kicked Ass
Look at the numbers. Like, just look at the numbers.
Treating the string as an “array” and accessing the character directly is significantly faster than anything else. Most of the time, by a factor of 10! :
string[string.length - 1] == "x"
Honestly, this should not come as a real surprise to developers micro-optimizing code because direct access will always be faster because there’s no additional allocations needed nor performed.
All the other contenders had respectible and similar performances. Except for Regex. Honestly, that should come as no real surprise either.
Final Say and What You Should Use:
When you have and feel the need for speed, using string[string.length - 1] == "x"
is the only method you should be using, especially as the number of strings you need to check grows.
For easy code maintenance and readability, String.EndsWith(“x”) should suit most people just fine.
Regex has once again proved to be one of the slowest methods out there and for a simple scenario like this, shouldn’t even be considered, especially given the easy to read and understand String methods built into the .Net framework.
The Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 |
using System; using System.Collections.Generic; using System.Collections; using System.Collections.Concurrent; using System.IO; using System.Linq; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Threading; using System.Diagnostics; namespace TestApplication { class Program { static void Main(string[] args) { DateTime end; DateTime start = DateTime.Now; #region FastestWayToTestLastCharacterOfString FastestWayToTestLastCharacterOfString(1000, "x"); FastestWayToTestLastCharacterOfString(100000, "x"); FastestWayToTestLastCharacterOfString(10000000, "x"); #endregion end = DateTime.Now; Console.WriteLine(); Console.WriteLine("### Overall End Time: " + end.ToLongTimeString()); Console.WriteLine("### Overall Run Time: " + (end - start)); Console.WriteLine(); Console.WriteLine("Hit Enter to Exit"); Console.ReadLine(); } //######################################################################################################### //Test the fastest way to compare the last character of a string //NumberOfStrings - the number of strings to create to use in the comparison //EndCharToLookFor - the single character, as a string, to look for. static void FastestWayToTestLastCharacterOfString(int NumberOfStrings, string EndCharToLookFor) { if (EndCharToLookFor.Length > 1) { Console.WriteLine("The parameter EndCharToLookFor should be a single character string. It is: \"" + EndCharToLookFor + "\". Hit Enter to quit."); Console.ReadLine(); return; } Console.WriteLine(); Console.WriteLine("######## " + System.Reflection.MethodBase.GetCurrentMethod().Name); Console.WriteLine("Number of strings: " + NumberOfStrings.ToString("#,##0")); Console.WriteLine("Ending character to look for: " + EndCharToLookFor); Console.WriteLine(); char EndCharToLookForAsChar = Convert.ToChar(EndCharToLookFor); long numberOfMatches = 0; Stopwatch sw = new Stopwatch(); DateTime end = DateTime.Now; DateTime start = DateTime.Now; //the collections to loop over string[] s = new string[NumberOfStrings]; //initialize everything //strings to be searched. Completely random. Using generate password method to come up with all sorts of mixtures. Console.WriteLine("Generating strings to search."); int z = 10; for (int x = 0; x < s.Length; x++) { s[x] = System.Web.Security.Membership.GeneratePassword(z, x % 5) + (x % 5 == 0 ? EndCharToLookFor : String.Empty); //make sure there's some strings with our character we're looking for z += 1; if (z > 25) z = 10; } //s.EndsWith("x") ----------------------------------------------------------------------- Console.WriteLine("###########################################################"); Console.WriteLine("Starting s[x].EndsWith(\"" + EndCharToLookFor + "\") at: " + DateTime.Now.ToLongTimeString()); numberOfMatches = 0; sw.Restart(); for (int x = 0; x < s.Length; x++) { if (s[x].EndsWith(EndCharToLookFor)) numberOfMatches += 1; } sw.Stop(); Console.WriteLine("Finished at: " + DateTime.Now.ToLongTimeString()); Console.WriteLine("Total matches: " + numberOfMatches.ToString("#,##0")); Console.WriteLine("Time to run: " + sw.Elapsed.ToString("mm\\:ss\\.fffffff")); Thread.Sleep(500); //s[s.length - 1] == "x" ----------------------------------------------------------------------- //accessing the char directly is much faster and results in no allocations. https://www.dotnetperls.com/string-char Console.WriteLine("###########################################################"); Console.WriteLine("Starting s[x][s.length - 1] == '" + EndCharToLookFor + "' at: " + DateTime.Now.ToLongTimeString()); numberOfMatches = 0; sw.Restart(); for (int x = 0; x < s.Length; x++) { if (s[x][s[x].Length - 1] == EndCharToLookForAsChar) numberOfMatches += 1; } sw.Stop(); Console.WriteLine("Finished at: " + DateTime.Now.ToLongTimeString()); Console.WriteLine("Total matches: " + numberOfMatches.ToString("#,##0")); Console.WriteLine("Time to run: " + sw.Elapsed.ToString("mm\\:ss\\.fffffff")); Thread.Sleep(500); //s.Substring( s.Length - 1, 1 ); ----------------------------------------------------------------------- Console.WriteLine("###########################################################"); Console.WriteLine("Starting s[x].Substring( s[x].Length - 1, 1 ); at: " + DateTime.Now.ToLongTimeString()); numberOfMatches = 0; sw.Restart(); for (int x = 0; x < s.Length; x++) { if (s[x].Substring(s[x].Length - 1, 1) == EndCharToLookFor) numberOfMatches += 1; } sw.Stop(); Console.WriteLine("Finished at: " + DateTime.Now.ToLongTimeString()); Console.WriteLine("Total matches: " + numberOfMatches.ToString("#,##0")); Console.WriteLine("Time to run: " + sw.Elapsed.ToString("mm\\:ss\\.fffffff")); Thread.Sleep(500); //s.Last() ----------------------------------------------------------------------- Console.WriteLine("###########################################################"); Console.WriteLine("Starting s.Last() == '" + EndCharToLookFor + "' at: " + DateTime.Now.ToLongTimeString()); numberOfMatches = 0; sw.Restart(); for (int x = 0; x < s.Length; x++) { if (s[x].Last() == EndCharToLookForAsChar) numberOfMatches += 1; } sw.Stop(); Console.WriteLine("Finished at: " + DateTime.Now.ToLongTimeString()); Console.WriteLine("Total matches: " + numberOfMatches.ToString("#,##0")); Console.WriteLine("Time to run: " + sw.Elapsed.ToString("mm\\:ss\\.fffffff")); Thread.Sleep(500); //String.lastIndexOf() == s.Length – 1 ----------------------------------------------------------------------- Console.WriteLine("###########################################################"); Console.WriteLine("Starting String.lastIndexOf(\"" + EndCharToLookFor + "\") == s.Length – 1 at: " + DateTime.Now.ToLongTimeString()); numberOfMatches = 0; sw.Restart(); for (int x = 0; x < s.Length; x++) { if (s[x].LastIndexOf(EndCharToLookFor) == s[x].Length - 1) numberOfMatches += 1; } sw.Stop(); Console.WriteLine("Finished at: " + DateTime.Now.ToLongTimeString()); Console.WriteLine("Total matches: " + numberOfMatches.ToString("#,##0")); Console.WriteLine("Time to run: " + sw.Elapsed.ToString("mm\\:ss\\.fffffff")); Thread.Sleep(500); //Regex ----------------------------------------------------------------------- Console.WriteLine("###########################################################"); Console.WriteLine("Starting Regex at: " + DateTime.Now.ToLongTimeString()); numberOfMatches = 0; sw.Restart(); for (int x = 0; x < s.Length; x++) { if (Regex.IsMatch(s[x], EndCharToLookFor + @"$")) { numberOfMatches += 1; } } sw.Stop(); Console.WriteLine("Finished at: " + DateTime.Now.ToLongTimeString()); Console.WriteLine("Total matches: " + numberOfMatches.ToString("#,##0")); Console.WriteLine("Time to run: " + sw.Elapsed.ToString("mm\\:ss\\.fffffff")); Thread.Sleep(500); //cleanup if (s != null) Array.Clear(s, 0, s.Length); s = null; GC.Collect(1, GCCollectionMode.Forced, true); } } //class } //namespace |