LICM hoists a / b into the loop preheader, ahead of a new F() that comes before it in the loop body. F is finalizable. When the division throws, the object was never allocated, so its finalizer never runs.
Minimal Repro
using System;
using System.Runtime.CompilerServices;
class F
{
public static int Count;
~F() => Count++;
}
public class Program
{
public static void Main()
{
try
{
Test(1, 0, 10);
}
catch (DivideByZeroException) { }
GC.Collect();
GC.WaitForPendingFinalizers();
Console.WriteLine(F.Count);
}
[MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)]
static int Test(int a, int b, int n)
{
int sum = 0;
for (int i = 0; i < n; i++)
{
new F();
sum += a / b;
}
return sum;
}
}
Expected
1 (what MinOpts and DOTNET_JitNoHoist=1 print)
Actual
0
Regression?
No. .NET 8, 9, 10, 11 RC2, and main all print 0.
Notes
In HoistVisitor::PostOrderVisit, a helper call that can't be hoisted clears m_canHoistSideEffects only on MutatesHeap, MayRunCctor, or !NoThrow. CORINFO_HELP_NEWFAST passes all three checks, so the flag stays true and the throwing idiv gets hoisted above the allocation. The JIT should also check GTF_CALL_M_ALLOC_SIDE_EFFECTS (or HasSideEffects) here.
LICM hoists
a / binto the loop preheader, ahead of anew F()that comes before it in the loop body.Fis finalizable. When the division throws, the object was never allocated, so its finalizer never runs.Minimal Repro
Expected
1(what MinOpts andDOTNET_JitNoHoist=1print)Actual
0Regression?
No. .NET 8, 9, 10, 11 RC2, and main all print
0.Notes
In
HoistVisitor::PostOrderVisit, a helper call that can't be hoisted clearsm_canHoistSideEffectsonly onMutatesHeap,MayRunCctor, or!NoThrow.CORINFO_HELP_NEWFASTpasses all three checks, so the flag staystrueand the throwingidivgets hoisted above the allocation. The JIT should also checkGTF_CALL_M_ALLOC_SIDE_EFFECTS(orHasSideEffects) here.