Skip to content

Instantiating ValueTuple<int, int, int> significantly more expensive than ValueTuple<int, int> or equivalent struct #89170

Description

@canton7

Description

See the following benchmark, which compares the performance of instantiating and passing a ValueTuple<int, int>, ValueTuple<int, int, int>, and two structs which are functionally equivalent to ValueTuple.

publicclassTest{privateintsum=0;[Benchmark]publicintValueTuple2(){sum=0;for(inti=0;i<100;i++){M(newValueTuple<int,int>(i,i));}returnsum;}[Benchmark]publicintValueTuple3(){sum=0;for(inti=0;i<100;i++){M(newValueTuple<int,int,int>(i,i,i));}returnsum;}[Benchmark]publicintValueTupleIsh2(){sum=0;for(inti=0;i<100;i++){M(newValueTupleIsh<int,int>(i,i));}returnsum;}[Benchmark]publicintValueTupleIsh3(){sum=0;for(inti=0;i<100;i++){M(newValueTupleIsh<int,int,int>(i,i,i));}returnsum;}publicvoidM((int,int)s){sum+=s.Item1+s.Item2;}publicvoidM((int,int,int)s){sum+=s.Item1+s.Item2;}publicvoidM(ValueTupleIsh<int,int>s){sum+=s.Item1+s.Item2;}publicvoidM(ValueTupleIsh<int,int,int>s){sum+=s.Item1+s.Item2;}}publicstructValueTupleIsh<T1,T2>{publicT1Item1;publicT2Item2;publicValueTupleIsh(T1item1,T2item2){Item1=item1;Item2=item2;}}publicstructValueTupleIsh<T1,T2,T3>{publicT1Item1;publicT2Item2;publicT3Item3;publicValueTupleIsh(T1item1,T2item2,T3item3){Item1=item1;Item2=item2;Item3=item3;}}

Data

This gives the following:

BenchmarkDotNet v0.13.6, Windows 11 (10.0.22621.1555/22H2/2022Update/SunValley2)
11th Gen Intel Core i7-11850H 2.50GHz, 1 CPU, 16 logical and 8 physical cores
.NET SDK 7.0.202
[Host] : .NET 6.0.15 (6.0.1523.11507), X64 RyuJIT AVX2
DefaultJob : .NET 6.0.15 (6.0.1523.11507), X64 RyuJIT AVX2
MethodMeanErrorStdDev
ValueTuple2113.4 ns1.45 ns1.28 ns
ValueTuple3661.5 ns7.37 ns6.90 ns
ValueTupleIsh2112.0 ns1.22 ns1.08 ns
ValueTupleIsh3111.5 ns1.03 ns0.86 ns

As you can see, instantiating/passing a ValueTuple<int, int, int> is significantly slower than a ValueTuple<int, int>. This change is not seen with an equivalent struct.

Analysis

From SharpLab, we see:

Test.ValueTuple2() L0000: xoreax,eax L0002: mov[rcx+8],eax L0005: movedx,eax L0007: addedx,[rcx+8] L000a: addedx,eax L000c: mov[rcx+8],edx L000f: inceax L0011: cmpeax,0x64 L0014: jl short L0005 L0016: moveax,[rcx+8] L0019: retTest.ValueTuple3() L0000: subrsp,0x28 L0004: vzeroupper L0007: xoreax,eax L0009: mov[rcx+8],eax L000c: nop[rax] L0010: vxorps xmm0,xmm0,xmm0 L0014: vmovupd[rsp+0x18],xmm0 L001a: mov[rsp+0x18],eax L001e: mov[rsp+0x1c],eax L0022: mov[rsp+0x20],eax L0026: vmovupdxmm0,[rsp+0x18] L002c: vmovupd[rsp+8],xmm0 L0032: movedx,[rcx+8] L0035: addedx,[rsp+8] L0039: addedx,[rsp+0xc] L003d: mov[rcx+8],edx L0040: inceax L0042: cmpeax,0x64 L0045: jl short L0010 L0047: moveax,[rcx+8] L004a: addrsp,0x28 L004e: retTest.ValueTupleIsh2() L0000: xoreax,eax L0002: mov[rcx+8],eax L0005: movedx,eax L0007: addedx,[rcx+8] L000a: addedx,eax L000c: mov[rcx+8],edx L000f: inceax L0011: cmpeax,0x64 L0014: jl short L0005 L0016: moveax,[rcx+8] L0019: retTest.ValueTupleIsh3() L0000: xoreax,eax L0002: mov[rcx+8],eax L0005: movedx,eax L0007: addedx,[rcx+8] L000a: addedx,eax L000c: mov[rcx+8],edx L000f: inceax L0011: cmpeax,0x64 L0014: jl short L0005 L0016: moveax,[rcx+8] L0019: ret

I'm not sure why the runtime has significantly different codegen for the ValueTuple<int, int, int> case, but it doesn't seem to be helping!

Interestingly, the difference disappears if double is used instead of int as all generic type parameters.

Regression?

Unsure

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMItenet-performancePerformance related issue

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions