Faster to Trim during or after assignment in C# .Net
Does it make a difference if you Trim() during or after assigning a value to a variable in C# .Net? Which was is faster? Which object, String or StringBuilder, is faster?
This test stemmed from a project with production code that processed thousands of strings. While doing so, it trimmed each one of its whitespace.
Here’s the gist of the code:
1 2 3 4 5 6 7 8 9 |
while (do some TestReadingAndProcessingLinesFromFile_DoStuff) { string temp_str; //do some stuff temp_str = dr["textcolumn"]; //get the other fields and do stuff with them temp_str = temp_str.Trim(); //do some stuff with temp_str } |
Then as I was rereading the code one day, it occurred to me to try it like this:
1 2 3 4 5 6 7 8 |
while (do some TestReadingAndProcessingLinesFromFile_DoStuff) { string temp_str; //do some stuff temp_str = dr["textcolumn"].ToString().Trim(); //get the other fields and do stuff with them //do some stuff with temp_str } |
as it should hypothetically use less resources since it’s not first allocating temp_str, and then reallocating temp_str for the trim() operation.
So that’s when this curious consultant started wondering… is it faster to trim during or after assignment in C# .Net?
The Set Up:
The code is written in Visual Studio 2012 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 if you wish.
In a nutshell, the code does the following:
- Creates an array of Guids, converted to strings, and padded on the left, right, or both sides with white space.
- Assigns the string to either a string or StringBuilder object:
- a. while performing an inline Trim()
- b. Or doing the trim after the assignment
Only single-threaded for-loops are used because I’m just interested in raw conversion speeds, not the fastest way to loop through arrays.
The exe file was run on Windows 7 64-bit with 16 GB memory.
The test was run for the following comparisons:
- Trimming 2,147, 214,748, and 21,474,836 strings during the assignment to another string.
- Trimming 2,147, 214,748, and 21,474,836 strings after the initial assignment to another string.
- Trimming 2,147, 214,748, and 21,474,836 strings during the assignment to a StringBuilder object.
- Trimming 2,147, 214,748, and 21,474,836 strings after the initial assignment to a StringBuilder object.
The Runs:
Before starting, my hypothesis was that I expected the trim during the initial assignment to run faster with both strings and StringBuilder objects.
Let’s see what happened on my machine.
All times are indicated in minutes:seconds.milliseconds format. Lower numbers indicate faster runtime performance.
Winners are highlighted in green.
Run #1 |
Number of string assignments: |
||
|
2,147 |
214,748 |
21,474,836 |
——– Target is a STRING ——– |
|||
Trim() during assignment |
00:00.00 |
00:00.0468001 |
00:21.8329953 |
Trim() after initial assignment |
00:00.00 |
00:00.0468001 |
00:19.3908340 |
——– Target is a STRINGBUILDER ——– |
|||
Trim() during assignment |
00.00.00 |
00:00.1092001 |
02:04.9167826 |
Trim() after initial assignment |
00:00.00 |
00:00.2028004 |
00:47.0184825 |
Run #2 |
Number of string assignments: |
||
|
2,147 |
214,748 |
21,474,836 |
——– Target is a STRING ——– |
|||
Trim() during assignment |
00:00.00 |
00:00.0468000 |
00:21.1692372 |
Trim() after initial assignment |
00:00.00 |
00:00.0468001 |
00:18.7824330 |
——– Target is a STRINGBUILDER ——– |
|||
Trim() during assignment |
00.00.00 |
00:00.1092002 |
02:07.7918767 |
Trim() after initial assignment |
00:00.00 |
00:00.2028003 |
00:47.5332834 |
The Results:
Wow talk about a flip-flop depending on how many strings or StringBuilder objects being manipulated. So it seems from the results for both a string and StringBuilder it’s best to:
- Do either up to a few thousand trims
- Only do the trim during an assignment when a few hundred thousand are involved
- A million or more? Definitely should use the .Trim() after the assignment.
Even more surprising is why does the StringBuilder object take soooooooo loooooooong with the Trim() during an assignment?
And why does a Stringbuilder take soooooo looooong to trim afterwards?
Can any .Net coders shed light on this?
With results like these, one has to question why use a StringBuilder at all?
In Summary:
On my system, unless someone spots a flaw in my test code, it appears the time difference is so negligible when the final destination is a string object it doesn’t matter which way you go. This method holds its ground for both a very few .Trim operations (which I suspect most programmers face), or several million at once. So unless you need those 2 seconds over 21+ million strings, do what you want.
It really comes down to personal taste and how much one cares about code readability versus speed.
For StringBuilder objects of more than just a few thousand…don’t even bother. Convert them to a string, do what you got to do, then convert them back if you really need a StringBuilder.
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 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 |
using System; using System.Text; namespace TestApplication { class Program { static void Main(string[] args) { DateTime end; DateTime start = DateTime.Now; Console.WriteLine("### Overall Start Time: " + start.ToLongTimeString()); Console.WriteLine(); IsItFasterToTrimDuringOrAfterAssignment('S', 'D', (int.MaxValue / 1000000)); IsItFasterToTrimDuringOrAfterAssignment('S', 'D', (int.MaxValue / 10000)); IsItFasterToTrimDuringOrAfterAssignment('S', 'D', (int.MaxValue / 100)); IsItFasterToTrimDuringOrAfterAssignment('S', 'A', (int.MaxValue / 1000000)); IsItFasterToTrimDuringOrAfterAssignment('S', 'A', (int.MaxValue / 10000)); IsItFasterToTrimDuringOrAfterAssignment('S', 'A', (int.MaxValue / 100)); IsItFasterToTrimDuringOrAfterAssignment('B', 'D', (int.MaxValue / 1000000)); IsItFasterToTrimDuringOrAfterAssignment('B', 'D', (int.MaxValue / 10000)); IsItFasterToTrimDuringOrAfterAssignment('B', 'D', (int.MaxValue / 100)); IsItFasterToTrimDuringOrAfterAssignment('B', 'A', (int.MaxValue / 1000000)); IsItFasterToTrimDuringOrAfterAssignment('B', 'A', (int.MaxValue / 10000)); IsItFasterToTrimDuringOrAfterAssignment('B', 'A', (int.MaxValue / 100)); 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(); } //############################################################### //Is it faster to do a TRIM during an assignment or after? //StringOrStringBuilder = 'S'tring or string'B'uilder //DuringOrAfter = 'D'uring or 'A'fter static void IsItFasterToTrimDuringOrAfterAssignment(char StringOrStringBuilder, char DuringOrAfter, int numberOfAssignments) { DateTime end = DateTime.Now; DateTime start = DateTime.Now; String[] s = new String[numberOfAssignments]; //the strings to assign to StringBuilder[] sb = new StringBuilder[numberOfAssignments]; //the stringbuilder objects to assign to string[] a = new String[numberOfAssignments]; //the array containing all the strings to trim try { Console.WriteLine("######## " + System.Reflection.MethodBase.GetCurrentMethod().Name); Console.WriteLine("######## Number of assignments: " + numberOfAssignments.ToString("#,##0")); Console.WriteLine("###########################################################"); Console.WriteLine(); //set up the array of strings to use Console.WriteLine("###########################################################"); Console.WriteLine("Starting creating strings to trim: " + numberOfAssignments.ToString("#,##0") + " strings"); Console.WriteLine("###########################################################"); for (int x = 0; x < numberOfAssignments; x++) { switch (x % 3) { //let's add padding at both ends to give it a challenge case 0: a[x] = string.Empty.PadLeft((x % 17)) + (new Guid()).ToString(); break; case 1: a[x] = (new Guid()).ToString() + string.Empty.PadRight(x % 17); break; case 2: a[x] = string.Empty.PadLeft(x % 13) + (new Guid()).ToString() + string.Empty.PadRight(x % 13); break; } } Console.WriteLine("Completed creating strings to trim " + numberOfAssignments.ToString("#,##0") + " strings"); Console.WriteLine("###########################################################"); Console.WriteLine(); Console.WriteLine("###########################################################"); if (StringOrStringBuilder == 'S') { //Do it the long way so evaluating the if statement each round doesn't affect time if (DuringOrAfter == 'D') { Console.WriteLine("Starting Trim of Strings During assignment: " + DateTime.Now.ToLongTimeString()); start = DateTime.Now; for (int x = 0; x < numberOfAssignments; x++) { s[x] = a[x].Trim(); } end = DateTime.Now; Console.WriteLine("Finished at: " + end.ToLongTimeString()); Console.WriteLine("Time: " + (end - start)); Console.WriteLine(); } else if (DuringOrAfter == 'A') { Console.WriteLine("Starting Trim of Strings After assignment: " + DateTime.Now.ToLongTimeString()); start = DateTime.Now; for (int x = 0; x < numberOfAssignments; x++) { s[x] = a[x]; s[x] = s[x].Trim(); } end = DateTime.Now; Console.WriteLine("Finished at: " + end.ToLongTimeString()); Console.WriteLine("Time: " + (end - start)); Console.WriteLine(); } else { Console.WriteLine("Unknown value for parametered \"DuringOrAfter\". Valid values are 'D' or 'A'!\n"); } } else if (StringOrStringBuilder == 'B') { if (DuringOrAfter == 'D') { Console.WriteLine("Starting Trim of StringBuilders During assignment: " + DateTime.Now.ToLongTimeString()); start = DateTime.Now; for (int x = 0; x < numberOfAssignments; x++) { sb[x] = new StringBuilder(a[x].Trim()); } end = DateTime.Now; Console.WriteLine("Finished at: " + end.ToLongTimeString()); Console.WriteLine("Time: " + (end - start)); Console.WriteLine(); } else if (DuringOrAfter == 'A') { Console.WriteLine("Starting Trim of StringBuilders After assignment: " + DateTime.Now.ToLongTimeString()); start = DateTime.Now; int index = 0; for (int x = 0; x < numberOfAssignments; x++) { sb[x] = new StringBuilder(a[x]); //Remove all blankspace from the start. for (int z = 0; z < sb[x].Length; z++) { //get the first char at the beginning which isn't whitespace if (!char.IsWhiteSpace(sb[x][z])) { index = z; break; } } //remove the whitespace from the beginning sb[x].Remove(0, index + 1); //Remove all blankspace from the end. for (int z = sb[x].Length - 1; z >= 0; z--) { //get the first char at the end which isn't whitespace if (!char.IsWhiteSpace(sb[x][z])) { index = z; break; } } //remove the whitespace from the end sb[x].Remove(index, sb[x].Length - 1 - index); } end = DateTime.Now; Console.WriteLine("Finished at: " + end.ToLongTimeString()); Console.WriteLine("Time: " + (end - start)); Console.WriteLine(); } else { Console.WriteLine("Unknown value for parametered \"DuringOrAfter\". Valid values are 'D' or 'A'!\n"); } } else { Console.WriteLine("Unknown value for parametered \"StringOrStringBuilder\". Valid values are 'S' or 'B'!\n"); } } catch (Exception e) { Console.WriteLine("An EXCEPTION occurred as follows: " + Environment.NewLine + Environment.NewLine + e.ToString()); } finally { //clean up Array.Clear(s, 0, s.Length); s = null; Array.Clear(a, 0, a.Length); a = null; Array.Clear(sb, 0, sb.Length); sb = null; } GC.Collect(); Thread.Sleep(1000); //just to give other things a chance to do something. Console.WriteLine(); } } } |