PDF

通过Profiling展示Nuclei Model NICE/VNICE指令加速

Nuclei Model 已支持 Windows/Linux 版本,此文档测试都是基于 Nuclei Studio 的 Windows 版本 (>= 2025.10) 完成的。

背景描述

Nuclei Model Profiling

Nuclei Studio使用Profiling功能进行性能调优举例中已经通过 qemu 以及上板测试两种运行方式展示了 如何在IDE中导入特定程序进行 Profiling,此文档中的一部分将介绍如何针对 Nuclei Model 完成 Profiling。

Nuclei Model Profiling 的优势:

  • 无需使用开发板等硬件
  • model 中内建了 gprof 功能,无需 Profiling 库和 gcc -pg 选项就可以产生 Profiling 文件
  • 采取了指令级别的采样,可以进行指令级别的 Profiling 分析

NucleiStudio_User_Guide.pdf相关章节对 Nuclei Model 如何仿真性能分析配置已经有较详细的描述,此文档以一个例子来展示其实际应用。

NICE/VNICE 自定义指令加速

NICE/VNICE使得用户可以结合自己的应用扩展自定义指令,将芯来的标准处理器核扩展成为面向领域专用的处理器,NICE 具体编码规则可以参考 Nuclei_RISC-V_ISA_Spec.pdf 中的 NICE IntroductionNICE 适用于无需使用 RISCV Vector 的自定义指令,VNICE 适用于需要使用 RISCV Vector 的自定义指令。

demo_nice/demo_vnice介绍了 Nuclei 针对 NICE/VNICE 的 demo 应用 是如何编译运行的,此文档将通过改造一个更为常见的 AES 加解密的例子,重点说明该如何使用 NICE/VNICE 指令替换热点函数以及如何在 model 里实现 NICE/VNICE 指令,然后通过 Nuclei Studio 的 Profiling 功能分析替换前后的程序性能。

解决方案

环境准备

Nuclei Studio:NucleiStudio 2025.10 Windows

Model Profiling

工程创建方式有两种:

  • 方式1:用户可以使用 Nuclei Studio 中的 demo_nicedemo_vnice 模板来移植改造自己的 NICE/VNICE 程序
  • 方式2:用户导入自己的工程到 Nuclei Studio 中,然后再添加NICE 内嵌汇编头文件、NICE CSR 使能等代码

此文档将采取前一种方式创建工程,由于此 demo 会用到 VNICE 指令,故创建 demo_vnice 工程,然后将 AES 加解密程序移植替换到其中。

step1:新建 demo_vnice 工程

File->New->New Nuclei RISC-V C/C++ Project,选择Nuclei FPGA Evalution Board->sdk-nuclei_sdk @0.6.0

注意: Nuclei SDK 需选择 0.6.0 及以后版本

image-create_aes_project

step2:基于 demo_vnice 工程移植 aes_demo 裸机用例

移植 aes_demo 时,需要保留 demo_vnice 中的 insn.h 内嵌汇编头文件框架,方便后续添加自定义的 NICE/VNICE 指令,在 main.c 中需要保留 NICE/VNICE 指令执行前的 CSR 使能代码:

__RV_CSR_SET(CSR_MSTATUS, MSTATUS_XS);

其余 demo_vnice 工程中 application 原始用例可删除,替换成 aes_demo 用例,形成如下目录结构,并确保能够编译通过。

image-compile_aes_demo

用户可以下载我们移植好的 AES 加解密 demo:优化前AES工程链接下载

下载 zip 包后,可以直接导入到 Nuclei Studio 中运行(导入步骤:File->Import->Existing Projects into Workspace->Next->Select archive file->选择zip压缩包->next即可)

step3:model 仿真程序

首先将 aes_debug.h 中的 LOCAL_DEBUG 打开,准备测试 AES 算法的整体 cycle 数。

