Skip to content

default(T) produces non-optimized code in contrast to new T() on (generic) value types when the instance is not used #51825

Open
@Enderlook

Description

@Enderlook

Description

The runtime is not optimizing the instantiation of a struct (by default) when it's actually not used at all (due to optimizations of the code that make the instance not required).

The following minimal snippet:

using SharpLab.Runtime;

public class C
{    
    [JitGeneric(typeof(One))]
    public int New<T>()
        where T : struct, IValue
        => new T().Value;
    
    [JitGeneric(typeof(One))]
    public int Default<T>()
        where T : struct, IValue
        => default(T).Value;
}

public interface IValue
{
     int Value { get; }
}

public struct One : IValue
{
     public int Value => 1;
}

Produces this JIT asm:

C.New[[One, _]]()
    L0000: mov eax, 1
    L0005: ret

C.Default[[One, _]]()
    L0000: push eax
    L0001: xor eax, eax
    L0003: mov [esp], eax
    L0006: lea eax, [esp]
    L0009: xor edx, edx
    L000b: mov [eax], dl
    L000d: mov eax, 1
    L0012: pop ecx
    L0013: ret

The expected code generation of C.Default[[One, _]]() was the same as C.New[[One, _]]().

Configuration

Run on SharpLab (Core CLR v5.0.421.11614 on x86) on release mode.

Regression?

Don't know. Sorry.

Other information

No idea how to fix it. Sorry.

Though you currently can use new T() instead of default(T) to get the optimized code as a workaround.

By the way, I noticed the generated code is optimized if the type is actually returned to the caller.
For example:

[JitGeneric(typeof(One))]
public T New2<T>()
    where T : struct, IValue
    => new T();

[JitGeneric(typeof(One))]
public T Default2<T>()
    where T : struct, IValue
    => default(T);

Produces:

C.New2[[One, _]]()
    L0000: xor eax, eax
    L0002: ret

C.Default2[[One, _]]()
    L0000: xor eax, eax
    L0002: ret

Or when it's used as a parameter to another function (even if the function is actually inlined):

[JitGeneric(typeof(One))]
public int New3<T>()
    where T : struct, IValue
    => Consume(new T());
    
[JitGeneric(typeof(One))]
public int Default3<T>()
    where T : struct, IValue
    => Consume(default(T));
    
public int Consume<T>(T t)
    where T : IValue
    => t.Value;

Produces:

C.New3[[One, _]]()
    L0000: mov eax, 1
    L0005: ret

C.Default3[[One, _]]()
    L0000: mov eax, 1
    L0005: ret

Or if the instance is mutated:

[JitGeneric(typeof(One))]
public int New4<T>()
    where T : struct, IValue
{
    T t = new T();
    t.Value = 4;
    return t.Value;
}

[JitGeneric(typeof(One))]
public int Default4<T>()
    where T : struct, IValue
{
    T t = default(T);
    t.Value = 4;
    return t.Value;
}

public interface IValue
{
    int Value { get; set; }
}

public struct One : IValue
{
    public int Value { get; set; }
}

Produces:

C.New4[[One, _]]()
    L0000: mov eax, 4
    L0005: ret

C.Default4[[One, _]]()
    L0000: mov eax, 4
    L0005: ret

So, I think the problem only happens when an instance is created but not returned, passed as parameter nor mutated.

category:cq
theme:generics
skill-level:intermediate
cost:medium
impact:small

Metadata

Metadata

Assignees

No one assigned

    Labels

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

    Type

    No type

    Projects

    Status

    Optimizations

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions