将应用程序移植到 Arm64:使用 Arm64EC - 系列(第 1 部分)






4.55/5 (3投票s)
在本文中,您将构建一个基于 Qt 的 Python 应用程序,它具有两个基于 C/C++ 的 DLL 依赖项。这种架构模拟了使用 Python 和 Qt 进行快速 UI 原型设计以及使用 DLL 进行计算密集型工作的典型场景。
Arm64 提供了更大的内存空间、更快的纹理加载速度、比 Arm32 更快的某些操作以及更低的功耗。如果您完全切换到 Arm64 以利用其原生架构,将获得最佳性能提升。然而,当现有应用程序包含许多依赖项时,这种方法可能不可行。通常,这需要在移植实际应用程序之前移植所有依赖项到 Arm64,从而造成瓶颈。
Arm64EC 是一个 Windows 11 应用程序二进制接口 (ABI),可帮助您将现有的 x64 应用程序迁移到 Arm64。它允许您的现有 x64 依赖项在与 Arm64 二进制文件相同的进程中加载,从而解决了移植依赖项的瓶颈。这种方法可以在不更改任何代码的情况下提高应用程序的性能。
使用 Arm64EC 有助于迁移具有自身生态系统的大型复杂应用程序。应用程序供应商可能不知道应用程序客户所需的依赖项。例如,专业人士可能在图像处理应用程序中使用来自许多独立供应商的插件。当该图像处理应用程序与 Arm64EC 兼容时,用户可以使用所有插件,无论其制造商是否已将其移植到 Arm64。Arm64EC 允许发布链接了旧版二进制依赖项的应用程序,即使这些依赖项缺少源代码或不支持的工具链。
本系列演示了如何使用 Arm64EC 移植一个完整的应用程序,包括主应用程序和依赖项,其中还包括单独的 动态链接库 (DLL)。另一篇文章使用一个简单的单 DLL 示例来解释 Arm64EC 的工作原理 以及如何将现有的 x64 DLL 移植到 Arm64。
在本文中,您将构建一个基于 Qt 的 Python 应用程序,它具有两个基于 C/C++ 的 DLL 依赖项。这种架构模拟了使用 Python 和 Qt 进行快速 UI 原型设计以及使用 DLL 进行计算密集型工作的典型场景。Python 应用程序演示了为 C/C++ 基础 DLL 依赖项构建 UI 的另一种方法,因为原生的 C/C++ 依赖项在 Python 生态系统中很常见,并且可能尚未提供原生构建。但是,您仍然可以使用 基于 Qt 的 C/C++ UI,正如另一篇文章所示。
使用 Arm64EC 移植应用程序
本教程首先使用第一个 DLL,即 *Vectors.dll*,来计算两个向量的 点积。该 DLL 有一个函数,它运行计算几次并返回总计算时间。UI 显示时间。
第二个 DLL,*Filters.dll*,生成一个合成信号,然后使用预定义的阈值对其进行截断。Qt 图表显示输入信号和过滤后的信号,如下面的图像所示。
您将使用 ctypes 来调用 C/C++ DLL。
必备组件
要学习本教程,请确保您具备以下条件:
- Visual Studio 2022
- Arm64 构建工具。通过您的 Visual Studio 安装程序,在 **单个组件** > **MSVC v143** > **VS 2022 C++ Arm64/ARM64EC 构建工具** 下安装这些工具。请注意,这是默认选择的组件。
- 您的计算机上安装的 Python。此演示使用 Python 版本 3.11.3。
有关教程的详细信息,请查看 完整的项目代码。
项目设置
要设置项目,请先创建依赖项(DLL)。此演示使用 Visual Studio 2022 中的 CMake 来创建依赖项的基础项目。您还可以使用 MS Build/Visual C++ 项目模板通过将体系结构添加到构建配置来编译到 Arm64EC。要访问 CMake,请单击 **文件** > **新建** > **项目…**,然后在出现的窗口中查找 **CMake 项目**。
然后,单击 **下一步** 并进行以下配置
- 项目名称:Arm64EC.Porting
- 位置:选择任何位置
- 解决方案:创建新解决方案
- 解决方案名称:Arm64EC.Porting
最后,单击 **创建**。
应用程序构建
现在,调用您将很快创建的两个 DLL `Vectors.dll` 和 `Filters.dll` 导出的函数。要实现此应用程序,请在 Arm64EC.Porting 下创建一个名为 *Main-app* 的文件夹。在 *Main-app* 文件夹内,创建一个 *main.py* 脚本,并添加一个名为 *dependencies* 的子文件夹。*dependencies* 文件夹将包含您在本教程稍后编译的 DLL。
接下来,您需要安装几个依赖项。第一个是 PySide,它提供了 Qt 的 Python 绑定。此操作还将安装 Qt 相关的二进制文件。您可以通过运行 `pip install pyside6` 来安装 PySide。
或者,您可以使用虚拟环境安装 PySide,方法是运行 python ` -m venv path_to_virtual_environment`。然后,通过运行 `path_to_virtual_environment/Scripts/activate.bat` 激活环境,并通过运行 `pip install -r requirements.txt` 安装依赖项。请注意,对于此方法,您必须首先从配套代码下载 requirements.txt 文件。
在 *main.py* 文件中,导入 `ctypes`、`sys`、`os` 和 `Qt` 包
import ctypes, sys, os
from PySide6 import QtCore, QtWidgets
from PySide6.QtGui import QPainter
from PySide6.QtCharts import QChart, QChartView, QLineSeries, QChartView, QValueAxis
然后,使用以下代码获取 DLL 的绝对路径。
rootPath = os.getcwd()
vectorsLibName = os.path.join(rootPath, "Dependencies\\Vectors.dll")
filtersLibName = os.path.join(rootPath, "Dependencies\\Filters.dll")
接下来,定义 `MainWindowWidget` 类及其初始化程序
class MainWindowWidget(QtWidgets.QWidget):
def __init__(self):
super().__init__()
# Buttons
self.buttonVectors = QtWidgets.QPushButton("Vectors")
self.buttonFilters = QtWidgets.QPushButton("Filters")
# Label
self.computationTimeLabel = QtWidgets.QLabel("", alignment=QtCore.Qt.AlignTop)
# Chart
self.chart = QChart()
self.chart.legend().hide()
self.chartView = QChartView(self.chart)
self.chartView.setRenderHint(QPainter.Antialiasing)
# Configure layout
self.layout = QtWidgets.QVBoxLayout(self)
self.layout.addWidget(self.computationTimeLabel)
self.layout.addWidget(self.buttonVectors)
self.layout.addWidget(self.buttonFilters)
self.layout.addWidget(self.chartView)
# Configure chart y-axis
self.axisY = QValueAxis()
self.axisY.setRange(-150, 150)
self.chart.addAxis(self.axisY, QtCore.Qt.AlignLeft)
# Signals and slots
self.buttonVectors.clicked.connect(self.runVectorCalculations)
self.buttonFilters.clicked.connect(self.runTruncation)
此处的初始化程序定义了 UI。具体来说,上面的代码添加了两个按钮:“**Vectors**” 和 “**Filters**”。它还创建了一个标签来显示计算时间。然后,它生成图表。
代码还将所有 UI 组件添加到垂直布局中。它指定了绘图的 y 轴,并将两个方法 `runVectorCalculations` 和 `runTruncation` 与按钮关联起来。用户通过按下相应的按钮来调用这些方法。
接下来,按如下方式定义 `runVectorCalculations`
@QtCore.Slot()
def runVectorCalculations(self):
vectorsLib = ctypes.CDLL(vectorsLibName)
vectorsLib.performCalculations.restype = ctypes.c_double
computationTime = vectorsLib.performCalculations()
self.computationTimeLabel.setText(f"Computation time: {computationTime:.2f} ms")
该方法使用 ctypes 加载 DLL。然后,它将来自 *Vectors.dll* 库的 `performCalculations` 函数的返回类型设置为 double。最后,`runVectorCalculations` 方法调用库中的函数,标签显示计算结果时间。
接下来,在 `MainWindowWidget` 类下的 *main.py* 文件中定义 `runTruncation` 方法
@QtCore.Slot()
def runTruncation(self):
filtersLib = ctypes.CDLL(filtersLibName)
# Remove all previous series
self.chart.removeAllSeries()
# Generate signal
filtersLib.generateSignal()
filtersLib.getInputSignal.restype = ctypes.POINTER(ctypes.c_double)
# Display signal
signal = filtersLib.getInputSignal()
seriesSignal = self.prepareSeries(signal, filtersLib.getSignalLength())
self.chart.addSeries(seriesSignal)
# Run convolution
filtersLib.truncate()
filtersLib.getInputSignalAfterFilter.restype = ctypes.POINTER(ctypes.c_double)
# Display signal after convolution
signalAfterFilter = filtersLib.getInputSignalAfterFilter()
seriesSignalAfterFilter = self.prepareSeries(signalAfterFilter, filtersLib.getSignalLength())
self.chart.addSeries(seriesSignalAfterFilter)
# Configure y-axis
seriesSignal.attachAxis(self.axisY)
seriesSignalAfterFilter.attachAxis(self.axisY)
和以前一样,您首先加载 DLL。然后,您删除图表中的所有系列。这样,每当用户单击“**Filters**”按钮时,图表就会清除,然后再绘制新数据。
您应该检索 `inputSignal` 并使用辅助方法 `prepareSeries` 将其添加到图表中,该方法将数据从底层指针复制到 Python 数组。您还应该调用 `truncate` 方法,检索过滤后的信号,并将其添加到图中。要完成所有这些,请在 *main.py* 文件中添加以下方法
def prepareSeries(self, inputData, length):
series = QLineSeries()
for i in range(1, length):
series.append(i, inputData[i])
return series
最后一步是将 `MainWindowWidget` 添加到 Qt 应用程序并显示应用程序窗口。通过在 *main.py* 文件底部添加以下语句来完成此操作。
if __name__ == "__main__":
app = QtWidgets.QApplication([])
widget = MainWindowWidget()
widget.resize(600, 400)
widget.show()
sys.exit(app.exec())
添加依赖项
要为 Python 应用程序添加依赖项,您必须创建两个子文件夹:“*Vectors*” 和 “*Filters*”。
向量
在 *Vectors* 文件夹中,创建两个文件:“*Vectors.h*” 和 “*Vectors.cpp*”。这是 *Vectors.h* 的声明
#pragma once
#include <iostream>
#include <chrono>
using namespace std;
extern "C" __declspec(dllexport) double performCalculations();
在 `pragma` 预编译器声明之后,上面的声明导入了两个头文件:“`iostream`” 和 “`chrono`”。然后,您添加 `std` 命名空间并导出一个函数 `performCalculations`。稍后,您将从主 Python 应用程序调用此函数。
现在,通过包含 *Vectors.h* 并定义三个函数来修改 *Vectors.cpp*
#include "Vectors.h"
int* generateRamp(int startValue, int len) {
int* ramp = new int[len];
for (int i = 0; i < len; i++) {
ramp[i] = startValue + i;
}
return ramp;
}
double dotProduct(int* vector1, int* vector2, int len) {
double result = 0;
for (int i = 0; i < len; i++) {
result += (double)vector1[i] * vector2[i];
}
return result;
}
double msElapsedTime(chrono::system_clock::time_point start) {
auto end = chrono::system_clock::now();
return chrono::duration_cast<chrono::milliseconds>(end - start).count();
}
在上面的代码中,第一个函数 — `generateRamp` — 创建给定长度的合成向量。您使用 `startValue` 和 `len` 函数的参数来设置向量值。然后,您定义了 `dotProduct` 函数,它逐元素地乘以两个输入向量。最后,您添加了一个辅助函数 `msElapsedTime`,它使用 C++ `chrono` 库来测量代码执行时间。
接下来,在下面准备另一个辅助函数,用于生成两个向量,计算它们的点积,并测量代码执行时间。稍后您可以使用此函数来测量代码性能。
double performCalculations() {
// Ramp length and number of trials
const int rampLength = 1024;
const int trials = 100000;
// Generate two input vectors
// (0, 1, ..., rampLength - 1)
// (100, 101, ..., 100 + rampLength-1)
auto ramp1 = generateRamp(0, rampLength);
auto ramp2 = generateRamp(100, rampLength);
// Invoke dotProduct and measure performance
auto start = chrono::system_clock::now();
for (int i = 0; i < trials; i++) {
dotProduct(ramp1, ramp2, rampLength);
}
return msElapsedTime(start);
}
现在,在 *Vectors* 目录中创建下面的 *CMakeLists.txt* 文件。您将使用此文件进行构建。
add_library (Vectors SHARED "Vectors.cpp" "Vectors.h")
if (CMAKE_VERSION VERSION_GREATER 3.12)
set_property(TARGET Vectors PROPERTY CXX_STANDARD 20)
endif()
上面的文件使用 `add_library` 语句中的 `SHARED` 标志将构建目标设置为 DLL。
过滤器。
现在您可以类似地实现第二个 DLL。再次,您使用 CMake(请参阅 Filters/CMakeLists.txt)。首先,在 *Filters* 文件夹中创建 *Filters.h* 头文件
#pragma once
#define _USE_MATH_DEFINES
#include <math.h>
#include <iostream>
#include <algorithm>
using namespace std;
#define SIGNAL_LENGTH 1024
#define SIGNAL_AMPLITUDE 100
#define NOISE_AMPLITUDE 50
#define THRESHOLD 70
// Global variables
double inputSignal[SIGNAL_LENGTH];
double inputSignalAfterFilter[SIGNAL_LENGTH];
// Exports
extern "C" __declspec(dllexport) int getSignalLength();
extern "C" __declspec(dllexport) void generateSignal();
extern "C" __declspec(dllexport) void truncate();
extern "C" __declspec(dllexport) double* getInputSignal();
extern "C" __declspec(dllexport) double* getInputSignalAfterFilter();
然后,导出五个函数
- `getSignalLength` — 返回 `SIGNAL_LENGTH` 下定义的合成信号的长度
- `generateSignal` — 创建合成信号并将其存储在 inputSignal 全局变量中
- `truncate` — 通过截断所有大于 `THRESHOLD` 的值来过滤信号
- `getInputSignal` — 返回生成的信号(存储在 `inputSignal` 变量中)
- `getInputSignalAfterFilter` — 返回过滤后的信号(存储在 `inputSignalAfterFilter` 变量中)
使用下面的代码在 *Filters.cpp* 中定义这些函数
#include "Filters.h"
double* getInputSignal() {
return inputSignal;
}
double* getInputSignalAfterFilter() {
return inputSignalAfterFilter;
}
int getSignalLength() {
return SIGNAL_LENGTH;
}
然后,添加 `generateSignal` 函数,该函数创建带有随机附加噪声的正弦波
void generateSignal() {
auto phaseStep = 2 * M_PI / SIGNAL_LENGTH;
for (int i = 0; i < SIGNAL_LENGTH; i++) {
auto phase = i * phaseStep;
auto noise = rand() % NOISE_AMPLITUDE;
inputSignal[i] = SIGNAL_AMPLITUDE * sin(phase) + noise;
}
}
最后,`truncate` 函数如下所示
void truncate() {
for (int i = 0; i < SIGNAL_LENGTH; i++) {
inputSignalAfterFilter[i] = min(inputSignal[i], (double)THRESHOLD);
}
}
此函数分析 `inputSignal` 并将所有大于 `THRESHOLD` 的值替换为该值。其他值保持不变。例如,如果值为 100,则将其替换为 70。另一方面,值 50 将不会改变。
编译
在运行应用程序之前,您需要编译这两个库。只需单击 **生成/全部生成** 菜单项,DLL 文件将在 *out/build/x64-release* 文件夹(*Vectors/Vectors.dll* 和 *Filters/Filters.dll*)中可用。您也可以使用命令行构建 DLL。
生成 DLL 后,将它们复制到 `Main-app/Dependencies`。
结果
最后,按照以下步骤运行主应用程序
- 打开终端并将文件夹更改到您的 `Arm64EC.Porting` 项目所在的位置(例如,*c:\Users\<用户名>\source\repos\Arm64EC.Porting*)
- 如果您使用了虚拟环境来安装 Python 包,请通过键入 *Scripts\activate.bat* 激活环境。确保您已通过调用 `pip install -r requirements.txt` 安装了 Python 依赖项。
- 然后,将文件夹更改为 *Main-app*,并键入 `python main.py`。
现在应用程序已启动。
首先,单击标记为“**Vectors**”的按钮。应用程序将运行点积计算,稍后,总计算时间将显示在标签上。然后,单击第二个按钮“**Filters**”。此操作将以蓝色绘制原始信号,并以绿色绘制过滤后的信号,如下面的屏幕截图所示。
您现在已确认基于 Qt 的 Python 应用程序可以加载这两个依赖项。您可以使用此方法在 C++ DLL 中实现计算密集型计算,并使用 Qt 的 Python 绑定快速构建 UI。
后续步骤
开发人员经常使用 Python 和 Qt 进行快速 UI 原型设计。在本文中,您学习了如何准备一个基于 Qt 的 Python 应用程序,该应用程序具有两个基于 C/C++ 的 DLL 依赖项。您已为 x64 配置了这两个 DLL。
在 Arm64EC 可用之前,将包含多个 C++ 依赖项的应用程序移植到 Arm64 非常耗时。它要求同时将平台更改为 Arm64 并重新编译整个解决方案,这通常涉及代码更改。
现在,您可以使用 Arm64EC 通过简单地将构建目标从 x64 切换到 Arm64EC 来将选定的依赖项移植到 Arm64。当二进制文件的源不再可用,由于缺少或有错误的编译器或工具链而无法构建依赖项,或者您不控制第三方生态系统依赖项但仍希望在运行时加载它们以使用户受益时,此方法很有帮助。
Arm64EC 依赖项可以在同一进程中与 x64 依赖项一起工作,从而使您能够移植应用程序并受益于 Arm64 的原生计算。在本系列文章的下一篇文章中,您将学习如何使用 Arm64EC 将 C++ 依赖项移植到 Arm64。具体来说,您将学习如何配置 C++ 项目以构建 Arm64EC 的 DLL。然后,您将发现如何在由 Arm64 的 Python 启动的 Python 应用程序中加载这些依赖项。
在 Windows 11 上尝试 Arm64EC 并使用 Windows Dev Kit 2023 作为测试在运行 Windows 的 Arm64 设备上运行应用程序的经济高效的方式。