Repository navigation
Span-perf for copy via loop much slower than array-version #9977
Copy link
Copy link
Open
Labels
area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMICLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMIoptimization
Milestone
Description
Description
In a simple copy-loop Span is a lot slower than an array-version.
I'd expect that Span and array have similar perf.
Note: Span_CopyTo is just for reference included.
Benchmark
Results
BenchmarkDotNet=v0.10.13, OS=Windows 10 Redstone 3 [1709, Fall Creators Update] (10.0.16299.309)
Intel Core i7-7700HQ CPU 2.80GHz (Kaby Lake), 1 CPU, 8 logical cores and 4 physical cores
Frequency=2742189 Hz, Resolution=364.6722 ns, Timer=TSC
.NET Core SDK=2.1.300-preview3-008384
[Host] : .NET Core 2.1.0-preview2-26313-01 (CoreCLR 4.6.26310.01, CoreFX 4.6.26313.01), 64bit RyuJIT
DefaultJob : .NET Core 2.1.0-preview2-26313-01 (CoreCLR 4.6.26310.01, CoreFX 4.6.26313.01), 64bit RyuJIT
| Method | Mean | Error | StdDev | Scaled |
|---|---|---|---|---|
| Array | 4.435 us | 0.0400 us | 0.0374 us | 1.00 |
| Span | 6.885 us | 0.0307 us | 0.0256 us | 1.55 |
| Span_CopyTo | 1.132 us | 0.0072 us | 0.0067 us | 0.26 |
Code
public class Benchmarks
{
private readonly int[] _source;
private readonly int[] _destination;
public Benchmarks()
{
_source = Enumerable.Range(0, 10_000).ToArray();
_destination = new int[_source.Length];
}
[Benchmark(Baseline = true)]
public void Array()
{
Copy(_source, _destination);
}
[Benchmark]
public void Span()
{
Copy(_source.AsSpan(), _destination.AsSpan());
}
[Benchmark]
public void Span_CopyTo()
{
_source.AsSpan().CopyTo(_destination.AsSpan());
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static void Copy(int[] src, int[] dst)
{
for (int i = 0; i < src.Length; ++i)
dst[i] = src[i];
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static void Copy(ReadOnlySpan<int> src, Span<int> dst)
{
for (int i = 0; i < src.Length; ++i)
dst[i] = src[i];
}
}Disassembly
Note: The JitDisasm is produced of fresh coreclr-build on ubuntu.
dotnet --info
.NET Core SDK (reflecting any global.json):
Version: 2.1.300-preview3-008384
Commit: 4343118151
Runtime Environment:
OS Name: ubuntu
OS Version: 16.04
OS Platform: Linux
RID: ubuntu.16.04-x64
Base Path: /usr/share/dotnet/sdk/2.1.300-preview3-008384/
Host (useful for support):
Version: 2.1.0-preview3-26319-04
Commit: 939333dbc8
.NET Core SDKs installed:
2.1.4 [/usr/share/dotnet/sdk]
2.1.300-preview3-008384 [/usr/share/dotnet/sdk]
.NET Core runtimes installed:
Microsoft.AspNetCore.All 2.1.0-preview2-30338 [/usr/share/dotnet/shared/Microsoft.AspNetCore.All]
Microsoft.AspNetCore.App 2.1.0-preview2-30338 [/usr/share/dotnet/shared/Microsoft.AspNetCore.App]
Microsoft.NETCore.App 2.0.5 [/usr/share/dotnet/shared/Microsoft.NETCore.App]
Microsoft.NETCore.App 2.1.0-preview2-26313-01 [/usr/share/dotnet/shared/Microsoft.NETCore.App]
Microsoft.NETCore.App 2.1.0-preview3-26319-04 [/usr/share/dotnet/shared/Microsoft.NETCore.App]
Also only the loops are shown.
Array-variant
G_M16768_IG04:
4863C8 movsxd rcx, eax
448B448F10 mov r8d, dword ptr [rdi+4*rcx+16]
4489448E10 mov dword ptr [rsi+4*rcx+16], r8d
FFC0 inc eax
3BD0 cmp edx, eax
7FED jg SHORT G_M16768_IG04Span-variant
G_M16768_IG03:
413BDE cmp ebx, r14d
732D jae SHORT G_M16768_IG05
4863FB movsxd rdi, ebx
3B5DE0 cmp ebx, dword ptr [rbp-20H]
7325 jae SHORT G_M16768_IG05
488B45D8 mov rax, bword ptr [rbp-28H]
8B04B8 mov eax, dword ptr [rax+4*rdi]
418904BF mov dword ptr [r15+4*rdi], eax
FFC3 inc ebx
488D7DD8 lea rdi, bword ptr [rbp-28H]
E8278F5CFF call System.ReadOnlySpan`1[Int32][System.Int32]:get_Length():int:this
3BC3 cmp eax, ebx
7FD9 jg SHORT G_M16768_IG03category:cq
theme:loop-opt
skill-level:expert
cost:medium
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMICLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMIoptimization
Activity
gfoidl commentedon Mar 20, 2018
/cc @AndyAyersMS please have a look on this
AndyAyersMS commentedon Mar 20, 2018
Seems quite odd that
get_Lengthis not inlined in the span variant. Can you show the first few lines of the jit-disasm that describe what kind of code the jit was creating?gfoidl commentedon Mar 20, 2018
Here is the whole dasm of this method:
Edit: I've uploaded the whole jitdump to https://1drv.ms/t/s!Aq_kXFBmTtM2hFNDRqUcEsqNFGcA
AndyAyersMS commentedon Mar 20, 2018
Since you have the ability to look at jit info, what reason does the jit give for rejecting the inline of
get_Length(say viaCOMPlus_JitPrintInlinedMethods=1) ?mikedn commentedon Mar 20, 2018
Could it be that this is Unix specific? On Windows this works as expected.
gfoidl commentedon Mar 20, 2018
I can't verify the dasm on Windows.
But the benchmark numbers are done on Windows 10.
On ubuntu I get similar results.
When I change
ReadOnlySpan<int>toSpan<int>it's analog.mikedn commentedon Mar 20, 2018
Well, when I try this the Copy method generates
AndyAyersMS commentedon Mar 20, 2018
@gfoidl What sort of build are you using to get the disassembly? If you use
Debugyou can get misleading results. For instance the core library is built differently.mikedn commentedon Mar 20, 2018
Hmm, the array-variant assembly you have shown is a bit misleading, in that case loop cloning kicks in and you show only the loop version without range checks. I don't think loop cloning ever kicks in for span range checks so that version will always be slightly slower even without the weird
Lengthinlining failuregfoidl commentedon Mar 20, 2018
@mikedn thx for your dasm in https://github.com/dotnet/coreclr/issues/17065#issuecomment-374705550
When I view the disassembly in VS while debugging (Release + suppress JIT optimizations unchecked, ...) then I'll see equal code (Windows)
@AndyAyersMS I followed the steps described in Viewing JIT Dumps. Built from dotnet/coreclr@ff2abe5
If this is the wrong way, please point me to the adequate way.
Even if the dasm looks quite good for Windows, the perf-numbers aren't.
AndyAyersMS commentedon Mar 20, 2018
Best practice would be to locally build both Release and Debug. Then use the release bits to overwrite the DLLs. Then overwrite again with just the Debug-built jit DLL so you can enable disasm and dumps and such.
So you end up with all the copied bits from the same build, and everything copied is built Release except for the jit, which is Debug.
gfoidl commentedon Mar 20, 2018
Thanks for the info! I'll try this.
mikedn commentedon Mar 20, 2018
Yep, I can reproduce your numbers on my machine with the assembly I posted above. Whatever happens on Linux that causes
Lengthnot to be inlined is a separate issue.Best I can tell the reason why the span version is slower is a combination of lack of loop cloning and lack of hoisting of destination span field loads. This can be seen if you try the following version:
that generates
This does not have range checks and no in-loop loads of span fields. It does have an extra compare so it's still a bit slower than the array version but quite a bit faster than the original version.
AndyAyersMS commentedon Mar 20, 2018
I take it we don't promote the destination span and so probably think the writes can alias the span fields?
I have toyed with making the implicit byref promotion more aggressive....
mikedn commentedon Mar 20, 2018
Ah, right. It looks like we do promote but then the local variable associated with the second argument has zero references so I guess promotion was undone in that case. I'll dig a bit more.
6 remaining items
Updated steps in 'Viewing JIT Dumps' to see optimized code (#17077)
colgreen commentedon Apr 23, 2022
For the record, this is still an issue in .NET 6. Here's the Benchmark output from my PC:
colinfang commentedon Mar 10, 2026
On .NET 10, the runtime manages to elide bounds check for span in one of my pc, but not the other. And I struggle to understand why.
https://godbolt.org/z/9cTfnasf3
this is when it works