1. 项目概述
最近在调试自己设计的RISC-V处理器时,遇到了一个有趣的问题:虽然处理器通过了riscv-tests测试套件,但在运行CoreMark基准测试时却出现了异常。这让我意识到,仅通过基本的功能测试是不够的,性能基准测试同样重要。本文将详细介绍如何在RISC-V处理器上运行CoreMark基准测试,包括环境搭建、代码移植、编译优化和结果分析的全过程。
CoreMark是由EEMBC(嵌入式微处理器基准协会)开发的标准化基准测试程序,专门用于评估嵌入式处理器的核心性能。它通过执行一系列典型算法(包括链表操作、矩阵运算和状态机处理)来测试处理器的整数运算、内存访问和控制流性能。与Dhrystone等传统基准测试相比,CoreMark具有更严格的规范,能更准确地反映处理器的实际性能。
2. 环境准备与工具链配置
2.1 硬件平台选择
本次测试使用的是Xilinx Genesys2开发板,搭载了我自己设计的RV32IMF架构处理器。处理器主要规格如下:
- 指令集:RV32IMF(支持整数和单精度浮点指令)
- 时钟频率:100MHz
- 内存配置:64KB指令存储器 + 16KB数据存储器
- 外设:UART用于调试输出,定时器用于性能计数
2.2 开发环境搭建
在Ubuntu 20.04虚拟机上搭建交叉编译环境,主要步骤如下:
- 安装RISC-V工具链:
bash复制sudo apt-get install autoconf automake autotools-dev curl libmpc-dev libmpfr-dev libgmp-dev gawk build-essential bison flex texinfo gperf libtool patchutils bc zlib1g-dev libexpat-dev
git clone --recursive https://github.com/riscv/riscv-gnu-toolchain
cd riscv-gnu-toolchain
./configure --prefix=/opt/riscv --enable-multilib
make linux
- 验证工具链安装:
bash复制/opt/riscv/bin/riscv64-unknown-elf-gcc --version
提示:如果仅需要裸机开发环境,可以在configure时使用
--with-newlib选项替代--enable-multilib,这样可以减少编译时间。
2.3 CoreMark源码获取
CoreMark源码托管在GitHub上,可以直接克隆:
bash复制git clone https://github.com/eembc/coremark.git
源码目录结构如下:
| 文件/目录 | 描述 |
|---|---|
| coremark.h | 主头文件,定义CoreMark特定的宏、函数原型和基准测试配置 |
| core_list_join.c | 链表操作基准测试内核,侧重指针操作和内存访问 |
| core_matrix.c | 矩阵操作基准测试内核,侧重整数计算 |
| core_state.c | 有限状态机处理基准测试内核,侧重控制流 |
| core_main.c | 主程序入口,包含main()函数 |
| barebones/ | 包含需要移植到目标平台的代码 |
| barebones/core_portme.h | 平台特定的宏定义和函数原型 |
| barebones/core_portme.c | 平台特定函数的实现 |
3. 代码移植与适配
3.1 处理器支持检查
在移植CoreMark前,需要确认处理器满足以下基本要求:
- 自由运行的32位计数器(如mtime),用于精确计时
- UART接口,用于输出测试结果
- 足够的指令和数据RAM空间(至少36KB)
3.2 关键移植文件修改
3.2.1 core_portme.h配置
这个头文件定义了平台相关的配置参数,需要根据处理器特性进行调整:
c复制/* 浮点支持配置 */
#define HAS_FLOAT 1 // RV32IMF支持浮点,设置为1
/* 标准库支持配置 */
#define HAS_TIME_H 0 // 裸机环境无time.h
#define USE_CLOCK 0 // 不使用标准clock()函数
#define HAS_STDIO 0 // 无标准stdio.h
#define HAS_PRINTF 0 // 使用自定义ee_printf而非标准printf
/* 内存分配方式 */
#define MEM_METHOD MEM_STACK // 使用栈分配内存
/* 计时器类型定义 */
typedef ee_u32 CORE_TICKS;
#define CORETIMETYPE ee_u32
3.2.2 core_portme.c实现
这个文件需要实现平台特定的函数,主要是计时和串口输出功能:
c复制#include "coremark.h"
#include "core_portme.h"
/* 获取当前计时器值 */
CORETIMETYPE barebones_clock() {
return get_mtime_cur(); // 自定义函数,读取mtime计数器
}
/* 计时开始/结束函数 */
void start_time(void) {
set_mtime_en(1); // 启用计时器
GETMYTIME(&start_time_val);
}
void stop_time(void) {
GETMYTIME(&stop_time_val);
set_mtime_en(0); // 禁用计时器
}
/* 平台初始化函数 */
void portable_init(core_portable *p, int *argc, char *argv[]) {
uart_init(); // 初始化UART
ee_printf("Starting coremark on My CPU\n");
// 验证数据类型大小
if (sizeof(ee_ptr_int) != sizeof(ee_u8 *)) {
ee_printf("ERROR! ee_ptr_int type mismatch!\n");
}
p->portable_id = 1;
}
注意:
get_mtime_cur()和uart_init()需要根据具体硬件实现。在RISC-V中,mtime计数器通常位于CSR寄存器或MMIO地址空间。
3.3 串口输出适配
由于裸机环境没有标准printf,需要使用CoreMark提供的简化版ee_printf。在barebones/ee_printf.c中,需要实现底层字符输出函数:
c复制int uart_send_char(char c) {
// 实现将字符发送到UART的逻辑
while (!uart_tx_ready()); // 等待发送就绪
UART_TX_REG = c; // 写入发送寄存器
return c;
}
4. 编译与链接配置
4.1 启动文件(start.s)
RISC-V处理器上电后需要初始化栈指针和清零.bss段:
assembly复制.section .text
.global _start
_start:
la sp, _stack_top # 设置栈指针
# 清零.bss段
la a0, _sbss
la a1, _ebss
1:
bgeu a0, a1, 2f
sw zero, 0(a0)
addi a0, a0, 4
j 1b
2:
# 调用main函数
call main
# 程序结束处理
li x31, 0xFFFFFFFF
j . # 无限循环
4.2 链接脚本(kernel.ld)
链接脚本定义了内存布局和段分布:
ld复制MEMORY {
IRAM (rx) : ORIGIN = 0x00000000, LENGTH = 64K
DRAM (rw) : ORIGIN = 0x00010000, LENGTH = 16K
}
SECTIONS {
.text : {
*(.text .text.*)
} > IRAM
.rodata : {
*(.rodata .rodata.*)
} > DRAM
.data : {
*(.data .data.*)
} > DRAM
.bss : {
_sbss = .;
*(.bss .bss.*)
_ebss = .;
} > DRAM
_stack_top = (ORIGIN(DRAM) + LENGTH(DRAM)) & ~0xF;
}
4.3 Makefile配置
关键编译选项和参数设置:
makefile复制CROSS_COMPILE := riscv64-unknown-elf-
CC := $(CROSS_COMPILE)gcc
# CoreMark特定参数
ITERATIONS = 5000 # 迭代次数
CLOCKS_PER_SEC = 100000000 # 100MHz时钟
# 编译选项
ARCH := -march=rv32imf -mabi=ilp32f
CFLAGS := $(ARCH) -O3 -g -ffreestanding -fno-builtin -nostartfiles
XCFLAGS := -DITERATIONS=$(ITERATIONS) -DCLOCKS_PER_SEC=$(CLOCKS_PER_SEC)
# 链接选项
LDFLAGS := -T kernel.ld -static -nostdlib -lgcc
# 构建目标
all: kernel.bin
kernel.bin: kernel.elf
$(OBJCOPY) -O binary $< $@
kernel.elf: $(OBJS)
$(CC) $(LDFLAGS) -o $@ $^
5. 运行测试与结果分析
5.1 编译优化对比
使用不同优化级别编译并运行CoreMark:
| 优化级别 | 运行时间(s) | CoreMark分数 | CoreMark/MHz |
|---|---|---|---|
| -O2 | 20.92 | 239 | 2.39 |
| -O3 | 20.09 | 249 | 2.49 |
| -O3 (无浮点) | 20.09 | 250 | 2.50 |
注意:CoreMark分数计算公式为:
CoreMark = (ITERATIONS / time_in_secs) * CLOCKS_PER_SEC / 1000000
5.2 常见问题排查
5.2.1 测试失败的可能原因
-
内存不足:CoreMark在-O3优化下需要约36KB代码空间。如果出现异常,首先检查链接脚本中的内存配置是否足够。
-
计时器不准确:确保
get_mtime_cur()返回的计时器值单调递增,且与CPU同频。 -
栈溢出:增大栈空间(至少1KB),可以在链接脚本中调整
_stack_top的位置。
5.2.2 差分调试技巧
当CoreMark运行失败时,可以采用差分测试方法定位问题:
- 将迭代次数设为1:
makefile复制ITERATIONS = 1
- 在仿真中添加寄存器操作记录:
verilog复制always @(posedge clk) begin
if (commit_we && commit_rd != 0) begin
$display("PC=%h x%0d=%h", commit_pc, commit_rd, commit_data);
end
end
- 将输出结果与参考模型对比,定位第一个出现差异的指令。
6. 性能优化建议
基于测试结果,可以考虑以下优化方向:
-
分支预测优化:添加简单的静态分支预测器(如2-bit动态预测),可显著提升控制密集型任务的性能。
-
内存访问优化:
- 添加指令和数据缓存
- 实现内存访问流水化
- 支持未对齐内存访问
-
乘法器优化:将单周期乘法改为多级流水线,减少性能瓶颈。
-
编译器调优:尝试不同的编译选项组合,如:
makefile复制CFLAGS += -funroll-loops -fgcse-sm -fgcse-las
7. 结论与延伸
通过本次CoreMark基准测试,我们验证了处理器的基本功能并量化了其性能表现。2.5 CoreMark/MHz的分数对于简单的顺序执行处理器来说是一个合理的起点。后续可以通过添加流水线、分支预测和缓存等特性来进一步提升性能。
对于想更深入了解处理器性能分析的读者,建议:
- 尝试其他基准测试如Dhrystone、SPEC CPU
- 使用perf或自定义性能计数器进行细粒度分析
- 研究不同编译选项对代码生成的影响
最后附上完整的项目代码结构供参考:
code复制coremark/
├── barebones/
│ ├── core_portme.c
│ ├── core_portme.h
│ └── ee_printf.c
├── core_list_join.c
├── core_main.c
├── core_matrix.c
├── core_state.c
├── core_util.c
├── bsp/
│ ├── uart/
│ ├── timer/
│ └── common/
├── kernel.ld
├── start.S
└── Makefile