Skip to content

JIT (x64, .NET 10): optimizer drops ?? value.ToString(), stores the null LHS for non-string boxed values #130035

Description

@cp-1921

Description

JIT miscompilation: optimized codegen drops the RHS of ?? (value.ToString()), storing the null LHS

Summary

In optimized (Tier1 / Optimize=true) codegen for a shared generic method, the JIT
eliminates the right-hand side of a null-coalescing expression

dict[key] = strVal ?? value.ToString();

and emits dict[key] = strVal instead. When strVal is null (which it is for every
non-string value), the dictionary receives null — value.ToString() is never called.
The return value of an immediately-preceding method call assigned to value is also discarded
(dead).

The method is correct under MinOpts and with [MethodImpl(MethodImplOptions.NoOptimization)],
and broken only when the optimizer runs. This is a codegen correctness bug, not a logic bug —
e.g. for a boxed long, the optimized method behaves as if ((object)5L).ToString() returned
null, and a real string fails an is string check.

Environment

  • .NET SDK: 10.0.201 (runtime band 10.0.2), rollForward: patch
  • OS / arch: Windows 11 x64
  • Config: Release / optimized, Tiered Compilation on, "optimized using Synthesized PGO"
  • also tested with .NET 10.0.9 (same result)

Reproduces with optimizations on. Disappears with any of:

  • DOTNET_JITMinOpts=1
  • [MethodImpl(MethodImplOptions.NoOptimization)] on the method

Does not help (still broken):

  • DOTNET_TieredPGO=0
  • DOTNET_TieredCompilation=0 (forces full opt immediately — still wrong)

Affected source

The real method (generic class FlatObjectList<T>), reduced to the relevant statement:

		private void AddData(Dictionary<string, string> flatObject, string fullName, string name, object value)
		{
			string strVal = (value is string) ? (string)value : null; // null for non-string value
			if (value == null || (strVal != null && (strVal.Equals("N/A") || strVal.Equals(" "))))
			{
				strVal = MessagingDataTranslator.NullOrEmptyText;
			}

			if (value is bool bVal)
			{
				value = bVal ? MessagingDataTranslator.BoolValueStringTrue : MessagingDataTranslator.BoolValueStringFalse;
			}

			if (name == MessagingProtoNameMapping.ProtoName_ApplicationTypeInt)
			{
				value = (MessagingApplicationType)value;
			}
			
			if (name == MessagingProtoNameMapping.ProtoName_ServiceTypeInt)
			{
				value = (TServiceType)value;
			}

			value = HandleDateTimeAndNumericValues(value, name); // return value ignored in opt build
			flatObject[fullName] = strVal ?? value.ToString(); // RHS dropped in opt build
		}

Observed: every non-string column (System.Int64, System.UInt32, System.UInt64,
enums such as KS.Messaging.Status / DiskType / BinaryType) is stored as null.

Expected vs actual

  • Expected: for a boxed long, strVal is null, so strVal ?? value.ToString() evaluates
    value.ToString() → "5". Dictionary value is non-null.
  • Actual (optimized): dictionary value is null; value.ToString() is never executed.

Disassembly evidence

Two DOTNET_JitDisasm=*AddData* dumps of the same method instantiation
FlatObjectList1[System.__Canon]:AddData(...)`.

MinOpts — CORRECT (value.ToString() present)

call  ...:HandleDateTimeAndNumericValues(System.Object,System.String):System.Object:this
mov   [rbp+0x30], rax                 ; value = result
mov   rax, [rbp-0x08]                 ; text = strVal
mov   [rbp-0x30], rax
cmp   [rbp-0x08], 0                   ; strVal == null ?
jne   IG12                            ;   if non-null, skip
mov   rcx, [rbp+0x30]                 ;   else value.ToString()
mov   rax, [rbp+0x30]
mov   rax, [rax]
mov   rax, [rax+0x40]
call  [rax+0x08] System.Object:ToString():System.String:this
mov   [rbp-0x30], rax                 ; text = ToString()
IG12:
mov   r8, [rbp-0x30]                  ; dict value = text
call  Dictionary`2[__Canon,__Canon]:set_Item(__Canon,__Canon):this

Tier1 (optimized, Synthesized PGO) — BROKEN (no ToString, stores LHS)

call  ...:HandleDateTimeAndNumericValues(System.Object,System.String):System.Object:this
                                       ; rax (return) discarded — dead
; no strVal == null test, no Object:ToString() call anywhere in the method
...
mov   rdx, r15                         ; dict value = r15 = strVal  (null for non-string)
call  CORINFO_HELP_ASSIGN_REF          ; inlined Dictionary insert stores strVal

