I timed a few ways to look for a string in a string array (with 82 elements in the example) in a case sensitive manner plus ignoring case.
I am kind of suprised, that the plain for loops are faster than all other variants.
- Sure, there are no checks, but are there any other flaws here?
- Does Linq not do some vectorization behind the scenes?
- Can I w/o concerns use these loops?
- Any other notes?
Code
class Program
{
static void Main(string[] args)
{
Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture;
StringArrayContains();
Console.Read();
}
public static void StringArrayContains()
{
// Init timing
var watch = Start();
watch.Stop();
watch.Reset();
// Setup vars
string query = "big";
string[] stringArray = GetStringArray();
bool contains = false;
int times = (int)1e6;
Console.WriteLine("Case Sensitive Search");
// Loop with case sensitive equal
watch.Start();
for (int i = 0; i < times; i++)
{
for (int j = 0; j < stringArray.Length; j++)
{
if (string.CompareOrdinal(query, stringArray[j]) == 0)
{
contains = true;
break;
}
}
}
Stop(watch, $"Loop with case sensitive equal", times);
// Array.Exists + Equals
watch.Start();
for (int i = 0; i < times; i++)
{
contains = Array.Exists(stringArray, item => string.CompareOrdinal(item, query) == 0);
}
Stop(watch, $"Array.Exists + Equals", times);
// Array.Exists + ==
watch.Start();
for (int i = 0; i < times; i++)
{
contains = Array.Exists(stringArray, item => item == query);
}
Stop(watch, $"Array.Exists + ==", times);
// Array.IndexOf
watch.Start();
for (int i = 0; i < times; i++)
{
contains = Array.IndexOf(stringArray, query) > -1;
}
Stop(watch, $"Array.IndexOf", times);
Console.WriteLine("Case Insensitive Search");
// Loop with case insensitive equal
watch.Start();
for (int i = 0; i < times; i++)
{
for (int j = 0; j < stringArray.Length; j++)
{
if (string.Equals(query, stringArray[j], StringComparison.OrdinalIgnoreCase))
{
contains = true;
break;
}
}
}
Stop(watch, $"Loop with case insensitive equal", times);
// Array.Exists + EqualsIgnoreCase
watch.Start();
for (int i = 0; i < times; i++)
{
contains = Array.Exists(stringArray, item => string.Equals(item, query, StringComparison.OrdinalIgnoreCase));
}
Stop(watch, $"Array.Exists + EqualsIgnoreCase", times);
// Linq Contains: OrdinalIgnoreCase
watch.Start();
for (int i = 0; i < times; i++)
{
contains = stringArray.Contains(query, StringComparer.OrdinalIgnoreCase);
}
Stop(watch, $"Linq Contains: OrdinalIgnoreCase", times);
// Linq Contains: InvariantCultureIgnoreCase
watch.Start();
for (int i = 0; i < times; i++)
{
contains = stringArray.Contains(query, StringComparer.InvariantCultureIgnoreCase);
}
Stop(watch, $"Linq Contains: InvariantCultureIgnoreCase", times);
// Linq Contains: CurrentCultureIgnoreCase
watch.Start();
for (int i = 0; i < times; i++)
{
contains = stringArray.Contains(query, StringComparer.CurrentCultureIgnoreCase);
}
Stop(watch, $"Linq Contains: CurrentCultureIgnoreCase", times);
if (!contains)
throw new Exception();
}
#region Miscellaneous
public static string[] GetStringArray()
{
string words =
@"the that not look put
and with then don’t could
a all were come house
to we go will old
said can little into too
in are as back by
he up no from day
I had mum children made
of my one him time
it her them Mr I’m
was what do get if
you there me just help
they out down now Mrs
on this dad came called
she have big oh here
is went when about off
for be it’s got asked
at like see their saw
his some looked people make
but so very your an ";
return words.Split(" ");
}
public static Stopwatch Start() => Stopwatch.StartNew();
public static void Stop(Stopwatch watch, string item, int? cnt = null)
{
watch.Stop();
string msg = $"{item,53}\ttook {watch.ElapsedMilliseconds,6:#,0} ms";
if (cnt != null)
msg += $" to execute {cnt:#,0} times ({watch.ElapsedMilliseconds * 1e3 / cnt:0.0} us per exec).";
watch.Reset();
Console.WriteLine(msg);
Debug.WriteLine(msg);
Console.WriteLine("");
Debug.WriteLine("");
}
#endregion
}
Result
Case Sensitive Search
Loop with case sensitive equal took 323 ms to execute 1,000,000 times (0.3 us per exec).
Array.Exists + Equals took 445 ms to execute 1,000,000 times (0.4 us per exec).
Array.Exists + == took 461 ms to execute 1,000,000 times (0.5 us per exec).
Array.IndexOf took 511 ms to execute 1,000,000 times (0.5 us per exec).
Case Insensitive Search
Loop with case insensitive equal took 408 ms to execute 1,000,000 times (0.4 us per exec).
Array.Exists + EqualsIgnoreCase took 543 ms to execute 1,000,000 times (0.5 us per exec).
Linq Contains: OrdinalIgnoreCase took 827 ms to execute 1,000,000 times (0.8 us per exec).
Linq Contains: InvariantCultureIgnoreCase took 6,220 ms to execute 1,000,000 times (6.2 us per exec).
Linq Contains: CurrentCultureIgnoreCase took 6,249 ms to execute 1,000,000 times (6.2 us per exec).
Edit: Added code to have MWE
Array.Exists
\$\endgroup\$String.CompareOrdinal
while you actually needString.Equal()
with the appropriate comparer. Why? Because, in theory, an implementation MIGHT perform an ordinal case sensitive comparison slightly faster (for example because it just need to compareZF
after callingREPE CMPSx
, this assuming that the compiler will optimize your own== 0
away anyway)... \$\endgroup\$IndexOf
you're comparing different things then it doesn't really make sense to have it here. If - in the real world case - you will search inside the array multiple times then you'll find yourself that (MAYBE) to sort the array and then performing aBinarySearch
is actually faster. Also, other approaches might be easier to make parallel (if the number of items in the array is big enough to justify it). \$\endgroup\$