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?
foreach
loop online 30
to afor
loop, it fixes the problem as well. Weird. – Decontaminate