Является ли это возможной ошибкой в ​​.Net Собственная компиляция и оптимизация?

Я обнаружил проблему с (что может быть) чрезмерной оптимизацией в .Net Native и structs. Я не уверен, что компилятор слишком агрессивен, или я слишком слеп, чтобы понять, что я сделал неправильно.

Чтобы воспроизвести это, выполните следующие действия:

Шаг 1. Создайте новое приложение Blank Universal (win10) в Visual Studio 2015 Обновление 2 таргетинга 10586 с минимальной сборкой 10240. Вызовите проект NativeBug, чтобы мы имели одно и то же пространство имен.

Шаг 2: откройте MainPage.xaml и вставьте эту метку

<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>

Шаг 3: скопируйте/вставьте следующее в 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);
        }
    }
}

Шаг 4. Запустите приложение в Debug x64. Вы должны увидеть эту метку:

42,5, 42,5 = > 107,5, 107,5

Шаг 5. Запустите приложение в Release x64. Вы должны увидеть эту метку:

-7,5, -7,5 = > 7,5, 7,5

Шаг 6: раскомментируйте line 45 в MainPage.xaml.cs и повторите шаг 5. Теперь вы видите оригинальную метку

42,5, 42,5 = > 107,5, 107,5


Прокомментировав line 45, код будет использовать Rectangle2D.Inflate2(...), который будет точно таким же, как Rectangle2D.Inflate1(...), за исключением того, что он создает локальную копию вычислений перед отправкой их в конструктор Rectangle2D. В режиме отладки эти две функции одинаковы. В релизе, однако, что-то оптимизируется.

Это была неприятная ошибка в нашем приложении. Код, который вы видите здесь, был удален из гораздо большей библиотеки, и я боюсь, что может быть больше. Прежде чем сообщить об этом Microsoft, я был бы признателен, если бы вы могли взглянуть и сообщить мне, почему Inflate1 не работает в режиме выпуска. Почему мы должны создавать локальные копии?

Ответы

Ответ 1

Довольно непонятно, почему этот вопрос имеет щедрость. Да, это ошибка, о которой сказал @Matt. Он знает, он работает на .NET Native. И он документировал временное обходное решение, используя атрибут, чтобы предотвратить включение метода оптимизатором. Трюк, который часто работает, чтобы плыть вокруг ошибок оптимизатора.

using System.Runtime.CompilerServices;
....
    [MethodImpl(MethodImplOptions.NoInlining)]
    public Rectangle2D Inflate1(double value)
    {
        // etc...
    }

Они будут исправлены, следующий основной релиз - обычное обещание.