Is this a possible bug in .Net Native compilation and optimization?
Asked Answered
D

1

14

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?

Decontaminate answered 19/6, 2016 at 14:15 Comment(5)
The big advantage of a struct is that code that uses them can always be heavily optimized. The big problem with that is that they have historically been the #1 source of optimizer bugs. Just send the report.Lumbricalis
Thanks @HansPassant, I just sent Microsoft an email.Decontaminate
Funny thing, if you change the foreach loop on line 30 to a for loop, it fixes the problem as well. Weird.Decontaminate
You may also want to report the bug on connect.microsoft.comEyecup
I see your email report and have reproduced the issue. The trouble seems to be somewhere in Inflate1 but I'm still tracking down the details. In the mean time you can add this attribute to Inflate1 to work around the issue: [MethodImpl(MethodImplOptions.NoOptimization|MethodImplOptions.NoInlining)]Treenware
L
4

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.

Lumbricalis answered 19/6, 2016 at 14:16 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.