r15 holds strVal, set to null (xor r15,r15) in the non-string path. The whole
?? value.ToString() fallback is gone, and the value = HandleDateTimeAndNumericValues(...)
result is unused.

(Full listings: MinOpts ~548 bytes; buggy Tier1 ~1214 bytes. Both attached.)

Root cause (suspected)

Assertion propagation / null-check elimination in optimized shared-generic codegen incorrectly
concludes the LHS of ?? is non-null (or the RHS unreachable) and folds
strVal ?? value.ToString() to strVal, dropping the value.ToString() call. Likely related to
the .NET 10 de-abstraction / assertion-prop work (#108913, #108988, #74671).

Workaround

[MethodImpl(MethodImplOptions.NoOptimization)] on the affected method (scoped deopt; no
process-wide perf cost), or DOTNET_JITMinOpts=1 (whole process).

this code change also solves the problem:

		private void AddData(Dictionary<string, string> flatObject, string fullName, string name, object value)
		{
			if (value == null)
			{
				flatObject[fullName] = MessagingDataTranslator.NullOrEmptyText;
				return;
			}

			string strVal = (value is string) ? (string)value : null;
			if (strVal != null && (strVal.Equals("N/A") || strVal.Equals(" ")))
			{
				strVal = MessagingDataTranslator.NullOrEmptyText;
			}

			if (value is bool bVal)
			{
				value = bVal ? MessagingDataTranslator.BoolValueStringTrue : MessagingDataTranslator.BoolValueStringFalse;
			}

			if (name == MessagingProtoNameMapping.ProtoName_ApplicationTypeInt)
			{
				value = (MessagingApplicationType)value;
			}
			
			if (name == MessagingProtoNameMapping.ProtoName_ServiceTypeInt)
			{
				value = (TServiceType)value;
			}

			value = HandleDateTimeAndNumericValues(value, name);
			flatObject[fullName] = strVal ?? value.ToString();
		}

add_data_noOptimization.txt

add_data_withOptimization.txt

Reproduction Steps

could not reproduce with minimal test project. Snippets are in Description.

Expected behavior

strVal ?? value.ToString() evaluates value.ToString() whenever strVal is null, and the result is stored in the dictionary. For a non-string value (e.g. a boxed long), strVal = value as string is null, so the dictionary entry should hold value.ToString() (e.g. "5"). This is what MinOpts produces, and what the method does when compiled with [MethodImpl(MethodImplOptions.NoOptimization)] or DOTNET_JITMinOpts=1.

Actual behavior

In optimized (Tier1) codegen the JIT eliminates the value.ToString() call entirely and stores strVal unconditionally, so the dictionary receives null for every non-string value. The value = HandleDateTimeAndNumericValues(value, name) assignment immediately before is also dead-coded (its return is discarded). Effectively the optimized method behaves as if ((object)5L).ToString() returned null and "someString" is string were false. The disassembly confirms there is no strVal == null test and no System.Object:ToString() call anywhere in the optimized method body; the dictionary value comes straight from the register holding strVal (null on the non-string path).

Regression?

it did work on .net8

Not a recent regression — reproduces on .NET 10.0.2,

Known Workarounds

Workarounds and fixes are descriped in the description section

Configuration

No response

Other information

No response

Activity

  1. added
    area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI
    on Jun 30, 2026
  2. dotnet-policy-service commented on Jun 30, 2026

    @dotnet-policy-service
    Contributor

    Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
    See info in area-owners.md if you want to be subscribed.

  3. added this to the 11.0.0 milestone on Jun 30, 2026
  4. removed
    untriagedNew issue has not been triaged by the area owner
    on Jun 30, 2026
  5. added
    Priority:2Work that is important, but not critical for the release
    on Jul 9, 2026
  6. dhartglassMSFT commented on Jul 14, 2026

    @dhartglassMSFT
    Contributor

    Hi @cp-1921 , this is fixed in NET11 by PR #125093 from a few months ago.

    I'll see if it's possible to port that fix back into servicing.

  7. added a commit that references this issue on Jul 20, 2026
    f70b741
  8. JulieLeeMSFT commented on Jul 21, 2026

    @JulieLeeMSFT
    Member

    @cp-1921, we backported to 10.0.12 which will be released in September 8th.
    Please report back if it does not fix the issue.
    I am closing this now.

  9. locked and limited conversation to collaborators on Aug 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

Priority:2Work that is important, but not critical for the releasearea-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions