欢迎加入开源鸿蒙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 内存
ISPC1.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 自己的库目录并不充分。

鸿蒙PC真机运行截图

  1. 2223 个上游用例,结果怎么读

完整的上游测试来自两套入口:lit 的 check-all 和 scripts/run_tests.py 驱动的 func-tests。设备原始日志中的分类如下:

套件总数PASSFAILUnsupportedXFAILSKIPPED
lit64754639440
func-tests157615670009
合计2223211339449

任务汇总把 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 问题、代码与测试数据来自本次适配材料和设备验证。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