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

将 Arm64EC 与 Windows 11 结合使用

emptyStarIconemptyStarIconemptyStarIconemptyStarIconemptyStarIcon

0/5 (0投票)

2023 年 5 月 22 日

CPOL

9分钟阅读

viewsIcon

6342

本文演示了如何在 C++ 应用程序中使用 Arm64EC。您将实现的应用程序将执行两个伪随机生成的方形矩阵的乘法。

随着复杂的机器学习和人工智能模型的出现,您需要快速且节能的硬件。为了实现快速高效的硬件,Arm 推出了大多数嵌入式和移动系统芯片所使用的架构。它已被应用于笔记本电脑、台式机和服务器,并能加速您的应用程序。

传统上,应用程序是为特定平台编译的,并且需要您无法完全控制的第三方库。因此,将基于 Intel (x86 或 x64) 的应用程序移植到 Arm 处理器上通常很困难。您必须重新编译整个应用程序及其所有依赖项,而许多应用程序的依赖项(静态或动态)尚未移植,这使得将完整应用程序迁移到 Arm64 变得非常困难。

为了帮助解决其中一些问题,Windows 11 提供了 Arm64EC (Emulation Compatible)。Arm64EC 是适用于在 Windows 11 上的 Arm 设备上运行的应用程序的应用程序二进制接口 (ABI)。使用 Arm64EC,您可以选择要移植和不移植的组件,从而更轻松地将应用程序迁移到 Arm64 设备。

Arm64EC 使您能够将 x64 应用程序移植到 Arm 驱动的设备上,以利用其原生速度和性能。与仅针对 Arm64 进行编译相反,您可以在同一进程中混合 Arm64 和 x64 二进制文件。因此,您可以逐步移植应用程序,以受益于 Arm 驱动的设备。这包括更低的功耗、更长的电池续航时间以及加速的人工智能 (AI) 和机器学习 (ML) 处理。

本文演示了如何在 C++ 应用程序中使用 Arm64EC。您将实现的应用程序将执行两个伪随机生成的方形矩阵的乘法。您将针对不同的矩阵维度运行该算法,并重复计算几次以测量应用程序性能。最后,为了比较性能,您将使用不同的配置运行该应用程序:x86、x64 和 Arm64EC。

使用 Arm64EC 进行应用程序移植

本节将介绍如何使用 Arm64EC 移植应用程序。这篇入门文章使用了一个相对简单的应用程序。但是,您可以在此处找到一个实际场景。它还解释了 Arm64EC ABI 的工作原理,演示了 Arm64EC 的实际应用,并逐步解释了移植过程,让您了解在实际场景中这可以多么直接。

工作原理

Arm64EC ABI 允许与现有 x64 依赖项进行透明且直接的互操作,这意味着您可以在同一进程中使用 x64 和 Arm64EC。这是因为 Arm64EC 代码的编译方式与 x64 代码使用相同的预处理器架构定义,并且 Arm64EC ABI 与 X64 ABI 兼容。因此,您可以通过采用 Arm 架构来加速计算密集型代码,从而在不立即移植所有内容的情况下获得性能提升。

下图展示了性能改进的一个例子。通过将构建目标从 x86 更改为 x64,您可以将计算时间缩短近 10%(平均而言)。通过将构建目标更改为 Arm64EC 并重新编译,您可以获得进一步的性能提升。Arm64EC 为最小的矩阵(100x100 元素)提供了最佳的性能提升。

当您从 x64 切换到 ARM64EC 时,无需进行任何代码更改即可获得性能优势。切换到 Arm64EC 意味着加载时间会缩短,内存加载会减少,性能会提高,因为 Arm64 系统库将直接使用,无需模拟。

此实现使用了一个简单的算法。因此,您可以通过使用 Arm 特定处理器指令进行进一步优化。

  • 应用程序可以在 CMake 文件中引用 x64 依赖项。依赖项会加载到同一个进程中。您可以使用此功能逐步将计算密集型依赖项转换为 Arm64EC,以提高整体应用程序性能。

运行应用程序后,您应该会在任务管理器窗口中看到 Arm64 (x64 兼容),如下图所示。

Windows 11 中的 Arm64EC ABI

为了实现 Arm64 和 x64 之间的互操作性,Windows 11 在经典的 Arm64 ABI 中引入了四项更改。这些包括:

寄存器映射和阻塞寄存器使用 CONTEXT 结构,该结构定义了给定时间的 CPU 状态。该结构包括 x64 和 Arm64EC 代码。共享结构意味着 Arm64 寄存器被有效地映射为 x64 寄存器。映射在 ARM64EC_NT_CONTEXT 下定义。换句话说,这些结构模拟了 Arm64 中的 x64。

调用检查器用于验证被调用函数的架构。调用检查器会验证要调用的函数是 x64 还是 Arm64EC。x64 函数在模拟下调用。

Arm64EC 和 x64 使用不同的堆栈检查器来验证为堆栈中的函数分配的区域。

虽然 Arm64EC 其他方面遵循经典 Arm64 ABI 调用约定,但可变参数函数(也称为 varargs)遵循与 x64 可变参数类似的调用约定。例如,遵循 x64 可变参数调用约定,只有前四个寄存器 — x0x1x2x3 — 用于参数传递。有关 Arm64EC 文档中有关可变参数调用约定的关键规则,请阅读此处

Arm64EC 实际应用

现在,您将一个 Intel 应用程序移植到 Arm64EC 并在 Windows Dev Kit 2023(也称为 Volterra 设备)上运行它。您将本地构建项目,以了解如何包含无法移植到 Arm64EC 的 x64 依赖项。为了清晰起见,本文使用了一个相对简单的应用程序来演示您在移植时必须执行的步骤。

必备组件

本教程使用 Windows Dev Kit 2023 和 Visual Studio 2022 17.4,已安装了桌面开发和 C++ 工作负载。请注意,Windows Dev Kit 2023 预装了 Windows 11。您可以像使用其他任何 Windows 桌面一样使用它。安装 Visual Studio 2022 后,您可以使用 Volterra 作为您的开发环境。

要查看最终项目,请查看完整代码

本地构建项目

在 Visual Studio 中使用 **CMake Project** 模板创建新项目。或者,您也可以使用 **Console App C++** 项目模板。源代码是相同的。唯一区别在于平台配置

在下一个屏幕上,按如下方式配置项目:

  • 项目名称:Volterra.Matrix
  • 位置:任何本地文件夹
  • 解决方案:创建新解决方案
  • 解决方案名称:Volterra.Matrix

创建解决方案后,有四个重要文件。两个标准文件实现了应用程序:Volterra.Matrix.hVolterra.Matrix.cpp。还有两个 CMake 文件:CMakeList.txtCMakePresets.json(如果 Visual Studio 未生成此文件,请使用此文件)。Visual Studio 使用后者配置应用程序,以便可以直接从 Visual Studio 构建和执行。当然,您也可以使用命令行:cl /arm64EC main.cpp

此处,您使用了 CMake Project 模板。因此,您有 CMake 文件。或者,您可以使用 Console Application 模板配合MSBuild

实现

要实现应用程序,首先在 Volterra.Matrix.cpp 中添加几个函数。具体来说,您必须创建 generateRandomSquareMatrix,它使用随机数生成器生成一个方形矩阵,该矩阵的元素是在 0 到 1 之间均匀分布的实数。

此函数定义在 includesusing namespace 语句之后

#include "Volterra.Matrix.h"
#include <chrono>
#include <random>

using namespace std;

double** generateRandomSquareMatrix(int size) {
    // Initialize random number generator
    random_device random_device;
    default_random_engine engine(random_device());
 
    // Prepare uniform distribution 
    uniform_real_distribution uniform_distribution;
 
    // Generate matrix
    double** matrix = new double*[size];
 
    for (int i = 0; i < size; i++) {
        matrix[i] = new double[size];
 
        for (int j = 0; j < size; j++) {
            matrix[i][j] = uniform_distribution(engine);
        }
    }
 
    return matrix;
}

然后,使用一个简单的算法实现矩阵乘法

double** squareMatrixProduct(double** matrix1, double** matrix2, int size)
{
    double** result = new double*[size];
 
    for (int i = 0; i < size; i++) {
        result[i] = new double[size];
 
        for (int j = 0; j < size; j++) {
 
            result[i][j] = 0;
 
            for (int k = 0; k < size; k++) {
                result[i][j] += matrix1[i][k] * matrix2[k][j];
            }
        }
    }
 
    return result;
}

之后,您必须添加两个辅助函数:printSquareMatrixreleaseSquareMatrix。第一个函数打印所有矩阵元素,第二个函数删除为给定矩阵分配的底层内存。

要测试代码,您需要实现另一个方法 simpleTest,该方法生成两个小的矩阵(3x3),将它们打印到控制台,然后计算它们的乘积

void simpleTest() {
    const int size = 3;

    double** matrix1 = generateRandomSquareMatrix(size);
    double** matrix2 = generateRandomSquareMatrix(size);
 
    cout << "Matrix1:" << endl;
    printSquareMatrix(matrix1, size);
 
    cout << endl << "Matrix2:" << endl;
    printSquareMatrix(matrix2, size);
 
    cout << endl << "Product:" << endl;
    double** product = squareMatrixProduct(matrix1, matrix2, size);
    printSquareMatrix(product, size);
 
    releaseSquareMatrix(matrix1, size);
    releaseSquareMatrix(matrix2, size);
    releaseSquareMatrix(product, size);
}

在将 simpleTest 函数添加到 main 函数后

int main(int argc, char** argv)
{
    simpleTest();
 
    return 0;
}

您应该会看到以下输出

x86 和 x64 依赖项

要包含任何剩余的 x64 依赖项,您可以使用 CMakeLists.txt 并使用 CMake 文档将其添加为库。

Arm64EC

假设一切就绪,请编译 Arm4EC 应用程序。包含依赖项作为库后,它们将保持不变。默认情况下,项目模板包含四个平台配置:

  • x86-debug
  • x86-release
  • x64-debug
  • x64-release

这些配置在 CMakePresets.json 文件中定义。要添加 Arm64EC 平台,请按如下方式补充 CMakePresets.json 文件:

{
  "version": 3,
  "configurePresets": [
    {
      "name": "windows-base",
      "hidden": true,
      "generator": "Ninja",
      "binaryDir": "${sourceDir}/out/build/${presetName}",
      "installDir": "${sourceDir}/out/install/${presetName}",
      "cacheVariables": {
        "CMAKE_C_COMPILER": "cl.exe",
        "CMAKE_CXX_COMPILER": "cl.exe"
      },
      "condition": {
        "type": "equals",
        "lhs": "${hostSystemName}",
        "rhs": "Windows"
      }
    },
    {
      "name": "x64-release",
      "displayName": "x64 Release",
      "inherits": "windows-base",
      "architecture": {
        "value": "x64",
        "strategy": "external"
      },
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "Release"
      }
    },
    {
      "name": "x86-release",
      "displayName": "x86 Release",
      "inherits": "windows-base",
      "architecture": {
        "value": "x86",
        "strategy": "external"
      },
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "Release"
      }
    },
    {
      "name": "Arm64EC-release",
      "displayName": "ARM64EC Release",
      "inherits": "x64-release",
      "architecture": {
        "value": "Arm64EC",
        "strategy": "external"
      },
      "environment": {
        "CXXFLAGS": "/arm64EC",
        "CFLAGS": "/arm64EC"
      }
    }
  ]
}

这会添加 Arm64EC-release 配置。此外,您还添加了两个环境变量:CSSFLAGSCFLAGS。在进行这些更改后,Visual Studio 会添加 Arm64EC 构建平台,该平台用于为 Arm64EC 架构构建应用程序。

性能测试

要评估性能提升,您必须实现两个用于测量代码执行时间的辅助函数

double msElapsedTime(chrono::system_clock::time_point start) {
    auto end = chrono::system_clock::now();
 
    return chrono::duration_cast<chrono::milliseconds>(end - start).count();
}

然后,添加 performanceTest 函数。它计算两个伪随机生成的矩阵的乘积并测量执行时间。然后将执行时间打印到控制台

void performanceTest(int size, int trialCount) {
    double** matrix1, ** matrix2, ** product;
 
    auto start = now();
 
    for (int i = 0; i < trialCount; i++) {
        matrix1 = generateRandomSquareMatrix(size);
        matrix2 = generateRandomSquareMatrix(size);
 
        product = squareMatrixProduct(matrix1, matrix2, size);
 
        releaseSquareMatrix(matrix1, size);
        releaseSquareMatrix(matrix2, size);
        releaseSquareMatrix(product, size);
    }
 
    auto elapsedTime = msElapsedTime(start);
 
    cout << "Matrix size: " << size << "x" << size
        << ", Trial count: " << trialCount 
        << ", Elapsed time [s]: " << fixed << setprecision(2) << elapsedTime / 60 << endl;
}

performanceTest 函数接受一个附加参数 trialCount。后者指定要重复计算的次数。

最后,在 main 函数中调用 performanceTest 方法。请注意,trialCount 是从命令行参数读取的

int main(int argc, char** argv)
{
    //simpleTest();
  
    // Performance testing
    if (argc < 2)
    {
        cout << "Trial count is missing." << endl;
        return 0;
    }
 
    const int szCount = 5;
    int size[szCount] {100, 200, 300, 400, 500};
 
    int trialCount = stoi(argv[1]);
 
    for (int i = 0; i < szCount; i++) {
        performanceTest(size[i], trialCount);
    }
 
    cout << endl;
 
    return 0;
}

上面的代码将执行矩阵乘法,次数由 trialCount 指定,针对不同大小的矩阵:

  • 100x100
  • 200x200
  • 300x300
  • 400x400
  • 500x500

在针对不同的构建目标(x86、x64、Arm64EC)运行应用程序后,您应该会看到以下结果

通过从 x64 切换到 Arm64EC 架构,计算时间有了明显的减少。在最小的矩阵(接近 50%)上,性能提升最为显著。对于大型矩阵(500x500),速度提升约为 5%。

您可以开始移植您需要提高性能的依赖项。例如,任何计算密集型依赖项都可以移植以提高性能。

实际上,您只需要修改 CMakeLists.txt 文件来构建正在移植为 Arm64EC 的源代码。所有其他依赖项都将作为共享库包含在内。

摘要

本文介绍了 Arm64EC 架构,然后演示了如何为 Visual Studio 开发配置 Windows Dev Kit 2023。它还展示了如何使用该环境来实现 C++ 应用程序,然后如何在 Arm64EC 设备上实现它。最后,它确认了在 Arm 驱动的设备上从 x64 切换到 Arm64EC 所带来的性能提升。

当然,如果您完全切换到 Arm64 原生,将获得最佳性能。然而,这并非总是可能的,因为应用程序可能包含许多依赖项。Arm64EC 允许进行一个中间步骤,该步骤可以使用您现有的 x64 依赖项,只移植应用程序的关键部分到 Arm。有关更通用的描述,您可以阅读这篇博文 使用 Arm64EC 从 Arm 代码加载 x64 插件(如 VST)

当然,Arm64 将提供最佳的性能提升。切换到 Arm64 非常简单,前提是您的应用程序和依赖项可以编译为 Arm64。

尝试在 Windows 11 上使用 Arm64EC,并使用 Windows Dev Kit 2023 作为一种经济高效的方式,在运行 Windows 的 Arm64 设备上测试您的应用程序。

© . All rights reserved.