当今将Windows应用程序迁移到Windows on Arm的实践

目录

仿真层

示例应用程序的架构

构建x86 WPF应用程序

为ARM64编译项目

在Surface上部署包

完成Mandelbrot应用程序的UWP版本

将两个应用程序部署到Surface

总结


在本文中,我们将演示示例应用程序在仿真下的性能影响,并演示如何将现有代码库移植到Windows on Arm。我们将展示如何使用.NET框架4.8设置您的开发环境以面向ARM64处理器。

我们开始看到Windows 10 for Arm的采用加速通过市场上可用的许多新设备,这些设备在基于Arm的设备上运行,例如Microsoft Surface Pro X、三星Galaxy Book S和联想 Yoga 5G设备。Windows 10 on Arm完全支持运行x86 win32应用程序,包括Windows窗体应用程序和Windows Presentation Foundation (WPF)应用程序。主要优势是Arm处理器惊人的电池寿命。我在本文中使用的设备是在ARM64处理器上运行的Surface Pro X

虽然这些应用程序确实在Windows 10 on Arm下运行,但为英特尔处理器编译的应用程序在x86X64仿真层下的这些设备上运行。当然,在仿真上运行的应用程序比本机运行的ARM64应用程序要慢。

在本文中,我将从执行许多浮点计算的WPF应用程序开始。我们可以为x86x64编译它。当应用程序在Surface Pro X上执行时,它将在x86仿真下运行。

然后我将演示如何将此应用程序移植到UWP应用程序并将此UWP应用程序部署为在ARM64处理器上运行。

最后,我将比较在仿真下运行的WPF应用程序与在Windows 10 on Arm上本机运行的UWP之间的运行时性能结果。

