AngelicCore
AngelicCore

Reputation: 1453

Foreach MUCH faster than For in some cases?

I don't like premature optimization but I was curious while doing a simple task so I added a stopwatch. I don't understand how the difference can be so big.

Array of strings (7 characters) each [richtextbox.text].

Length of array: 5500 elements.

Foreach Elapsed Time: 0.0015 seconds

For Elapsed Time: 9.757 seconds

For:

if (chkLineBreaks.Checked)
{
    for (int i = 0; i < txtInput.Lines.Length; i++)
    {
        outputStringBuilder.Append($@"'{txtInput.Lines[i]}',");
    }
}

Foreach:

foreach (var line in txtInput.Lines)
{
    outputStringBuilder.Append($@"'{line}',");
    if (chkLineBreaks.Checked)
        outputStringBuilder.AppendLine();
}

From what I've read the difference should be negligible and the For would slightly faster.

Even more, the foreach has a condition in each iteration (unless it is being 'hoisted' up before the loop.

What is going on here?

Edit: I've changed the foreach code to:

int i = 0;
foreach (var line in txtInput.Lines)
{
    outputStringBuilder.Append($@"'{txtInput.Lines[i]}',");
    i++;
}

So it is now doing the same thing. It is taking 4.625 seconds.. still about half of the time for the FOR

Also I know that I can extract the array outside the loop but this is not what I am testing here :)

Edit #2: This is the whole code for that section:

Stopwatch sw = new Stopwatch();
        sw.Start();
        //    for (int i = 0; i < txtInput.Lines.Length; i++)
        //    {
        //        outputStringBuilder.Append($@"'{txtInput.Lines[i]}',");
        //    }
        int i = 0;
        foreach (var line in txtInput.Lines)
        {
            outputStringBuilder.Append($@"'{txtInput.Lines[i]}',");
            i++;
        }
        MessageBox.Show(sw.Elapsed.ToString());

Upvotes: 0

Views: 101

Answers (2)

John Alexiou
John Alexiou

Reputation: 29244

The compiled code sees very little difference between a for and a foreach statement when traversing an array (or list).

Consider this simple code the writes out a list of strings three different ways:

class Program
{
    static void Main(string[] args)
    {
        var list = Enum.GetNames(typeof(System.UriComponents));

        // 1. for each
        foreach (var item in list)
        {
            Console.WriteLine(item);
        }
        // 2. for loop
        for (int i = 0; i<list.Length; i++)
        {
            Console.WriteLine(list[i]);
        }
        // 3. LINQ
        Console.WriteLine(string.Join(Environment.NewLine, list));
    }
}

Now look at the MSIL compiled code, translated back into C# using ILSpy or DotNetPeek.

// ConsoleApplication1.Program
private static void Main(string[] args)
{
    string[] list = Enum.GetNames(typeof(UriComponents));
    string[] array = list;
    for (int j = 0; j < array.Length; j++)
    {
        string item = array[j];
        Console.WriteLine(item);
    }
    for (int i = 0; i < list.Length; i++)
    {
        Console.WriteLine(list[i]);
    }
    Console.WriteLine(string.Join(Environment.NewLine, list));
}

See the two for loops. The foreach statement became a for loop by the compiler. As far as the string.Join() statement it calls the SZArrayEnumerator which holds a reference to the array, and the current index value. At each .MoveNext() call the index is incremented and a new value returned. Basically, it is equivalent to the following:

int i = 0;
while (i<list.Length)
{
    Console.WriteLine(list[i]);
    i++;
}

Upvotes: 1

mjwills
mjwills

Reputation: 23819

The issue is that txtInput.Lines is executing many times (once per line) in your for loop (due to use of txtInput.Lines[i]). So for every line of the file you are saying 'OK, please parse this textbox into multiple lines - and then get me the nth line' - and the parsing is the killer bit.

For a fairer comparison:

if (chkLineBreaks.Checked)
{
    var lines = txtInput.Lines;
    for (int i = 0; i < lines.Length; i++)
    {
        outputStringBuilder.Append($@"'{lines[i]}',");
    }
}

this way the Lines call is done only once (i.e. equivalent to the foreach scenario).

One way to spot these kinds of issues is to compare the timings. The slow one is about 6K slower than the fast one, and you have 5.5K entries. Since 5.5K and 6K are very similar numbers, it may prompt you to think 'am I doing something in the loop that I really shouldn't?'

Upvotes: 3

Related Questions