Runtime: for loop little slower on .NET Core 3.0

Created on 7 Nov 2018  Â·  15Comments  Â·  Source: dotnet/runtime

The (very) basic benchmark below shows that a for loop on .NET Core 3.0 is a little slower than on .NET Core 2.1.5. What surprised me most, is that a foreach is "a lot" faster than a for loop on .NET Core 3.0.

using BenchmarkDotNet.Attributes;
using System.Linq;

namespace Benchmarks
{
    public class LoopBenchmark
    {
        private int[] _ints;
        private string[] _strings;

        [Benchmark]
        public void Int32_ForEach()
        {
            foreach (var s in _ints)
            {
                if (s == -1)
                    break;
            }
        }

        [Benchmark]
        public void Int32_For()
        {
            for (var i = 0; i < _ints.Length; i++)
            {
                if (_ints[i] == -1)
                    break;
            }
        }

        [Benchmark]
        public void String_ForEach()
        {
            foreach (var s in _strings)
            {
                if (s == null)
                    break;
            }
        }

        [Benchmark]
        public void String_For()
        {
            for (var i = 0; i < _strings.Length; i++)
            {
                if (_strings[i] == null)
                    break;
            }
        }

        [GlobalSetup]
        public void GlobalSetup()
        {
            _ints = Enumerable.Range(1, 100).ToArray();
            _strings = _ints.Select(p => p.ToString()).ToArray();
        }
    }
}

.NET Core 3.0

BenchmarkDotNet=v0.11.2, OS=Windows 10.0.17134.345 (1803/April2018Update/Redstone4)
Intel Core i7-6700K CPU 4.00GHz (Skylake), 1 CPU, 8 logical and 4 physical cores
Frequency=3914070 Hz, Resolution=255.4885 ns, Timer=TSC
.NET Core SDK=3.0.100-alpha1-009640
  [Host]     : .NET Core 3.0.0-preview1-27004-04 (CoreCLR 4.6.27003.04, CoreFX 4.6.27003.02), 64bit RyuJIT
  DefaultJob : .NET Core 3.0.0-preview1-27004-04 (CoreCLR 4.6.27003.04, CoreFX 4.6.27003.02), 64bit RyuJIT


| Method | Mean | Error | StdDev |
|--------------- |---------:|----------:|----------:|
| Int32_ForEach | 40.81 ns | 0.2533 ns | 0.2369 ns |
| Int32_For | 56.18 ns | 0.8982 ns | 0.8402 ns |
| String_ForEach | 41.19 ns | 0.2446 ns | 0.2169 ns |
| String_For | 56.52 ns | 0.3497 ns | 0.3271 ns |

.NET Core 2.1.5

BenchmarkDotNet=v0.11.2, OS=Windows 10.0.17134.345 (1803/April2018Update/Redstone4)
Intel Core i7-6700K CPU 4.00GHz (Skylake), 1 CPU, 8 logical and 4 physical cores
Frequency=3914070 Hz, Resolution=255.4885 ns, Timer=TSC
.NET Core SDK=3.0.100-alpha1-009640
  [Host]     : .NET Core 2.1.5 (CoreCLR 4.6.26919.02, CoreFX 4.6.26919.02), 64bit RyuJIT
  DefaultJob : .NET Core 2.1.5 (CoreCLR 4.6.26919.02, CoreFX 4.6.26919.02), 64bit RyuJIT


| Method | Mean | Error | StdDev |
|--------------- |---------:|----------:|----------:|
| Int32_ForEach | 52.81 ns | 0.3626 ns | 0.3392 ns |
| Int32_For | 54.38 ns | 0.2734 ns | 0.2557 ns |
| String_ForEach | 52.76 ns | 0.3920 ns | 0.3274 ns |
| String_For | 54.35 ns | 0.3005 ns | 0.2811 ns |

I always (wrongly?) assumed that a for loop would produce far (?) more optimized code.

category:cq
theme:loop-opt
skill-level:intermediate
cost:medium

JitUntriaged area-CodeGen-coreclr optimization

All 15 comments

Try using a local copy of the array reference e.g.

public void Int32_For()
{
    var ints = _ints;
    for (var i = 0; i < ints.Length; i++)
    {
        if (ints[i] == -1)
            break;
    }
}

I think it would be caused by https://github.com/dotnet/coreclr/pull/15756?

If we are fetching an Array Length for an array ref that came from global memory
then for CSE safety we must use the conservative value number for both

/cc @mikedn

@benaadams I already tried before (been stalking you :p), but I since get more comparable results (when do we get that developer mode?)…

The code that's produced by .NET Core 2.1.5 and 3.0 is quite different though (as captured by BDN):

