I discovered an issue with (what might be) over-optimization in .Net Native
and structs
. I'm not sure if the compiler is too aggressive, or I'm too blind to see what I've done wrong.
To reproduce this, follow these steps:
Step 1: Create a new Blank Universal (win10) app in Visual Studio 2015 Update 2 targeting build 10586 with a min build of 10240. Call the project NativeBug so we have the same namespace.
Step 2: Open MainPage.xaml
and insert this label
<Page x:Class="NativeBug.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<!-- INSERT THIS LABEL -->
<TextBlock x:Name="_Label" HorizontalAlignment="Center" VerticalAlignment="Center" />
</Grid>
</Page>
Step 3: Copy/paste the following into MainPage.xaml.cs
using System;
using System.Collections.Generic;
namespace NativeBug
{
public sealed partial class MainPage
{
public MainPage()
{
InitializeComponent();
var startPoint = new Point2D(50, 50);
var points = new[]
{
new Point2D(100, 100),
new Point2D(100, 50),
new Point2D(50, 100),
};
var bounds = ComputeBounds(startPoint, points, 15);
_Label.Text = $"{bounds.MinX} , {bounds.MinY} => {bounds.MaxX} , {bounds.MaxY}";
}
private static Rectangle2D ComputeBounds(Point2D startPoint, IEnumerable<Point2D> points, double strokeThickness = 0)
{
var lastPoint = startPoint;
var cumulativeBounds = new Rectangle2D();
foreach (var point in points)
{
var bounds = ComputeBounds(lastPoint, point, strokeThickness);
cumulativeBounds = cumulativeBounds.Union(bounds);
lastPoint = point;
}
return cumulativeBounds;
}
private static Rectangle2D ComputeBounds(Point2D fromPoint, Point2D toPoint, double strokeThickness)
{
var bounds = new Rectangle2D(fromPoint.X, fromPoint.Y, toPoint.X, toPoint.Y);
// ** Uncomment the line below to see the difference **
//return strokeThickness <= 0 ? bounds : bounds.Inflate2(strokeThickness);
return strokeThickness <= 0 ? bounds : bounds.Inflate1(strokeThickness);
}
}
public struct Point2D
{
public readonly double X;
public readonly double Y;
public Point2D(double x, double y)
{
X = x;
Y = y;
}
}
public struct Rectangle2D
{
public readonly double MinX;
public readonly double MinY;
public readonly double MaxX;
public readonly double MaxY;
private bool IsEmpty => MinX == 0 && MinY == 0 && MaxX == 0 && MaxY == 0;
public Rectangle2D(double x1, double y1, double x2, double y2)
{
MinX = Math.Min(x1, x2);
MinY = Math.Min(y1, y2);
MaxX = Math.Max(x1, x2);
MaxY = Math.Max(y1, y2);
}
public Rectangle2D Union(Rectangle2D rectangle)
{
if (IsEmpty)
{
return rectangle;
}
var newMinX = Math.Min(MinX, rectangle.MinX);
var newMinY = Math.Min(MinY, rectangle.MinY);
var newMaxX = Math.Max(MaxX, rectangle.MaxX);
var newMaxY = Math.Max(MaxY, rectangle.MaxY);
return new Rectangle2D(newMinX, newMinY, newMaxX, newMaxY);
}
public Rectangle2D Inflate1(double value)
{
var halfValue = value * .5;
return new Rectangle2D(MinX - halfValue, MinY - halfValue, MaxX + halfValue, MaxY + halfValue);
}
public Rectangle2D Inflate2(double value)
{
var halfValue = value * .5;
var x1 = MinX - halfValue;
var y1 = MinY - halfValue;
var x2 = MaxX + halfValue;
var y2 = MaxY + halfValue;
return new Rectangle2D(x1, y1, x2, y2);
}
}
}
Step 4: Run the application in Debug
x64
. You should see this label:
42.5 , 42.5 => 107.5 , 107.5
Step 5: Run the application in Release
x64
. You should see this label:
-7.5 , -7.5 => 7.5, 7.5
Step 6: Uncomment line 45
in MainPage.xaml.cs
and repeat step 5. Now you see the original label
42.5 , 42.5 => 107.5 , 107.5
By commenting out line 45
, the code will use Rectangle2D.Inflate2(...)
which is exactly the same as Rectangle2D.Inflate1(...)
except it creates a local copy of the computations before sending them to the constructor of Rectangle2D
. In debug mode, these two function exactly the same. In release however, something is getting optimized out.
This was a nasty bug in our app. The code you see here was stripped from a much larger library and I'm afraid there might be more. Before I report this to Microsoft, I would appreciate it if you could take a look and let me know why Inflate1
doesn't work in release mode. Why do we have to create local copies?
NET Native is a precompilation technology for building and deploying UWP apps. . NET Native is included with Visual Studio 2015 and later versions. It automatically compiles the release version of UWP apps that are written in managed code (C# or Visual Basic) to native code.
. NET uses two compilers, Roslyn, to compile C# or VB code into CIL (common intermediate language), and RyuJIT, to run just-in-time compilation of CIL into native code.
NET Framework is a software development framework for building and running applications on Windows. . NET Framework is part of the . NET platform, a collection of technologies for building apps for Linux, macOS, Windows, iOS, Android, and more.
NET Framework are written in a particular programming language and compiled into intermediate language (IL). At run time, a just-in-time (JIT) compiler is responsible for compiling the IL into native code for the local machine just before a method is executed for the first time.
NET 7 is fast. Really fast. A thousand performance-impacting PRs went into runtime and core libraries this release, never mind all the improvements in ASP.NET Core and Windows Forms and Entity Framework and beyond. It's the fastest .
Pretty unclear to me why this question has a bounty. Yes, it is a bug as @Matt told you. He knows, he works on .NET Native. And he documented the temporary workaround, use an attribute to prevent the method from getting inlined by the optimizer. A trick that often works to sail around optimizer bugs.
using System.Runtime.CompilerServices;
....
[MethodImpl(MethodImplOptions.NoInlining)]
public Rectangle2D Inflate1(double value)
{
// etc...
}
They'll get it fixed, next major release is the usual promise.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With