Runtime: JIT poor codegen for calculating field offsets of structs

Created on 28 Jul 2020  路  8Comments  路  Source: dotnet/runtime

Description

The JIT doesn't generate good assembly when calculating (what should be) constant fields offsets.
Example sharplab

Configuration

.NET 5, master branch on sharplab and I repro'd it locally with what i believe is a reasonably up to date copy of the runtime (i don't think it is more than a week old)

Analysis

I'm aware the JIT has to throw for null in the first null case, but they show it has the ability to resolve it to a constant (it emits it as a constant load of 4 there, although right after throwing an NRE). It doesn't seem to succeed doing this with either a local (even when SkipLocalsInit is enabled) or just with a pointer (where I would expect identical codegen to the null case, except comparing the pointer for null rather than immediately NREing).

[module: SkipLocalsInit]

public struct Example
{
    public byte Zero;

    private byte _1, _2, _3;

    public byte Four;
}

// good codegen :)! ... but instant null ref ex
public int OffsetAsNull()
{
    Example* p = null;
    return (int)(&p->Four - &p->Zero);
}

// bad codegen
public int OffsetAsNotNullWithLocal()
{
    Example local;
    Example* p = &local;
    return (int)(&p->Four - &p->Zero);
}

// bad codegen
public int OffsetAsNotNullWithParam(Example* p)
{
    return (int)(&p->Four - &p->Zero);
}

generates

C.OffsetAsNull()
    L0000: xor eax, eax
    L0002: cmp [eax], eax ; NRE
    L0004: mov eax, 4 ; constant offset load
    L0009: ret

C.OffsetAsNotNullWithLocal()
    L0000: sub esp, 8
    L0003: lea eax, [esp]
    L0006: cmp [eax], eax
    L0008: lea edx, [eax+4]
    L000b: sub edx, eax
    L000d: mov eax, edx
    L000f: add esp, 8
    L0012: ret

C.OffsetAsNotNullWithParam(Example*)
    L0000: cmp [edx], edx
    L0002: lea eax, [edx+4]  
    L0005: sub eax, edx ; it sets eax to edx + 4, then subs, but doesn't fold that to a constant?
    L0007: ret

category:cq
theme:basic-cq
skill-level:intermediate
cost:medium

area-CodeGen-coreclr tenet-performance

All 8 comments

.NET 5, master branch on sharplab

Yeah it's quite confusing, but "master" on sharplab means "master version of Roslyn + .NET Core 3.1 x86 JIT" (not even x64)
It confuses so many people so many times so we should ask sharplab authors to make it clearer 馃檪

I'd recommend to either use BenchmarkDotNet's disasm + .NET 5 or use COMPlus_JitDisasm=method (preferable)

I repro'd it locally with what i believe is a reasonably up to date copy of the runtime (i don't think it is more than a week old)

I did check it with my local copy of the runtime too 馃槃 , just can't do it on a freshly pulled copy right now because I am having some issues building it

edit: fixed the build issue and rechecked. There is no change in codegen except the JIT optimises the part which throws an NRE from xor; cmp to a single mov 馃槅 @EgorBo

@john-h-k thanks. Is this representative of something you've run into somewhere, or just something you happened to notice?

Marking this as future as we're battening down the hatches for 5.0.

no problem :smile:. Ran into it in a marshalling scenario where I have to relatively frequently offset some pointers based off of a field offset

Actually marking as future.

I ran into pretty much the same thing in #9791 - we can probably merge these issues because they look similar.

The workaround with null is nice. I'll probably use that.

While implementing the no. nullcheck prefix for ldfld, i realised if it supported ldflda then this would allow this to work. ECMA doesn't say no. nullcheck ldflda is valid for some reason (i really am not sure why), but when i allowed it, I got the perfect constant folded codegen 馃帀

asm G_M11044_IG01: ;; bbWeight=1 PerfScore 0.00 G_M11044_IG02: B804000000 mov eax, 4 ;; bbWeight=1 PerfScore 0.25 G_M11044_IG03: C3 ret ;; bbWeight=1 PerfScore 1.00

C.OffsetAsNotNullWithLocal() could lose the extra mov with slightly better register allocation.

Both C.OffsetAsNotNullWithLocal() and C.OffsetAsNotNullWithParam() look the same to the JIT after morph:

*  RETURN    int
\--*  CAST      int <- long
   \--*  SUB       long
      +--*  COMMA     long
      |  +--*  NULLCHECK byte
      |  |  \--*  LCL_VAR   long   V01 arg1
      |  \--*  ADD       long
      |     +--*  LCL_VAR   long   V01 arg1
      |     \--*  CNS_INT   long   4 field offset Fseq[Four]
      \--*  LCL_VAR   long   V01 arg1          Zero Fseq[Zero]

Presumably this pattern could be recognized and folded if it's common enough to warrant. The NULLCHECK node slightly complicates it. The JIT assumes that if you're creating a byref (or native pointer) that it will be dereferenced, so it conservatively creates the null check. Here, presumably we don't need the null checks.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

omajid picture omajid  路  3Comments

jkotas picture jkotas  路  3Comments

nalywa picture nalywa  路  3Comments

btecu picture btecu  路  3Comments

chunseoklee picture chunseoklee  路  3Comments