.NET Core 2.1.5

00007ffb`a5924370 Benchmarks.LoopBenchmark.Int32_ForEach()
00007ffb`a5924370 488b4108        mov     rax,qword ptr [rcx+8]
00007ffb`a5924374 33d2            xor     edx,edx
00007ffb`a5924376 8b4808          mov     ecx,dword ptr [rax+8]
00007ffb`a5924379 85c9            test    ecx,ecx
00007ffb`a592437b 7e14            jle     00007ffb`a5924391
00007ffb`a592437d 4c63c2          movsxd  r8,edx
00007ffb`a5924380 468b448010      mov     r8d,dword ptr [rax+r8*4+10h]
00007ffb`a5924385 4183f8ff        cmp     r8d,0FFFFFFFFh
00007ffb`a5924389 7406            je      00007ffb`a5924391
00007ffb`a592438b ffc2            inc     edx
00007ffb`a592438d 3bca            cmp     ecx,edx
00007ffb`a592438f 7fec            jg      00007ffb`a592437d
00007ffb`a5924391 c3              ret

00007ffb`a5914370 Benchmarks.LoopBenchmark.Int32_For()
00007ffb`a5914370 488b4108        mov     rax,qword ptr [rcx+8]
00007ffb`a5914374 488bd0          mov     rdx,rax
00007ffb`a5914377 33c9            xor     ecx,ecx
00007ffb`a5914379 8b5208          mov     edx,dword ptr [rdx+8]
00007ffb`a591437c 85d2            test    edx,edx
00007ffb`a591437e 7e14            jle     00007ffb`a5914394
00007ffb`a5914380 4c8bc0          mov     r8,rax
00007ffb`a5914383 4c63c9          movsxd  r9,ecx
00007ffb`a5914386 43837c8810ff    cmp     dword ptr [r8+r9*4+10h],0FFFFFFFFh
00007ffb`a591438c 7406            je      00007ffb`a5914394
00007ffb`a591438e ffc1            inc     ecx
00007ffb`a5914390 3bd1            cmp     edx,ecx
00007ffb`a5914392 7fec            jg      00007ffb`a5914380
00007ffb`a5914394 c3              ret

.NET Core 3.0 Preview1

00007ffb`a88f3eb0 Benchmarks.LoopBenchmark.Int32_ForEach()
00007ffb`a88f2533 488b4510        mov     rax,qword ptr [rbp+10h]
00007ffb`a88f2537 488b4008        mov     rax,qword ptr [rax+8]
00007ffb`a88f253b 488945e8        mov     qword ptr [rbp-18h],rax
00007ffb`a88f253f 33c0            xor     eax,eax
00007ffb`a88f2541 8945e4          mov     dword ptr [rbp-1Ch],eax
00007ffb`a88f2544 eb25            jmp     00007ffb`a88f256b
00007ffb`a88f2546 488b45e8        mov     rax,qword ptr [rbp-18h]
00007ffb`a88f254a 8b55e4          mov     edx,dword ptr [rbp-1Ch]
00007ffb`a88f254d 483b5008        cmp     rdx,qword ptr [rax+8]
00007ffb`a88f2551 732c            jae     00007ffb`a88f257f
00007ffb`a88f2553 488d449010      lea     rax,[rax+rdx*4+10h]
00007ffb`a88f2558 8b00            mov     eax,dword ptr [rax]
00007ffb`a88f255a 8945e0          mov     dword ptr [rbp-20h],eax
00007ffb`a88f255d 837de0ff        cmp     dword ptr [rbp-20h],0FFFFFFFFh
00007ffb`a88f2561 7414            je      00007ffb`a88f2577
00007ffb`a88f2563 8b45e4          mov     eax,dword ptr [rbp-1Ch]
00007ffb`a88f2566 ffc0            inc     eax
00007ffb`a88f2568 8945e4          mov     dword ptr [rbp-1Ch],eax
00007ffb`a88f256b 8b45e4          mov     eax,dword ptr [rbp-1Ch]
00007ffb`a88f256e 488b55e8        mov     rdx,qword ptr [rbp-18h]
00007ffb`a88f2572 3b4208          cmp     eax,dword ptr [rdx+8]
00007ffb`a88f2575 7ccf            jl      00007ffb`a88f2546
00007ffb`a88f2577 488d65f0        lea     rsp,[rbp-10h]

00007ffb`a8913ea0 Benchmarks.LoopBenchmark.Int32_For()
00007ffb`a8912527 488b4510        mov     rax,qword ptr [rbp+10h]
00007ffb`a891252b 488b4008        mov     rax,qword ptr [rax+8]
00007ffb`a891252f 488945f8        mov     qword ptr [rbp-8],rax
00007ffb`a8912533 33c0            xor     eax,eax
00007ffb`a8912535 8945f4          mov     dword ptr [rbp-0Ch],eax
00007ffb`a8912538 eb23            jmp     00007ffb`a891255d
00007ffb`a891253a 488b4510        mov     rax,qword ptr [rbp+10h]
00007ffb`a891253e 488b4008        mov     rax,qword ptr [rax+8]
00007ffb`a8912542 8b55f4          mov     edx,dword ptr [rbp-0Ch]
00007ffb`a8912545 483b5008        cmp     rdx,qword ptr [rax+8]
00007ffb`a8912549 7324            jae     00007ffb`a891256f
00007ffb`a891254b 488d449010      lea     rax,[rax+rdx*4+10h]
00007ffb`a8912550 8338ff          cmp     dword ptr [rax],0FFFFFFFFh
00007ffb`a8912553 7414            je      00007ffb`a8912569
00007ffb`a8912555 8b45f4          mov     eax,dword ptr [rbp-0Ch]
00007ffb`a8912558 ffc0            inc     eax
00007ffb`a891255a 8945f4          mov     dword ptr [rbp-0Ch],eax
00007ffb`a891255d 8b45f4          mov     eax,dword ptr [rbp-0Ch]
00007ffb`a8912560 488b55f8        mov     rdx,qword ptr [rbp-8]
00007ffb`a8912564 3b4208          cmp     eax,dword ptr [rdx+8]
00007ffb`a8912567 7cd1            jl      00007ffb`a891253a
00007ffb`a8912569 488d6500        lea     rsp,[rbp]

