把 ISPC 1.28.2 跑在鸿蒙PC上:从 LLVM 依赖到 NEON 数值验证
欢迎加入开源鸿蒙PC社区:https://harmonypc.csdn.net/
欢迎在PC社区平台申请新建项目:https://atomgit.com/OpenHarmonyPCDeveloper
ISPC 的可执行文件编出来之后,第一次运行 --version 就抛出了 std::bad_cast。继续排查,又遇到了“构建成功,但生成的位码没有所需符号”的情况。对编译器做移植,编译结束只是其中一步;它还得能把用户的代码编译出来,并让生成的程序在目标设备上算出正确结果。
这篇文章记录 ISPC 1.28.2 在鸿蒙PC aarch64 上的适配过程,重点讨论依赖、C++ 运行库、构建脚本和实际执行之间的关系。配方位于 AtomGit 社区仓库 OpenHarmonyPCDeveloper/build_in_harmonyos 的 archives/i/ispc/1.28.2,对应 PR !15301。任务记录显示,该 PR 于 2026 年 10 月 18 日合并,最终 CI 为 build #98211。
本文使用设备原始日志说明上游测试结果,并重新执行了一次消费者示例。上游用例通过率为 2113/2223(95.05%),仍有 3 个失败和 107 个按任务口径归入跳过的用例,具体分类在后文列出。
文章目录
1. 先明确要移植的是什么
ISPC 全称是 Intel Implicit SPMD Program Compiler。它接受带有 SPMD 扩展的 C 风格代码,通过 LLVM 生成目标代码。以数组运算为例,可以用 foreach 描述各元素的计算,再由编译器面向相应的 SIMD 目标生成实现。
本次交付的是命令行编译器 bin/ispc,不包含 libispc/JIT 库。验证重点是 aarch64/NEON 原生编译和执行;保留 X86 后端用于代码生成与相关测试,并不表示鸿蒙PC可以直接运行 x86 机器码。文中的数值示例只验证正确性,没有测量加速比。
本次适配记录中的环境如下:
| 项目 | 版本或配置 |
|---|---|
| 设备系统、架构 | HarmonyOS、aarch64 |
| 设备资源 | 8 核、32 GB 内存 |
| ISPC | 1.28.2 |
| 构建 ISPC 的设备编译器 | clang/clang++ 15.0.4 |
| ISPC 使用的 LLVM 依赖 | llvm-project/20.1.8 |
| 构建工具 | Conan 2.29.1、CMake 4.1.2、Ninja 1.13.1 |
这里同时出现了 LLVM 20 和 clang 15。前者是 ISPC 所依赖的编译基础设施,后者是设备上用来构建、链接程序的工具链。后面两次故障都与这两套工具的边界有关。
2. 为什么先做了一个 LLVM 依赖包
ISPC 需要的不只是一个 llvm-config。构建还会使用 Clang 前端组件库、LLVM 目标后端,以及 clang、clang++、llvm-as、FileCheck 等工具。
适配前的依赖检查发现,当时可用的几个包各有缺口:llvm/20.1.0 缺少所需的 Clang 前端库,后端也不完整;clang/20.1.8 的远端二进制下载返回 404;clang/22.1.8 则超出了 ISPC 1.28.2 的 LLVM 支持范围,检查时也遇到了 API 不兼容。
因此先独立适配了 llvm-project/20.1.8,对应依赖 PR !15277。这个包提供 LLVM 和 Clang 的 CMake 配置、所需的前端静态库、libLLVM.so.20.1 以及配套工具,后端包含 AArch64、ARM 和 X86。
其中 ARM 与 AArch64 不能想当然地互相替代:该版本 ISPC 的配置检查会同时检查这两个后端。X86 后端也有用途,上游测试中存在依赖 X86_ENABLED 的代码生成用例。只按宿主架构裁掉其他后端,会改变能够验证的范围。
这一步决定了后面的构建方案。遇到编译器项目时,先列清楚它需要哪些库、哪些可执行工具、哪些目标后端,比看到一个同名依赖就开始构建更稳妥。
3. 一个版本判断,为什么会挡住编译
第一处源码编译错误出现在 src/util.cpp:
error: exception specification in declaration does not match previous declaration
ISPC 在这里定义了 std::__libcpp_verbose_abort,并根据所链接 LLVM 的版本决定是否添加 noexcept。但当前环境中,LLVM 是 20.1.8,宿主 SDK 的 libc++ 来自 15.0.4,两者的函数声明并不相同。
这类错误应当与当前参与编译的头文件对齐。最终补丁把判断条件改为宿主 libc++ 的版本:
-#if ISPC_LLVM_VERSION >= ISPC_LLVM_20_0
+#if _LIBCPP_VERSION >= 200000
noexcept
#endif
这是本次适配唯一的上游源码补丁,其余处理放在配方、构建参数或包装脚本中。修改范围小的原因也很具体:这里的问题是异常说明与头文件声明不匹配,没有必要改动 ISPC 的代码生成逻辑。
4. 编译通过,为什么连版本号都打印不出来
解决声明问题之后,可执行文件已经生成,启动时却报错:
libc++abi: terminating due to uncaught exception of type std::bad_cast
排查发现,打包选项会启用静态链接设置,向链接命令加入 -static-libstdc++。在这套 OHOS clang 工具链中,该选项作用于 libc++,使可执行文件静态带入一份 C++ 标准库。与此同时,依赖的 libLLVM.so.20.1 又使用动态的 libc++_shared.so。
任务中的最小复现对比了三种组合:
| 链接组合 | 设备上的结果 |
|---|---|
| 动态 C++ 标准库,加 LLVM 动态库 | 正常运行 |
| 静态 C++ 标准库,加 LLVM 动态库 | 启动异常,出现 std::bad_cast |
| 静态 C++ 标准库,不链接 LLVM | 正常运行 |
这组对照把问题缩小到了当前依赖组合中的运行库混用。最终显式关闭 ISPC_STATIC_LINK 和 ISPC_STATIC_STDCXX_LINK,让 ISPC 与 LLVM 使用一致的动态 C++ 运行库。
这不是说静态链接普遍不可用。这里需要避免的是同一个进程中的这组混用方式。若程序在进入实际业务前就因 iostream、locale 等标准库操作崩溃,检查可执行文件和依赖库的动态依赖,比直接修改业务代码更有针对性。
5. 最容易漏掉的失败:空位码也能让构建继续
另一个问题直到功能测试阶段才显现。21 个涉及 print/asin 的用例在链接时报:
ld.lld: error: undefined symbol: __do_print
继续向前追,发现用于生成 builtins 的编译命令引用了 Debian multiarch 的系统头目录。鸿蒙设备上没有对应目录,前面的 clang++ 已经报出找不到 unistd.h。但这条自定义命令使用了管道:
clang++ ... -o - | llvm-as -o out.bc
前一个程序失败,后面的 llvm-as 仍然接收空输入并生成空模块。由于这条命令的退出状态取自管道末尾,构建没有在真正出错的位置停下。
修复前,四个 builtins_cpp_*.bc 文件都是 1304 字节,符号检查找不到所需定义。配方随后通过包装脚本取得设备 clang 的 C 头文件搜索目录,并按系统头目录注入 builtins 的编译过程。
构建后还增加了文件大小检查:这些位码文件若小于 8 KiB,就立即报错。这是针对已观察到的空模块故障设置的检查,不能单独证明位码正确;符号检查和后续功能测试仍然需要保留。
这个问题给出的教训很直接:生成文件存在,不代表生成步骤成功。对管道里的工具,要检查前端诊断,也要检查产物是否包含预期内容。
6. 同样叫 clang++,调用到的可能不是同一个
后续测试又报了找不到 stdio.h、iostream 或 cmath。这些错误看起来像缺少 SDK 头文件,但实际原因是编译器路径发生了变化。
LLVM 依赖包也带有 clang++。测试环境把依赖包的 bin 放到 PATH 前面后,裸命令 clang++ 会解析到依赖包中的程序,而不是设备上配置好目标系统环境的编译器。
lit 测试与 Conan 消费者测试都遇到了这个问题,只是发生的位置不同。lit 侧使用排在前面的包装脚本指向设备编译器;消费者测试则只接受可用的绝对路径,依次检查 Conan 配置、绝对路径形式的 CXX,最后使用设备工具链路径。
本文示例的 C++ 链接步骤因此明确写为 /data/service/hnp/bin/clang++。这条路径是本机的实际配置,换设备时应先核对,不能只凭命令名称判断工具来源。
CI 还暴露出一处构建阶段的网络访问:ISPC_PREPARE_PACKAGE=ON 会注册 ispc_web 目标,从外网克隆文档站仓库。最终关闭该选项,并从已有源码补齐许可、说明、示例等静态文件。核对新旧安装清单后,差异为少了五个网页文件,其余 132 项一致。这样处理的是文档获取方式,没有据此裁掉编译器的验证步骤。
7. 在鸿蒙PC上编译并运行一个数值程序
下面使用适配目录中现有的消费者测试。ISPC 内核完成数组相加和点积,C++ 驱动负责准备数据、调用生成的函数并检查结果。
7.1 编写 ISPC 内核
将以下内容保存为 test.ispc:
export void add_arrays(uniform float a[], uniform float b[], uniform float c[], uniform int n) {
foreach (i = 0 ... n) {
c[i] = a[i] + b[i];
}
}
export uniform float dot_product(uniform float a[], uniform float b[], uniform int n) {
float acc = 0;
foreach (i = 0 ... n) {
acc += a[i] * b[i];
}
return reduce_add(acc);
}
foreach 表达各元素上的计算,reduce_add 汇总各程序实例的部分结果。
7.2 编写 C++ 校验程序
保存 C++ 驱动为 test.cpp:
#include <cmath>
#include <cstdio>
#include "ispc_probe.h"
int main() {
const int n = 64;
float a[n], b[n], c[n];
for (int i = 0; i < n; ++i) {
a[i] = static_cast<float>(i) * 0.5f;
b[i] = static_cast<float>(i) + 1.0f;
c[i] = -1.0f;
}
ispc::add_arrays(a, b, c, n);
for (int i = 0; i < n; ++i) {
if (c[i] != a[i] + b[i]) {
std::printf("FAIL add_arrays at %d: %f != %f\n", i, c[i], a[i] + b[i]);
return 1;
}
}
const float dot = ispc::dot_product(a, b, n);
float expect = 0.0f;
for (int i = 0; i < n; ++i) expect += a[i] * b[i];
if (std::fabs(dot - expect) > 1e-3f * std::fabs(expect)) {
std::printf("FAIL dot_product: %f != %f\n", dot, expect);
return 1;
}
std::printf("ispc probe ok: add_arrays(%d) + dot_product = %f\n", n, dot);
return 0;
}
生成的头文件把接口放在 ispc 命名空间中,因此 C++ 侧使用 ispc::add_arrays 和 ispc::dot_product。这个示例使用固定的有限数值输入,检查数组结果与点积;它不是覆盖 NaN、无穷大等情况的通用浮点测试。
7.3 设置包路径与运行环境
执行前需准备好已构建的 ISPC 包、LLVM 依赖包和设备 C++ 工具链。下面两条缓存路径来自本次设备复跑,其他环境必须替换为对应包的实际路径。Conan 缓存目录不具有跨设备固定性。
export ISPC=/storage/Users/currentUser/.conan2/p/b/ispc95f90df04bb69/p/bin/ispc
export LLVM_LIB=/storage/Users/currentUser/.conan2/p/b/llvm-a4635c5fa43c9/p/lib
export LD_LIBRARY_PATH="$LLVM_LIB${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
7.4 编译、链接并执行
在存放两个源码文件的可写目录执行:
(
set -eu
uname -s
uname -m
"$ISPC" --version
"$ISPC" test.ispc --arch=aarch64 --target=neon-i32x4 --pic \
-o test.o -h ispc_probe.h
/data/service/hnp/bin/clang++ test.cpp test.o -I. -o ispc_probe -lm
./ispc_probe
)
7.5 核对输出与真机截图
2026 年 10 月 20 日,在设备的独立临时目录中重新编译和执行,得到:
HarmonyOS
aarch64
Intel(r) Implicit SPMD Program Compiler (Intel(r) ISPC), 1.28.2 (build commit @ 20261014, LLVM 20.1.8)
ispc probe ok: add_arrays(64) + dot_product = 43680.000000
这次验证实际经过了 ISPC 代码生成、C++ 链接和设备执行。如果加载 ISPC 时提示缺少 libLLVM.so.20.1,应先核对 LLVM 包的 lib 路径;只添加 ISPC 自己的库目录并不充分。

- 2223 个上游用例,结果怎么读
完整的上游测试来自两套入口:lit 的 check-all 和 scripts/run_tests.py 驱动的 func-tests。设备原始日志中的分类如下:
| 套件 | 总数 | PASS | FAIL | Unsupported | XFAIL | SKIPPED |
|---|---|---|---|---|---|---|
| lit | 647 | 546 | 3 | 94 | 4 | 0 |
| func-tests | 1576 | 1567 | 0 | 0 | 0 | 9 |
| 合计 | 2223 | 2113 | 3 | 94 | 4 | 9 |
任务汇总把 94 个 Unsupported、4 个 XFAIL 和 9 个 SKIPPED 合并记为 107 个 SKIP。这里展开原始分类,是因为 XFAIL 表示预期失败,含义与未执行的跳过不同。按全部 2223 个用例为分母,PASS 比例为 2113/2223(95.05%)。
三个 FAIL 分别是 math-lib-value.ispc、2780-1.ispc 和 3571.ispc。任务分析将其归因于 x86 宿主相关假设:分别涉及在 aarch64 宿主目标上使用 SVML、用宿主编译器处理 x86 专用头,以及把 x86 对象文件交给 aarch64 链接器。这些失败保留在统计里,没有通过删除用例来提高比例。
前面的消费者程序另算一个鸿蒙验证用例,不加入上游 2223 的分母。64 次逐元素检查也不能写成新增了 64 个上游测试。
CI 的汇总数字曾与设备全量日志不同。素材记录了测试驱动回收队列结果时可能存在的竞态,但仅凭汇总退出码,无法逐项补齐缺失回执。因此本文采用已有完整设备日志的计数,不把两轮结果拼成一张“全部通过”的表。
9. CI 红灯要按失败位置处理
任务记录中共有 13 次 CI 结论,前 12 次失败,最后一次通过。失败既包含已经修复的配方问题,也包含环境准备、进程创建、下载工具和限流相关问题,还有一轮分类未明确,不能把它们统称为基础设施故障。
例如,构建期间克隆文档仓库失败,需要消除隐式网络依赖;消费者找不到 cmath,需要修正编译器选择。若日志在环境准备阶段就出现无法创建进程,则应继续检查对应 worker 的资源与环境状态,不能据此修改 ISPC 算法。
也不能只看 can't fork 就断言物理内存耗尽。它说明创建进程失败,进一步归因仍需要当时的资源和限制证据。把失败阶段、首个有效错误和本轮修改分开记录,才能判断下一步该修代码、修配置,还是等待运行环境恢复。
10. 这次适配留下了什么
ISPC 1.28.2 已在这台鸿蒙PC上完成实际代码生成、链接和数值执行。适配中可复用的处理集中在几个具体位置:依赖包要覆盖所需的前端库与后端;异常说明要跟随宿主头文件;主程序与动态依赖的 C++ 运行库要保持一致;生成步骤要检查真实产物;测试使用的编译器要能确定来源。
这些检查能帮助定位“本地能编、CI 不能编”或“编译成功、启动就崩”的问题。当前结果也有清楚的边界:上游测试保留了失败与跳过项,没有验证 GPU/JIT 能力,也没有性能加速结论。任务清单中的制品状态仍为 built,不能仅凭 PR 合并和 CI 通过就写成已发布到制品仓。
适配源码可在 AtomGit 社区仓库 中按 archives/i/ispc/1.28.2 查找;最终 CI 记录为 build #98211。
资料与延伸阅读
后三篇用于参考技术复盘的组织方式;本文的 ISPC 问题、代码与测试数据来自本次适配材料和设备验证。
更多推荐




所有评论(0)