65.9K
CodeProject 正在变化。 阅读更多。
Home

Arm64 上的 .NET 性能

starIconstarIconstarIconstarIconstarIcon

5.00/5 (3投票s)

2023 年 9 月 8 日

CPOL

8分钟阅读

viewsIcon

6359

downloadIcon

42

本文演示了如何利用 Arm64 运行 .NET 应用程序,从而获得原生架构的优势,例如更低的功耗和更快的速度。

Arm64(通常称为 AArch64)提供了一种经过功耗优化的架构,它是许多片上系统 (SoC) 的基础。SoC 集成了 CPU、内存、GPU 和 I/O 设备,可在各种行业、应用和设备中实现功耗高效的计算操作。由于其便携性和低功耗,Arm64 架构非常适合移动设备。然而,笔记本电脑和台式机也开始采用 Arm64。

Microsoft Windows 11 通过支持 Arm64 并提供多种简化应用程序移植的功能,加速了这一趋势。具体而言,Windows 11 提供了 Windows on Arm (WoA) 来通过原生 Arm64 方法高效运行 Python 应用程序,而 Arm64EC(兼容性模拟)则有助于逐步将您的 x64 应用程序移植到 Arm64。此外,包括 Qt 在内的许多框架现在都使用 Windows on Arm (WoA) 来原生运行 基于 UI 的应用程序。为了帮助开发人员,Microsoft 推出了 Windows Dev Kit 2023,它提供了一款便捷的 Arm64 驱动的设备。

Arm64 在使用 C++ 和 Python 时提供了原生优势,但在其他框架上也提供了众多好处。例如,.NET 是一个跨平台开发框架,支持 Windows、Linux 和 MacOS。将该框架与 Arm64 结合使用,即可在多个平台上实现高效的应用程序。

本文演示了如何利用 Arm64 运行 .NET 应用程序,从而获得原生架构的优势,例如更低的功耗和更快的速度。您可以设置 .NET 开发环境,并了解通过在 Arm64 上原生运行代码所能预期的性能提升。下载 配套代码以进行学习。

环境设置

要设置您的开发环境,您需要以下内容

首先,在您的 Arm64 设备上安装适用于 Windows 的 .NET 8.0 SDK 的两个架构版本,即 x64Arm64。该 SDK 目前为预览版本(在此 处下载)。为确认安装成功,请打开命令提示符窗口并输入

dotnet --info

这将产生以下输出。

默认情况下,dotnet 命令在 Arm64 设备上运行时使用 Arm64 架构。但是,它会识别出在 **找到的其他架构** 列表中也存在 x64 架构。

安装 .NET SDK 后,将 适用于 Windows 的 Visual Studio Code 作为您的 IDE 安装。选择 **Arm64** 的 **用户安装程序**,然后启动安装程序。使用默认设置。安装完成后,选择您的颜色主题。最后,使用 64 位独立安装程序安装 适用于 Windows 的 Git

基准测试 .NET 应用程序

**.NET 团队提供了一套 基准测试,您可以使用它们来评估不同架构上各种 .NET 版本的性能。这些基准测试依赖于 BenchmarkDotNet 库,该库提供了一个框架来简化代码执行时间的测量。**

您可以使用 C# 属性将这些测量添加到您的代码中。该库会评估执行时间并报告计算时间的平均值和标准差。此外,该库还可以生成图表,帮助您评估代码性能。所有这些功能在 .NET 性能基准测试中都可用。

要使用这些基准测试,请先克隆 `dotnet performance` 仓库

git clone https://github.com/dotnet/performance.git

然后,导航到 *performance\src\benchmarks\micro*,如下所示。

在 *performance\src\benchmarks\micro* 中,输入以下命令

dotnet run -c Release -f net8.0

应用程序将成功构建并启动,显示可用的性能基准测试列表。

现在,输入“475”并按 **Enter** 启动 C# 字符串数据类型的性能测试。此操作的结果如下(向上滚动以查看此表)。

默认情况下,该表汇总了性能测试结果。您可以看到每个性能测试的执行时间和统计数据(平均值、中位数、最小值、最大值和标准差)。这为您提供了关于代码性能的全面信息。

与控制台应用程序一样,dotnet run 命令默认使用 Arm64 架构。要使用 x64 启动性能测试,请使用 -a 开关

dotnet run -c Release -f net8.0 -a x64

然而,此时,BenchmarkDotNet 库与 Arm64 计算机上的 .NET SDK for x64 不兼容。因此,BenchmarkDotNet 会报告错误和不正确的执行时间。

要比较 x64 和 Arm64 上的 .NET 性能,请使用控制台应用程序模板并实现您自己的自定义基准测试。

自定义基准测试

要实现自定义基准测试,请使用 System.Diagnostics.Stopwatch。首先创建控制台应用程序 `Arm64.Performance`(`dotnet new console -o Arm64.Performance`)。然后,在 Visual Studio 中打开它。接着通过点击 **新建文件** 并键入“PerformanceHelper.cs”作为文件名来创建一个新文件。