Model 仿真程序需要配置 Nuclei Studio 中的 GDB Nuclei Model riscv Debugging 配置项,步骤如下:

  1. 打开 Nuclei Studio 主菜单栏的 Run 选项的 Run Configurations
  2. 选择 GDB Nuclei Model riscv Debugging 配置项,右键选择 New Configuration,会自动生成项目名的 Model 配置页面,launch bar也会同步更新
  3. 在右侧 Main 选项卡中点击 Search Project... 选择编译好的 elf 文件
  4. 在右侧 Debugger 选项卡中选择 Browse 找到 Nuclei Model 可执行程序默认路径:NucleiStudio/toolchain/nucleimodel/bin/xl_cpumodel.exe
  5. 在右侧 Debugger 选项卡中的 Nuclei Setup 中完成 model 运行配置, 选择 Nuclei RISC-V CoreOther Extensions 需要保持和 Nuclei SettingsCoreOther extensions 配置一致,Other Extensions 为空时不传递此参数, Enable Nuclei Model RVTrace 表示运行时生成 rvtrace,More options 加上 --gprof=1 开启 Profiling 功能, 然后点击 ApplyRun,model 就开始运行程序了

    image-Ori_Project_Model_Config

Nuclei Studio (< 2025.10) 只能使用 Run Configurations 中的 Nuclei Model 来配置 model,Nuclei Studio (>= 2025.10) 建议切换到使用 GDB Nuclei Model riscv Debugging 来配置

在 Console 中会看到 Total elapsed real time 说明 model 已经完成仿真了,得到 AES 算法整体消耗 161108 cycle。

image-ase_demo_cycle

aes_debug.h 中的 LOCAL_DEBUG 关掉去掉程序打印,为了准确测试 Profiling 数据,确保 Nuclei Studio 的 launch bar 为 aes_demo Debug, 重新 Run model,运行结束后会生成 Profiling 文件:

image-aes_demo_profiling

step4:解析 gprof 数据

model 仿真程序完成后,双击打开生成的 gprof*.gmon 文件,切换到函数视图,点击 % Time从高到低排列函数 CPU 占用率。

注意: Time/Call 显示的是每个函数的函数体 text 段的 cycle 数,并不是整个函数的 cycle 数,是不计入其中子函数占用的 cycle 数的。

image-parse_gprof

从而得到 CPU 占用率最高的 TOP5 热点函数为:

aes_mix_columns_dec
aes_mix_columns_enc
aes_key_schedule
aes_ecb_decrypt
aes_ecb_encrypt

注意: 此时需要备份当前的 aes_demo 工程,改名为 aes_demo_nice 工程,这样可以在 Nuclei Studio 中同时打开两个工程,方便添加 NICE/VNICE 指令优化后的工程和原 aes_demo 工程进行 Profiling 比较。

step5:NICE/VNICE 指令替换

用户需要在备份的 aes_demo_nice 工程下,研究热点函数算法特点,将其替换为 NICE/VNICE 指令,从而提升整体程序性能。

在包含 AES 加解密的 TOP5 热点函数的 aes_dec.caes_dec.c 两个C文件中 #include "insn.h" 以便添加 NICE/VNICE 指令替换。

TOP1 热点函数为 aes_mix_columns_dec,实现了 AES 算法解密的逆混合列,输入一个状态矩阵,经过计算后原地址输出一个计算后的状态矩阵,实现了 Load 数据、逆混合运算以及 Store 数据,代码如下:

static void aes_mix_columns_dec(
    uint8_t     pt[16]       //!< Current block state
){
    // Col 0
    for(int i = 0; i < 4; i ++) {
        uint8_t b0,b1,b2,b3;
        uint8_t s0,s1,s2,s3;

        s0 = pt[4*i+0];
        s1 = pt[4*i+1];
        s2 = pt[4*i+2];
        s3 = pt[4*i+3];

        b0 = XTE(s0) ^ XTB(s1) ^ XTD(s2) ^ XT9(s3);
        b1 = XT9(s0) ^ XTE(s1) ^ XTB(s2) ^ XTD(s3);
        b2 = XTD(s0) ^ XT9(s1) ^ XTE(s2) ^ XTB(s3);
        b3 = XTB(s0) ^ XTD(s1) ^ XT9(s2) ^ XTE(s3);

        pt[4*i+0] = b0;
        pt[4*i+1] = b1;
        pt[4*i+2] = b2;
        pt[4*i+3] = b3;
    }
}

由于输入输出地址一样,可以考虑用一条 NICE 指令替换,指令的 opcodefunct3funct7 都可以在编码位域中自定义,该指令设置 opcodeCustom-0funct3 设置为0,funct7 设置为0x10,寄存器只使用到 rs1 描述入参地址,不需要使用 rdrs2,指令写到 insn.h 中,内嵌汇编如下:

__STATIC_FORCEINLINE void custom_aes_mix_columns_dec(uint8_t* addr)
{
    int zero = 0;
    asm volatile(".insn r 0xb, 0, 0x10, x0, %1, x0" : "=r"(zero) : "r"(addr));
}

用户可以在 insn.h 中定义一个 USE_NICE 的宏选择是否使用 NICE ,在 aes_dec.c 改写 aes_mix_columns_dec 如下 :

static void aes_mix_columns_dec(
    uint8_t     pt[16]       //!< Current block state
){

#ifdef USE_NICE
    custom_aes_mix_columns_dec(pt);
#else
    // Col 0
    for(int i = 0; i < 4; i ++) {
        uint8_t b0,b1,b2,b3;
        uint8_t s0,s1,s2,s3;

        s0 = pt[4*i+0];
        s1 = pt[4*i+1];
        s2 = pt[4*i+2];
        s3 = pt[4*i+3];

        b0 = XTE(s0) ^ XTB(s1) ^ XTD(s2) ^ XT9(s3);
        b1 = XT9(s0) ^ XTE(s1) ^ XTB(s2) ^ XTD(s3);
        b2 = XTD(s0) ^ XT9(s1) ^ XTE(s2) ^ XTB(s3);
        b3 = XTB(s0) ^ XTD(s1) ^ XT9(s2) ^ XTE(s3);

        pt[4*i+0] = b0;
        pt[4*i+1] = b1;
        pt[4*i+2] = b2;
        pt[4*i+3] = b3;
    }
#endif
}

TOP2 热点函数为 aes_mix_columns_enc,和 TOP1 类似,实现的是 AES 加密的逆混合列,同样也是输入一个状态矩阵,经过计算后原地址输出一个计算后的状态矩阵:

static void aes_mix_columns_enc(
    uint8_t     ct [16]       //!< Current block state
){
    for(int i = 0; i < 4; i ++) {
        uint8_t b0,b1,b2,b3;
        uint8_t s0,s1,s2,s3;

        s0 = ct[4*i+0];
        s1 = ct[4*i+1];
        s2 = ct[4*i+2];
        s3 = ct[4*i+3];

        b0 = XT2(s0) ^ XT3(s1) ^    (s2) ^    (s3);
        b1 =    (s0) ^ XT2(s1) ^ XT3(s2) ^    (s3);
        b2 =    (s0) ^    (s1) ^ XT2(s2) ^ XT3(s3);
        b3 = XT3(s0) ^    (s1) ^    (s2) ^ XT2(s3);

        ct[4*i+0] = b0;
        ct[4*i+1] = b1;
        ct[4*i+2] = b2;
        ct[4*i+3] = b3;
    }
}

考虑到指令实现可能无法只用1条指令完成,可使用2条 VNICE 指令替换此算法,第一条 load 16 byte 数据到 Vector 寄存器,第二条再完成计算以及 store。

指令的 opcodefunct3funct7 仍然可以在编码位域中自定义,第一条指令使用 rd 描述 Vector 寄存器,rs1 描述入参地址,第二条指令使用 rs1 描述入参地址,rs1 描述入参 Vector 寄存器,两条 VNICE 指令的内嵌汇编写到 insn.h 中,定义如下:

__STATIC_FORCEINLINE vint8m1_t __custom_vnice_load_v_i8m1 (uint8_t* addr)
{
    vint8m1_t rdata ;
    asm volatile(".insn r 0xb,4,0,%0,%1,x0"
            : "=vr"(rdata)
            : "r"(addr)
            );
    return rdata;
}

__STATIC_FORCEINLINE void __custom_vnice_aes_mix_columns_enc_i8m1 (uint8_t *addr, vint8m1_t data)
{
    int zero = 0;
    asm volatile(".insn r 0xb,4,1,x0,%1,%2"
            : "=r"(zero)
            : "r"(addr)
            , "vr"(data)
            );
}

用户通过定义 Vector 寄存器以及使用上定义好的 VNICE 指令内嵌汇编改写 aes_enc.c 中的 aes_mix_columns_enc 如下:

static void aes_mix_columns_enc(
    uint8_t     ct [16]       //!< Current block state
){
#ifdef USE_NICE
    uint32_t blkCnt = 16;
    size_t l;
    vint8m1_t vin;
    for (; (l = __riscv_vsetvl_e8m1(blkCnt)) > 0; blkCnt -= l) {
        vin = __custom_vnice_load_v_i8m1(ct);
        __custom_vnice_aes_mix_columns_enc_i8m1(ct, vin);
    }
#else
    for(int i = 0; i < 4; i ++) {
        uint8_t b0,b1,b2,b3;
        uint8_t s0,s1,s2,s3;

        s0 = ct[4*i+0];
        s1 = ct[4*i+1];
        s2 = ct[4*i+2];
        s3 = ct[4*i+3];

        b0 = XT2(s0) ^ XT3(s1) ^    (s2) ^    (s3);
        b1 =    (s0) ^ XT2(s1) ^ XT3(s2) ^    (s3);
        b2 =    (s0) ^    (s1) ^ XT2(s2) ^ XT3(s3);
        b3 = XT3(s0) ^    (s1) ^    (s2) ^ XT2(s3);

        ct[4*i+0] = b0;
        ct[4*i+1] = b1;
        ct[4*i+2] = b2;
        ct[4*i+3] = b3;
    }
#endif
}

修改后的程序代码编译通过:(aes_demo_nice 工程)

image-compile_aes_nice_demo

step6:在 Nuclei Model 中实现 NICE/VNICE 指令

首先需要下载支持用户配置自定义 NICE/VNICE 指令的原始 Nuclei Model 软件包原始model软件包下载,解压软件包为 xlmodel_nice,然后将其导入 Nuclei Studio。

导入步骤:File->Import->Projects from Folder or Archive->Next->Directory->选择 xlmodel_nice->Finish即可

如何使用 Nuclei Model 以及查看 xlmodel_nice 软件包的目录结构可以参考Nuclei Model介绍xlmodel_nice 是由CMake构建的,用户无需修改即可编译,在 编译前选择 Nuclei Studio 的 launch bar 的 xlmodel_nice,然后点击编译,确保软件包本身编译通过:

Nuclei Studio (< 2025.10) 生成的 elf 文件所在路径为 build/default/xl_cpumodel

image-xlmodel_nice_build

打开 nice.cc 文件,用户需要用该文件的 do_nice 函数实现所有自定义的 NICE/VNICE 指令,当前 do_nice 里包含了针对 demo_nicedemo_vnice 的 Nuclei定义的 NICE/VNICE 指令, 用户可以参考其中注释完成自己的自定义指令。

注意: 当用户编写自定义 NICE/VNICE 指令时,需要关掉和 Nuclei demo_nice/demo_vnice 对应的 NUCLEI_NICE_SCALAR/NUCLEI_NICE_VECTOR 宏,以免和用户自定义的指令编码相冲突。

image-nice_inst_intro

AES demo 中定义的 NICE/VNICE 指令实现如下图,通过指令的 opcodefunct3funct7 编写条件判断语句指定该条指令,然后在其中实现指令行为以及指令 cycle 数添加。

NICE 指令实现中,MMU 宏表示 memory 访问,load memory 使用 MMU.load_uint<n>,store memory 使用 MMU.store_uint<n>RDRS1RS2RS3 宏表示其对应标量寄存器中的值,FRS1FRS2FRS3 宏表示其对应浮点寄存器中的值,这些宏的使用可以参考 nice/inc/decode_macros.h

VNICE 指令实现中仍然是用 MMU 宏访问 memory,只不过 Vector 寄存器数据会存储在 P.VU.elt 类中,用户可以参考 xlspike/include/riscv/v_ext_macros.h完成相关代码编写。

在指令实现完后,将自定义指令需要的 cycle 数 n 直接标定:STATE.mcycle->bump(n); 即可,这里根据硬件通过 NICE/VNICE 实现此算法的理论值,标定 custom_aes_mix_columns_dec 为 7 cycle,__custom_vnice_load_v_i8m1 为 1 cycle,__custom_vnice_aes_mix_columns_enc_i8m1 为 2 cycle。

image-xlmodel_nice_aes

以上介绍了用户如何从原始 Nuclei Model 软件包添加自定义 NICE/VNICE 指令,接下来需要将新编译出的 model 可执行程序导入到 Nuclei Studio 中,为了不和 Nuclei Studio 原始 model 名称混淆,可以将 model 导入到 NucleiStudio/toolchain/nucleimodel/bin_aes/ 的创建路径下,我们提供了两种 model 可执行程序获取方式:

  1. 实现 AES demo NICE/VNICE 指令的 Nuclei model 软件包添加AES NICE指令model软件包,编译后将 xl_cpumodel 可执行程序导入上述路径。
  2. 编译好的 model 的可执行程序 xl_cpumodel,直接导入上述路径。

step7:热点函数再分析

注意: 请务必完成 step6 中介绍的实现了 NICE/VNICE 指令的 model 导入 Nuclei Studio 中才能用 model Run aes_demo_nice 工程。

首先打开 Nuclei Studio 主菜单栏的 Run 选项的 Run Configurations,model 配置需要重新添加新的 GDB Nuclei Model riscv Debugging 运行配置 aes_demo_nice Debug,在 Main 选项卡中选择 aes_demo_nice.elf

image-aes_demo_nice_model_config_main

然后在 Debugger 选项卡中的 model 执行路径 Executable path 改为 step6 中新修改 model 的执行路径: .../NucleiStudio/toolchain/nucleimodel/bin_aes/xl_cpumodel:

image-aes_demo_nice_model_config_debugger

运行前将 aes_debug.h 中的 LOCAL_DEBUG 打开,测试优化后 AES 算法的整体 cycle 数,选择 Nuclei Studio 的 launch bar 的 aes_demo_nice Debug后 Run model,得到 AES 算法优化后整体消耗 cycle 数从优化前的 161108 降到了 42066 cycle。

image-aes_demo_nice_cycle

aes_debug.h 中的 LOCAL_DEBUG 关掉测试重新 Run model 测试 Profiling 数据,双击 gprof0.gmon 可以看到 CPU 占用率较高的热点函数已经没有 aes_mix_columns_encaes_mix_columns_dec 了:

image-parse_gprof_nice

搜索 aes_mix_columns_encaes_mix_columns_dec ,CPU 占用率 aes_mix_columns_enc 从 8.05% 降到了 2.93%,aes_mix_columns_dec 从 57.87% 降到了 0.5%,函数 Time per Call 消耗 cycle 数 aes_mix_columns_enc 从 1141 cycle 降到了 146 cycle,aes_mix_columns_dec 从 8209 cycle 降到了 25 cycle,说明了通过 NICE/VNICE 指令替换热点函数可以大幅提高程序算法性能。

image-parse_gprof_aes_enc_dec

数据统计如下:(enc: aes_mix_columns_enc, dec: aes_mix_columns_dec)

Function Before Optimization NICE/VNICE Optimization
CPU Usage % (enc) 8.05 2.93
CPU Usage % (dec) 57.87 0.5
Time per Call Cycles (enc) 1,141 146
Time per Call Cycles (dec) 8,209 25
AES Program Total Before Optimization NICE/VNICE Optimization
Cycles 161,108 42,066

AES加解密 NICE/VNICE demo:优化后AES工程链接下载