Shall I close this issue?

/cc @AndyAyersMS

COMPlus_TieredCompilation?

@omariom Tiered JIT was indeed still enabled by default in the preview build I used. With Tiered JIT disabled, the generate code is identical.

With Tiered JIT disabled, _foreach_ is way faster than _for_ (including the change @benaadams suggested) in .NET Core 3.0:

.NET Core 2.1.5:

| Method | Mean | Error | StdDev |
|-------------- |---------:|----------:|----------:|
| Int32_ForEach | 51.96 ns | 0.0162 ns | 0.0136 ns |
| Int32_For | 52.21 ns | 0.3361 ns | 0.2979 ns |

.NET Core 3.0 Preview1

| Method | Mean | Error | StdDev |
|-------------- |---------:|----------:|----------:|
| Int32_ForEach | 36.86 ns | 0.0186 ns | 0.0174 ns |
| Int32_For | 53.79 ns | 0.0984 ns | 0.0872 ns |

As you can see, _foreach_ has gotten quite a bit faster with .NET Core 3.0.

can you post the relevant assembly with tiered jit off? I'm curious as to what's different between these two cases.

; Benchmarks.LoopBenchmark.Int32_ForEach()
       mov     rax,qword ptr [rcx+8]
       xor     edx,edx
       mov     ecx,dword ptr [rax+8]
       test    ecx,ecx
       jle     M00_L01
M00_L00:
       movsxd  r8,edx
       mov     r8d,dword ptr [rax+r8*4+10h]
       cmp     r8d,0FFFFFFFFh
       je      M00_L01
       inc     edx
       cmp     ecx,edx
       jg      M00_L00
M00_L01:
       ret
; Total bytes of code 34
; Benchmarks.LoopBenchmark.Int32_For()
       mov     rax,qword ptr [rcx+8]
       mov     rdx,rax
       xor     ecx,ecx
       mov     edx,dword ptr [rdx+8]
       test    edx,edx
       jle     M00_L01
M00_L00:
       mov     r8,rax
       movsxd  r9,ecx
       cmp     dword ptr [r8+r9*4+10h],0FFFFFFFFh
       je      M00_L01
       inc     ecx
       cmp     edx,ecx
       jg      M00_L00
M00_L01:
       ret
; Total bytes of code 37

C# converts a foreach over an array to a for loop

@benaadams Why do I consistently get better results with the foreach?

Do you happen to know the address of the first instruction in the respective loops?

While instructions in the loop are a bit different the likely suspect is loop head alignment … see dotnet/runtime#11409 for an example of the kind of impact that can have.

We can make the jitted code more resilient here by padding (#11607).

00007ffb`381c9110 Benchmarks.LoopBenchmark.Int32_ForEach()
00007ffb`381c9115 488b4108        mov     rax,qword ptr [rcx+8]
00007ffb`381c9119 33d2            xor     edx,edx
00007ffb`381c911b 8b4808          mov     ecx,dword ptr [rax+8]
00007ffb`381c911e 85c9            test    ecx,ecx
00007ffb`381c9120 7e14            jle     00007ffb`381c9136
00007ffb`381c9122 4c63c2          movsxd  r8,edx
00007ffb`381c9125 468b448010      mov     r8d,dword ptr [rax+r8*4+10h]
00007ffb`381c912a 4183f8ff        cmp     r8d,0FFFFFFFFh
00007ffb`381c912e 7406            je      00007ffb`381c9136
00007ffb`381c9130 ffc2            inc     edx
00007ffb`381c9132 3bca            cmp     ecx,edx
00007ffb`381c9134 7fec            jg      00007ffb`381c9122
00007ffb`381c9136 c3              ret
00007ffb`381d9110 Benchmarks.LoopBenchmark.Int32_For()
00007ffb`381d9115 488b4108        mov     rax,qword ptr [rcx+8]
00007ffb`381d9119 488bd0          mov     rdx,rax
00007ffb`381d911c 33c9            xor     ecx,ecx
00007ffb`381d911e 8b5208          mov     edx,dword ptr [rdx+8]
00007ffb`381d9121 85d2            test    edx,edx
00007ffb`381d9123 7e14            jle     00007ffb`381d9139
00007ffb`381d9125 4c8bc0          mov     r8,rax
00007ffb`381d9128 4c63c9          movsxd  r9,ecx
00007ffb`381d912b 43837c8810ff    cmp     dword ptr [r8+r9*4+10h],0FFFFFFFFh
00007ffb`381d9131 7406            je      00007ffb`381d9139
00007ffb`381d9133 ffc1            inc     ecx
00007ffb`381d9135 3bd1            cmp     edx,ecx
00007ffb`381d9137 7fec            jg      00007ffb`381d9125
00007ffb`381d9139 c3              ret

Let me know if and how I can provide more info.

Looks like in the faster ForEach case the first four instructions in the loop are all in the same 16 byte bundle, while in the slower For case they split across two bundes (the cmp hangs over just a bit). So loop alignment still seems like the best guess.

If you have access to the hardware perf counters (say via vtune) you could confirm, you should see similar instructions retired but higher clocks (and hence lower CPI).

Alternatively you can try adding some padding by hand to see if you can influence results (either for better or worse) -- say add an out param to the method and store to it above the loop (or store to it more than once, etc). If doing that drastically alters perf for either variant then the code is highly alignment senstitive.

Why do I consistently get better results with the foreach?

I have seen this issue before when I was measuring looping over a span versus an array. As you can see, I added a random static variable at the beginning of the method which helped.

Try @AndyAyersMS's suggestion.

```C#
[DisassemblyDiagnoser(printAsm: true, printPrologAndEpilog: true)]
public class Program
{
// Work-around for loop head alignment issue since this micro-benchmark is sensitive to it.
public static int zero = 0;

public static int ArraySum(byte[] data)
{
    int sum = zero;
    for (int i = 0; i < data.Length; i++)
    {
        sum += data[i];
    }
    return sum;
}

public static int SpanSum(Span<byte> data)
{
    int sum = zero;
    for (int i = 0; i < data.Length; i++)
    {
        sum += data[i];
    }
    return sum;
}

private byte[] data;

[GlobalSetup]
public void Setup()
{
    data = new byte[10_000];
    new Random(42).NextBytes(data);
}

[Benchmark(Baseline = true)]
public int ArraySum() => ArraySum(data);

[Benchmark]
public int SpanSum() => SpanSum(data);

static void Main(string[] args)
{
    byte[] data = { 1, 2, 3, 4, 5, 6, 7 };
    ArraySum(data); // returns 28
    SpanSum(data);  // returns 28

    Summary summary = BenchmarkRunner.Run<Program>();
}

}
```

Adding my simple benchmark result here:
Version used: 3.0.100

| Method | Mean | Error | StdDev | Ratio |
|------------- |----------:|----------:|----------:|------:|
| ArrayForeach | 34.64 ns | 0.0661 ns | 0.0619 ns | 1.00 |
| ArrayFor | 41.62 ns | 0.0606 ns | 0.0506 ns | 1.20 |
| ListForeach | 163.54 ns | 0.4555 ns | 0.3556 ns | 4.72 |
| ListFor | 54.17 ns | 0.4392 ns | 0.4108 ns | 1.56 |
| ArrayAsSpan | 46.85 ns | 0.0278 ns | 0.0232 ns | 1.35 |

The codegen of span looks suboptimal.

Was this page helpful?
0 / 5 - 0 ratings