然后,打开 *PerformanceHelper.cs*,并定义 PerformanceHelper

using System.Diagnostics;
 
namespace Arm64.Performance
{
    public static class PerformanceHelper
    {
        private static readonly Stopwatch stopwatch = new();
 
        public static void MeasurePerformance(Action method, int executionCount, string label)
        {
            stopwatch.Restart();
 
            for(int i = 0; i < executionCount; i++)
            {
                method();
            }
 
            stopwatch.Stop();
 
            Console.WriteLine($"[{label}]: {stopwatch.ElapsedMilliseconds.ToString("f2")} ms");
        }
    }
}

PerformanceHelper 类是静态的。它有一个方法 `MeasurePerformance`,该方法通过调用提供的函数来工作,使用 Action 委托。此委托作为 `MeasurePerformance` 方法的第一个参数传递。您会调用它 `executionCount` 次,具体次数由该方法的第二个参数指定。之后,`MeasurePerformance` 方法会打印执行特定代码所需的时间。此外,`MeasurePerformance` 还接受第三个参数 `label`,您可以使用它来传递描述性能测试的字符串。

通过创建一个名为 *PerformanceTests.cs* 的新文件来定义性能测试,在该文件中声明 PerformanceTests

namespace Arm64.Performance
{
    public static class PerformanceTests 
    { 
    }
}

此类是空的。您将在下一节中对其进行扩展。

列表排序

列表排序的性能测试显示了对包含 100,000 个双精度类型元素的列表进行排序所需的时间。您可以使用 System.Random中提供的伪随机数生成器来准备列表。

要实现此测试,请在 PerformanceTests 类中添加以下代码

public static class PerformanceTests
{ 
    private static readonly Random random = new();
 
    private static List<double> PrepareList()
    {
        const int listSize = 100000;
 
        return Enumerable.Range(0, listSize)
                    .Select(r => random.NextDouble())
                    .ToList();
    }
 
    public static void ListSorting()
    {
        var list = PrepareList();
 
        list.Sort();
    }    
}

有三个新元素

  • 声明和初始化 `private static random` 成员。这是一个伪随机数生成器的实例。
  • 一个 `private static PrepareList` 方法创建一个包含 100,000 个双精度类型元素的伪随机列表。要生成此列表,请使用 `System.Linq` 中的 Enumerate.Range 方法。伪随机数生成器会创建此列表的每个元素。
  • 一个公共静态 `ListSorting` 方法首先调用 `PrepareList` 辅助方法来创建随机列表。然后,`Sort` 方法对该随机列表进行排序。

矩阵乘法

现在,您将实现方阵乘法。首先,使用 `GenerateRandomMatrix` 方法扩展 `PerformanceTests` 类的定义。将此方法添加到 *PerformanceTests.cs* 文件中 `ListSorting` 的后面

private static double[,] GenerateRandomMatrix()
{
    const int matrixSize = 500;
 
    var matrix = new double[matrixSize, matrixSize];
 
    for (int i = 0; i < matrixSize; i++)
    {
        for (int j = 0; j < matrixSize; j++)
        {
            matrix[i, j] = random.NextDouble();
        }
    }
 
    return matrix;
}

此方法生成一个 500x500 的方阵。一个双重 `for` 循环,每一步都调用 `random.NextDouble`,生成一个伪随机生成的双精度类型值。

接下来,在 `PerformanceTests` 类中,添加以下方法

private static double[,] MatrixMultiplication(double[,] matrix1, double[,] matrix2)
{
    if (matrix1.Length != matrix2.Length)
    {
        throw new ArgumentException("The matrices must be of equal size");
    }
 
    if (matrix1.GetLength(0) != matrix1.GetLength(1) || matrix2.GetLength(0) != matrix2.GetLength(1))
    {
        throw new ArgumentException("The matrices must be square");
    }
 
    int matrixSize = matrix2.GetLength(0);
 
    var result = new double[matrixSize, matrixSize];
 
    for (int i = 0; i < matrixSize; i++)
    {
        for (int j = 0; j < matrixSize; j++)
        {
            result[i, j] = 0;
 
            for (int k = 0; k < matrixSize; k++)
            {
                result[i, j] += matrix1[i, k] * matrix2[k, j];
            }
        }
    }
 
    return result;
}

`MatrixMultiplication` 方法以两个方阵作为输入,然后使用 数学公式 计算乘积。使用三个 `for` 循环,`result` 变量存储矩阵乘法的结果,该结果由 `MatrixMultiplication` 方法返回。

最后,在 `PerformanceTests` 类中,您将实现以下公共方法,该方法生成两个方阵,然后计算它们的乘积

public static void SquareMatrixMultiplication()
{
    var matrix1 = GenerateRandomMatrix();
    var matrix2 = GenerateRandomMatrix();
 
    MatrixMultiplication(matrix1, matrix2);
}

字符串操作

对于最后一个性能基准测试,请使用字符串操作。在 `PerformanceTests` 类中,定义 `loremIpsum` 变量,该变量存储 占位符文本 的片段

private static readonly string loremIpsum = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. " +
    "Curabitur ut enim dapibus, pharetra lorem ut, accumsan massa. " +
    "Morbi et nisi feugiat magna dapibus finibus. Pellentesque habitant morbi " +
    "tristique senectus et netus et malesuada fames ac turpis egestas. Proin non luctus lectus, " +
    "vel sollicitudin ante. Nullam finibus lobortis venenatis. Nulla sit amet posuere magna, " +
    "a suscipit velit. Cras et commodo elit, nec vestibulum libero. " +
    "Cras at faucibus ex. Suspendisse ac nulla non massa aliquet sagittis. " +
    "Fusce tortor enim, feugiat ultricies ultricies at, viverra et neque. " +
    "Praesent dolor mauris, pellentesque euismod pharetra ut, interdum non velit. " +
    "Fusce vel nunc nibh. Sed mi tortor, tempor luctus tincidunt et, tristique id enim. " +
    "In nec pellentesque orci. Nulla efficitur, orci sit amet volutpat consectetur, " +
    "enim risus condimentum ex, ac tincidunt mi ipsum eu orci. Maecenas maximus nec massa in hendrerit.";

然后,实现 `StringOperations` 公共方法

public static void StringOperations()
{
    loremIpsum.Split(' ');
 
    loremIpsum.Substring(loremIpsum.LastIndexOf("consectetur"));
 
    loremIpsum.Replace("nec", "orci");
 
    loremIpsum.ToLower();
 
    loremIpsum.ToUpper();
}

此方法使用空格分隔符将占位符文本拆分为子字符串。然后,它获取从单词 `consectetur` 的最后一个索引开始的子字符串。接下来,它用 `orci` 替换 `nec`,将字符串转换为小写,然后再转换为大写,以模拟 C# 应用程序中对字符串变量的典型操作。

整合

您现在可以在控制台应用程序中使用这些性能测试。通过将默认内容(`Console.WriteLine("Hello, World!");`)替换为以下语句来修改 *Program.cs* 文件

using Arm64.Performance;
 
Console.WriteLine($"Processor architecture: " +
    $"{Environment.GetEnvironmentVariable("PROCESSOR_ARCHITECTURE")}");
 
const int trialCount = 5;
 
for ( int i = 0; i < trialCount; i++ )
{
    Console.WriteLine($"Trial no: {i + 1} / {trialCount}");
 
    PerformanceHelper.MeasurePerformance(PerformanceTests.ListSorting, 
        executionCount: 500, "List sorting");
 
    PerformanceHelper.MeasurePerformance(PerformanceTests.SquareMatrixMultiplication, 
        executionCount: 10, "Matrix multiplication");
 
    PerformanceHelper.MeasurePerformance(PerformanceTests.StringOperations, 
        executionCount: 500000, "String operations");
}

此代码导入了 `Arm64.Performance` 命名空间,您在该命名空间中定义了 `PerformanceHelper` 和 `PerformanceTests` 类。然后,代码会打印处理器架构,Arm64 或 AMD64,具体取决于运行应用程序的 SDK 架构。

您有一个常量 `trialCount`,您可以使用它来指定性能测试集的执行次数。对每个测试批次运行 `ListSorting` 500 次,然后执行 `SquareMatrixMultiplication` 10 次,`StringOperations` 500,000 次。这样可以实现可比的测试批次执行时间。单个矩阵乘法比单个字符串操作慢。因此,后者必须执行更多次数。

Arm64 上的 .NET 性能提升

您现在可以启动应用程序以评估其在不同架构上的性能。首先,使用 Arm64 运行应用程序。在 `Arm64.Performance` 文件夹中,输入

dotnet run -c Release

这将启动控制台应用程序,您应该会看到以下输出。

现在,为了比较执行时间,请使用 x64 架构启动应用程序

dotnet run -c Release -a x64

此命令将导致以下输出

与在 Arm64 上原生执行相比,模拟的 x64 上的所有操作都花费了更多时间。平均而言,原生执行在列表排序方面提供了约 15% 的性能改进,在矩阵乘法方面提高了 291%,在字符串操作方面提高了 239%。

下表总结了 x64 和 Arm64 原生执行代码的平均执行时间。

该图显示执行时间显著提高。

结论

本文演示了如何使用 .NET SDK 进行跨平台控制台应用程序开发。它解释了如何创建项目应用程序模板并使用不同的处理器架构(x64 或 Arm64)启动应用程序。

然后,它展示了如何使用标准和自定义代码来基准测试 .NET 应用程序。后者用于演示在 Arm64 驱动的设备上原生执行代码时性能的显着提升——矩阵乘法速度近乎三倍。在 x64 上运行的代码必须使用仿真层,这会消耗额外的 CPU 和内存。没有这个额外的层,原生的 Arm64 就能获得性能优势和更好的效率。

现在您已经学会了如何利用 Arm64 的强大功能,开始为您的 .NET 应用程序使用 Arm64

© . All rights reserved.