对于本文,我假设您具有.NET知识(代码使用C#编写),以及一些WPFUWPXAML 知识。所有示例代码都可以在GVerelst/Mandelbrot (github.com) 找到

仿真层

.NET Framework编译的应用程序可以在ARM64 Windows系统上运行,但代码将在模拟器中执行。这允许Arm上的Windows 10运行几乎所有代码,但不是以最佳方式运行。

对于x86应用程序,与在x86 Windows上运行没有区别。x86系统DLL然后通过Windows on Windows (WoW)应用程序层。该层包含一个x86Arm CPU仿真器,它将使x86代码在Arm处理器上运行。之后,继续正常执行到系统服务和内核。

x86指令在运行时转换为ARM64并缓存在磁盘上以供进一步使用。这意味着不需要特殊安装。尽可能限制非本地代码的数量,这解释了为什么它仍然可以运行得相当快。但这不会像本地ARM64应用程序运行得那么快。要了解更多信息,请参阅Microsoft Windows开发人员文档中的x86仿真如何在 ARM工作

但是,如果您可以为Arm处理器编译二进制文件,则它们可以直接与本机系统DLL对话。这些反过来与系统服务对话,如果需要,内核会从那里接管。不需要模拟。这显然是最佳方案。

示例应用程序的架构

要查看性能差异,我们需要一个进行大量计算的应用程序。为了使其具有图形吸引力,我选择了著名的Mandelbrot集的表示。维基文章准确地解释了这些点是如何计算的,但以下是我们需要知道的重要信息:

屏幕上的每个点都是通过在该点上重复执行一个小函数来计算的,直到该点到原点的距离大于2,或者达到最大迭代次数。在达到最大迭代次数后没有从2个单位圆逃逸的点在Mandelbrot集中,其他点不在。一个点在转义之前调用该函数的次数将决定它的颜色。

这里的重点是,对于每个点,需要执行大量的浮点计算来确定其颜色。这是应用程序的样子:

更多的迭代意味着更多的处理时间。在这种情况下,我们使用了5000次迭代,在我的开发PC上花费了3.824秒。我们将使用它作为我们的基准。我们只计算实际计算的时间,而不是将其输出到位图并显示的时间。

橙色表面上的每个点都需要计算,因此计算次数也取决于位图大小。它就像一个隐藏的参数。

我为计算创建了一个单独的Mandelbrot库。该库在两个项目之间共享,因此为计算执行的代码完全相同。

WPF应用程序使用此库并由一个主窗口和一个绘图区域和一些控件组成。单击开始计算按钮调用计算函数并输出结果。

UWP应用程序将执行相同的操作,但由于UWP的性质而进行了一些更改。

对于这个项目,我使用了最新版本的Visual Studio 2019.NET Framework 4.8

VS2019安装程序中,验证您是否已安装“.NET桌面开发通用Windows平台开发工作负载。

 

构建x86 WPF应用程序

初始应用程序是使用我单独创建的Mandelbrot库的WPF应用程序。我们可以在Surface上运行它。它不是专门为ARM64编译的,因此它将在x86仿真模式下运行。

要部署WPF应用程序,我们只需为任何CPU”编译解决方案,然后将包含可执行文件和DLL的应用程序的bin文件夹复制到Surface上的文件夹中。

在当前的.NET Framework 5中,无法直接为ARM64编译WPF应用程序。Microsoft计划在.NET 5或可能的.NET 6的未来版本中包含WPF ARM64支持。

现在,我们可以使用UWP创建一个可以编译为ARM64的应用程序。我们将使用.NET Framework 4.8在我们的解决方案中创建一个新的UWP项目。UWP应用程序可以作为本机ARM64应用程序发布,无需仿真即可运行。

UWP应用程序中,我们将尽可能多地重用WPF代码。

创建新的UWP应用程序的过程非常简单:

  1. 右键单击解决方案。
  2. 选择添加>新建项目...
  3. 在窗口顶部的搜索框中,键入“UWP”。
  4. 选择空白应用程序(通用Windows
  5. 选择正确语言的项目。在本文中,我将使用C#。
  6. 点击下一步。在下一页上,为项目命名(Mandelbrot.UWP),保留默认位置并单击Create

当我们运行这个项目时,它显示一个空窗口。

ARM64编译项目

在我们开始编码之前,让我们发布项目并将其安装在Surface上。当我们运行项目时,它应该在ARM64本机模式下运行。

首先,右键单击UWP项目,然后选择发布>创建应用程序包...

在第一页,我们选择分配方法。在我们开发时选择Sideloading。另一种选择是Microsoft Store,它带来了其他挑战。保持未选中启用自动更新,然后单击下一步

Signing Methods页面上,保留默认值并单击Next

在最后一页使用以下设置:

我只选择ARM64,因为这个选项与其他选项是互斥的。您不能将ARM64与此页面上的其他选项一起选择。

最后,点击创建。该项目现在已针对ARM64进行编译,并且在c:\temp\deploy中创建了包文件。

打包时,检查文件夹的内容。请注意,我们现在已经创建了一个包含调试信息的包,因为我们没有将配置设置为Release。现在,这没问题。对于基准测试,我们将使用发布模式。

Surface上部署包

Surface Pro X上,将在Deploy文件夹中创建的版本复制到Surface可以访问的文件夹。我使用的是OneDrive,但您可以使用任何您喜欢的方法。一个简单的USB驱动器即可工作。

在我们安装包之前,我们需要将Surface置于开发者模式:

Windows搜索框中(在开始按钮旁边)键入开发人员设置,然后打开设置。这将带您进入“Windows安全设置页面。

交换机开发模式上,这样你就可以直接在表面上安装UWP应用程序,而不必通过Windows应用商店。

完成后,转到共享文件夹并启动Install.ps1脚本。这将执行必要的步骤来部署空应用程序。

我们现在可以从Windows开始菜单启动应用程序。当应用程序启动时,启动任务管理器。请注意,UWP应用程序本机运行——没有模拟!WPF版本在32位仿真模式下运行。

完成Mandelbrot应用程序的UWP版本

现在我们已经证明了UWP版本在Surface上以Native模式运行,我们可以实现与WPF版本中相同的功能。我们将在两个项目中使用相同的计算库。计时将仅根据库中的计算进行。

我们从具有以下结构的WPF应用程序开始:

UWP应用程序的用户界面是XAML驱动的,因此大部分代码都可以重用。不过,并非所有API都相同。在WPF应用程序中,我使用MVVM模式从表示中分离功能。这将在转换期间得到奖励。

Mandelbrot.UWP中,通过右键单击引用来引用Mandelbrot.Calculations项目,然后选择添加引用…”选择Mandelbrot.Calculations”复选框以包含该项目。

接下来,通过右键单击项目,在UWP项目下创建一个名为ViewModels的文件夹,然后选择Add > New Folder。重复此操作以创建一个名为Extensions的文件夹。

将文件MandelbrotParameters.csMandelbrot.WPF/ViewModels复制到Mandelbrot.UWP/ViewModels

为了代码整洁,请通过将命名空间更改为Mandelbrot.UWP.ViewModels来修复命名空间。这不是强制性的,但建议这样做。

WPF应用程序在WPF窗口内运行,但UWP应用程序在页面内运行,因此我们必须修复数据类型:

class MandelbrotParameters : INotifyPropertyChanged
{
    private readonly MainPage _window;
    public MandelbrotParameters(MainPage window)
    {
        _window = window;
        Reset();
    }
// ...

您可能想将名称更改_window_page,但为了保持两个代码库相似,我决定不这样做。我也可以从使用Page而不是WindowWPF应用程序开始。但这是一个次要的转换步骤。

RelayCommand.cs复制到UWP项目。也修复命名空间。

现在让我们修复XAML。我们不能仅仅从WPF项目中复制XAML文件,因为它描述了一个Window。但是,我们可以复制XAML元素下的主要<Window>元素。所以选择WPF中的顶部<Grid>元素,用它替换UWP中的顶部<Grid>元素。

UWP不知道<Label>元素,所以让我们<Label><TextBlock>元素替换所有元素。为每个这样做<Label>

WPF版本

<Label Content="Top:" Grid.Row="1" Grid.Column="0" VerticalAlignment="Top" Margin="0,0,0,5" HorizontalAlignment="Right"
/>

UWP版本

<TextBlock Grid.Row="1" Grid.Column="0" VerticalAlignment="Top" Margin="0,0,0,5" HorizontalAlignment="Right" >
Top: </TextBlock>

UWP中,数据绑定默认为OneWay。也就是说,如果你在代码中设置绑定到控件的属性的值,它会反映在页面中,但是如果你在UI中更改控件的值,它的值将不会被发送回绑定的属性。现在每个{Binding xxx}元素都需要变成{Binding xxx, Mode=TwoWay}。这是顶部TextBox的示例:

<TextBox HorizontalAlignment="Left" VerticalAlignment="Center" Width="51" Grid.Row="1" Grid.Column="1" Margin="0,0,0,5" Text="{Binding Top, Mode=TwoWay}"/>

MainPage类中,将设置DataContext为一个新MandelbrotParameters实例。我们将此作为参数传递给我们的ViewModel调用DrawImage方法的可能性。

public MainPage()
{
    this.InitializeComponent();
    DataContext = new MandelbrotParameters(this);
}

DrawImage方法从WPF MainWindow.cs复制到UWP MainPage.cs。这里我们看到一个示例,说明UWP API并不总是与WPF API相同:WriteableBitmap构造函数不接受六个参数,而只有两个:宽度和高度。修复很简单:删除所有其他参数。

复制GetImageViewPort也一样。此方法不需要更改。

扩展文件夹中复制WriteableBitmapExtensions.cs。您会看到编译错误,因为UWP中的位图API与其对应的WPF略有不同。替换SetPixels方法:

这是原始的WPF版本:

public static void SetPixels(
    this WriteableBitmap wbm,
    IEnumerable<FractalPoint> pts)
{
    wbm.Lock();
    IntPtr buff = wbm.BackBuffer;
    int Stride = wbm.BackBufferStride;

    unsafe
    {
        byte* pbuff = (byte*)buff.ToPointer();

        foreach (FractalPoint pt in pts)
        {
            System.Drawing.Color c = pt.Color;
            int loc = pt.Point.Y * Stride + pt.Point.X * 4;
            pbuff[loc] = c.B;
            pbuff[loc + 1] = c.G;
            pbuff[loc + 2] = c.R;
            pbuff[loc + 3] = c.A;
        }
    }

    wbm.AddDirtyRect(new Int32Rect(0, 0, (int)wbm.Width, (int)wbm.Height));
    wbm.Unlock();
}

新的UWP版本应如下所示:

public static void SetPixels(
    this WriteableBitmap wbm,
    IEnumerable<FractalPoint> pts)
{
    byte[] imageArray = new byte[(int)(wbm.PixelWidth * wbm.PixelHeight * 4)];
    int i = 0;
    foreach (var p in pts)
    {
        imageArray[i] = p.Color.B;
        imageArray[i + 1] = p.Color.G;
        imageArray[i + 2] = p.Color.R;
        imageArray[i + 3] = p.Color.A;

        i += 4;
    }
    using (Stream stream = wbm.PixelBuffer.AsStream())
    {
        //write to bitmap
        stream.Write(imageArray, 0, imageArray.Length);
    }
}

将两个应用程序部署到Surface

现在我们将这两个应用程序部署到表面,以便我们可以比较性能。

确保使用发布模式并重建解决方案。

执行我们之前采取的步骤将UWP应用程序部署到Surface。对WPF项目执行相同的操作。

让应用程序在Arm 64位代码中本地运行是否值得所有的麻烦?让我们来看看。

为了计算Mandelbrot点,我最大化了应用程序以确保我们使用相同数量的计算进行比较,并保留默认参数,除了迭代计数设置为5000

WPF应用程序需要35.671来计算Mandelbrot点。

UWP应用程序只用了3.203!这大约11

总结

Native模式开发是完全值得的。我从一个WPF应用程序开始,其中的职责很好地分离。这是通过使用MVVM模式,并分离出位图生成和计算的代码而获得的。因此,在UWP应用程序中进行的修改很少。

我没有探索在使用并行性时这是如何表现的。这些点都是按顺序计算的。看看这是否会改变结果以及并行性是否对性能有很大影响可能会很有趣。

要了解更多信息,这里有一些代码示例和教程,深入探讨了示例应用程序中代码背后的一些主题。

https://www.codeproject.com/Articles/5293252/Todays-Best-Practices-for-Migrating-Windows-apps-t

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值