在嵌入式系统行业用于评价CPU性能指标的标准主要有三种:Dhrystone、MIPS、CoreMark,其中CoreMark是一种新兴流行的嵌入式系统处理器测试基准,被认为是比Dhrystone和MIPS更具有实际价值的测试基准。今天痞子衡就和大家详细聊一聊CoreMark。
一、EEMBC协会
在讲CoreMark之前,必须要先提EEMBC(Embedded Microprocessor Benchmark Consortium)即嵌入式微处理器基准评测协会,它是一个非盈利性组织,该组织目前为止(2018.03)共发布了46个性能测试基准,有了这些性能基准参考,嵌入式设计人员可以快速有效地选择处理器。
EEMBC测试是基于每秒钟算法执行的次数和编译代码大小的综合统计结果。众所周知,编译器选项会对代码大小和执行效率会产生巨大的影响,所以每种测试必须包括足够多的编译器信息并设置不同的优化项。
EEMBC发展势头很好,其很有可能发展成为嵌入式系统开发人员进行处理器和编译器性能比较的工业标准。关于EEMBC的更多介绍可移步它的官方网站 http://www.eembc.org/
二、CoreMark标准
CoreMark是由EEMBC的Shay Gla-On于2009年提出的一项基准测试程序,其主要目标是测试处理器核心性能。
CoreMark标准的测试方法很简单,就是在某配置参数组合下单位时间内跑了多少次CoreMark程序,其指标单位为CoreMark/MHz。CoreMark数字越高,意味着性能更高。
2.1 获取程序
CoreMark程序的目前(2018.03)最新版本是1.01。
核心程序下载 http://www.eembc.org/coremark/download.php
平台移植示例 http://www.eembc.org/coremark/ports.php
核心程序包下载后,在\coremark_v1.0\readme.txt里可见基本介绍,在\coremark_v1.0\docs\Coremark-requirements.doc里可见设计需求。详细文件目录如下:
\coremark_v1.0
\barebones --移植到裸机下需要修改的文件
\core_portme.h -- 移植平台工程具体配置信息
\core_portme.c -- 计时以及板级初始化实现
\cvt.c
\ee_printf.c -- 打印函数串口发送实现
\cygwin --移植到cygwin下需要修改的文件
\linux --移植到linux下需要修改的文件
\linux64 --移植到linux64下需要修改的文件
\simple --基本移植需要修改的文件
core_main.c --主程序入口
core_state.c --状态机控制子程序
core_list_join.c --列表操作子程序
core_matrix.c --矩阵运算子程序
core_util.c --CRC计算子程序
coremark.h --工程配置与数据结构定义
\docs --设计文档
coremark.md5
LICENSE.txt
Makefile
readme.txt --基本介绍
release_notes.txt --版本说明
如果是移植到ARM Cortex-M平台下裸系统运行,一般只需要修改\barebones目录下的文件即可(仅需改动三个函数portable_init()、barebones_clock()、uart_send_char()以及core_portme.h中若干宏定义),其余代码文件不需要修改。关于\barebones下的文件修改,EEMBC上有如下4个示例平台可参考:
2.2 测试项目及文件函数解读
2.2.1 链接列表
(1)概要
core_list_join.c |
|
函数: |
|
core_bench_list | Benchmark function |
cmp_complex | 比较列表单元格中的数据项。 |
cmp_idx | 比较列表单元格中的idx项目,并重新生成数据。 |
core_list_init | 用数据初始化列表。 |
core_list_insert | 将一个项目插入列表 |
core_list_remove | 从列表中删除一个项目。 |
core_list_undo_remove | 撤销删除操作。 |
core_list_find | 在列表中找到一个项目 |
core_list_reverse | 反转一个列表 |
core_list_mergesort | 在不递归的情况下对列表进行排序。 |
(2)描述
此Benchmark所做的项目 1.将一个项目插入列表2.从列表中删除一个项目。3.撤销删除操作。4.在列表中找到一个项目5.反转一个列表6.在不递归的情况下对列表进行排序。
虽然增加了间接访问数据的级别,但这种结构是现实的,可用于许多用于中小型列表的嵌入式应用程序。
列表本身将在将被传递给初始化函数的一块内存上初始化。尽管通常链表使用malloc作为新节点,但嵌入式应用程序有时会直接控制小数据结构(如数组和列表)的内存以避免系统调用的开销,因此这种方法是现实的。
链表将被初始化,以使得列表指针的四分之一指向存储器中的顺序区域,并且列表指针的三分之一以非顺序方式分布。这样做是为了模拟一个链接列表,其中添加/删除操作会暂时中断整齐的顺序,然后一系列可能来自连续内存位置的添加。
对于基准本身:
将执行多个查找操作。这些查找操作可能会导致整个列表被遍历。每次查找的结果将成为输出链的一部分。列表将使用基于data16值的合并排序进行排序,然后根据列表的一部分导出data16项目的CRC。CRC将成为产品链的一部分。
列表将使用基于idx值的合并排序再次排序。这种排序将保证列表在离开函数之前返回到主状态,这样函数的多次迭代将具有相同的结果。列表部分的data16的CRC将再次被计算并成为输出链的一部分。
每个单元中的实际数据16将根据单个16b输入进行伪随机编码,这些输入在编译时无法确定。此外,用于CRC的列表部分也将传递给该函数,并根据在运行时无法确定的输入来确定。
使用链接列表的基准。链接列表是许多应用程序中使用的常见数据结构。就我们的目的而言,这将锻炼处理器的内存单元。特别是使用列表指针来查找和更改数据。
其中没有使用Malloc,因为有些平台不支持这个库。
相反,被传入的内存块用于创建一个列表,并且该基准会小心不要添加更多项目,然后可以通过内存块调整。移植层将确保我们有一个有效的内存块。
所有操作均已完成,无需使用任何额外内存。
2.2.2 矩阵操纵基准
(1)概要
core_matrix.c | |
函数: | |
core_bench_matrix | Benchmark function |
matrix_test | 执行矩阵操纵。 |
matrix_sum | 计算一个依赖于矩阵中元素值的函数。 |
matrix_mul_const | 用一个常数乘以一个矩阵。 |
matrix_add_const | 为矩阵的所有元素添加一个常量值。 |
matrix_mul_vect | 用一个矢量乘一个矩阵。 |
matrix_mul_matrix | 矩阵乘矩阵。 |
matrix_mul_matrix_bitextract | 矩阵乘矩阵,并从结果中提取一些位。 |
(2)描述
Matrixmanipulation benchmark,这个非常简单的算法构成了许多更复杂算法的基础。紧密的内部循环是许多优化(编译器以及基于硬件)的重点,因此与嵌入式处理相关。
它所做的测试包含1.用一个常数乘以一个矩阵。2.为矩阵的所有元素添加一个常量值。3.用一个矢量乘一个矩阵。4.用矩阵乘以一个矩阵。5.将矩阵乘以矩阵。6.并从结果中提取一些bits。
总可用数据空间将被分为3部分
NxN矩阵A 用较小的值初始化(上部3/4位全部为零)。
NxN矩阵B 初始化为中等值(上半部分全部为零)。
NxN矩阵C 用于结果。
A和B的实际值必须根据编译时不可用的输入来派生。
2.2.3 状态机基准
(1)概要
core_state.c |
|
函数: |
|
core_bench_state | Benchmark function |
core_init_state | 初始化状态机的输入数据。 |
core_state_transition | 实际的状态机。 |
(2)描述
许多嵌入式产品都使用这种简单的状态机。对于更复杂的状态机,有时会使用状态转换表实现,而直接编码的交易速度易于维护。由于在CoreMark中使用状态机的主要目的是为了锻炼switch/if的运转情况,我们使用的是小型moore机器。特别是,这台机器测试字符串输入的类型,试图确定输入是数字还是别的东西。
2.3配置参数
前面讲到做平台移植时除了必须要改动3个函数外,还需要设置core_portme.h中若干宏定义,这些宏定义即为配置参数,需要根据要移植到的具体平台的属性而定。一共如下14个宏:
宏 | 解释 | 示例 |
---|---|---|
HAS_FLOAT | Define to 1 if the platform supports floating point | 1 |
HAS_TIME_H | Define to 1 if platform has the time.h header file and implementation of functions thereof | 0 |
USE_CLOCK | Define to 1 if platform has the time.h header file and implementation of functions thereof | 0 |
HAS_STDIO | Define to 1 if the platform has stdio.h. | 0 |
HAS_PRINTF | Define to 1 if the platform has stdio.h and implements the printf function. | 0 |
COMPILER_VERSION | Please put compiler version here (e.g. gcc 4.1) | "IAR EWARM v8.20.2" |
COMPILER_FLAGS | Please put compiler flags here (e.g. -o3) | "High - Speed - No size constraints" |
MEM_LOCATION | Please put the mem location of code execution here (e.g STACK) | "STACK" |
CORETIMETYPE | Define type of return from the timing functions. | ee_u32 |
SEED_METHOD | Defines method to get seed values that cannot be computed at compile time. | SEED_VOLATILE |
MEM_METHOD | Defines method to get a block of memry. | MEM_STATIC |
MULTITHREAD | Define for parallel execution | 1 |
MAIN_HAS_NOARGC | Needed if platform does not support getting arguments to main. | 1 |
MAIN_HAS_NORETURN | Needed if platform does not support returning a value from main. | 0 |
细心的朋友应该能注意到core_portme.h文件的最后有如下条件编译,实际上CoreMark主程序的运行有3种模式可选,即PROFILE_RUN(原型模式)/PERFORMANCE_RUN(性能模式)/VALIDATION_RUN(验证模式)
#if !defined(PROFILE_RUN) && !defined(PERFORMANCE_RUN) && !defined(VALIDATION_RUN)
#if (TOTAL_DATA_SIZE==1200)
#define PROFILE_RUN 1
#elif (TOTAL_DATA_SIZE==2000)
#define PERFORMANCE_RUN 1
#else
#define VALIDATION_RUN 1
#endif
#endif
而在coremark.h文件的最开始就定义了缺省的TOTAL_DATA_SIZE的值为2000,即CoreMark程序默认跑在PERFORMANCE_RUN(性能模式)下。如果你想修改运行模式,需要在编译器预编译选项里自定义TOTAL_DATA_SIZE。
/* Configuration: TOTAL_DATA_SIZE
Define total size for data algorithms will operate on
*/
#ifndef TOTAL_DATA_SIZE
#define TOTAL_DATA_SIZE 2*1000
#endif
2.4 程序解析
CoreMark程序使用C语言写成,包含如下四类运算法则:数学矩阵操作(普通矩阵运算)、列举(寻找并排序)、状态机(用来确定输入流中是否包含有效数字)、CRC(循环冗余校验),都是在真实的嵌入式应用中很常见的操作,这也是CoreMark比其他测试标准更有实际价值的原因所在。
a. Matrix multiply (allow for use of MAC operations, common math use)
b. Linked list search/sort/read (common pointer use)
c. State machine (common use of data dependent branches)
d. CRC (common in embedded)
让我们尝试分析CoreMark主函数入口main(以2.2节中配置示例值为例):
/* Function: main
Main entry routine for the benchmark.
This function is responsible for the following steps:
1 - Initialize input seeds from a source that cannot be determined at compile time.
2 - Initialize memory block for use.
3 - Run and time the benchmark.
4 - Report results, testing the validity of the output if the seeds are known.
Arguments:
1 - first seed : Any value
2 - second seed : Must be identical to first for iterations to be identical
3 - third seed : Any value, should be at least an order of magnitude less then the input size, but bigger then 32.
4 - Iterations : Special, if set to 0, iterations will be automatically determined such that the benchmark will run between 10 to 100 secs
*/
MAIN_RETURN_TYPE main(void) {
int argc=0;
char *argv[1];
ee_u16 i,j=0,num_algorithms=0;
ee_s16 known_id=-1,total_errors=0;
ee_u16 seedcrc=0;
CORE_TICKS total_time;
core_results results[MULTITHREAD];
// 系统板级初始化
portable_init(&(results[0].port), &argc, argv);
// ...
// 设置PERFORMANCE_RUN的初始参数
results[0].seed1=get_seed(1); //0x0
results[0].seed2=get_seed(2); //0x0
results[0].seed3=get_seed(3); //0x66
results[0].iterations=get_seed_32(4); //ITERATIONS
// execs参数为需要跑的算法使能位
results[0].execs=get_seed_32(5); //0x0
if (results[0].execs==0) { /* if not supplied, execute all algorithms */
results[0].execs=ALL_ALGORITHMS_MASK;
}
// ...
results[0].memblock[0]=(void *)static_memblk;
results[0].size=TOTAL_DATA_SIZE;
results[0].err=0;
/* Data init */
/* Find out how space much we have based on number of algorithms */
// ...
// 各算法子程序初始化(共LIST, MATRIX, STATE三种)
for (i=0 ; i<MULTITHREAD; i++) {
if (results[i].execs & ID_LIST) {
results[i].list=core_list_init(results[0].size,results[i].memblock[1],results[i].seed1);
}
if (results[i].execs & ID_MATRIX) {
core_init_matrix(results[0].size, results[i].memblock[2], (ee_s32)results[i].seed1 | (((ee_s32)results[i].seed2) << 16), &(results[i].mat) );
}
if (results[i].execs & ID_STATE) {
core_init_state(results[0].size,results[i].seed1,results[i].memblock[3]);
}
}
/* automatically determine number of iterations if not set */
// ...
// 开始跑CoreMark程序且记录累计消耗时间
start_time();
iterate(&results[0]);
stop_time();
total_time=get_time();
// ...
// 最终信息的打印
// ...
if (total_errors==0) {
ee_printf("Correct operation validated. See readme.txt for run and reporting rules.\n");
if (known_id==3) {
ee_printf("CoreMark 1.0 : %f / %s %s",default_num_contexts*results[0].iterations/time_in_secs(total_time),COMPILER_VERSION,COMPILER_FLAGS);
ee_printf("\n");
}
}
// ...
/* And last call any target specific code for finalizing */
portable_fini(&(results[0].port));
return MAIN_RETURN_VAL;
}
2.5 结果格式
当移植好CoreMark程序后,便可以开始跑起来了,在跑程序的时候,EEMBC同时制定了必须要遵守规则(不遵守的话,跑分结果不被EEMBC所认可),详见 https://www.eembc.org/coremark/CoreMarkRunRules.pdf。
当得到跑分结果后可将结果提交到EEMBC网站上,跑分结果需按如下标准格式进行提交:
CoreMark 1.0 : N / C [/ P] [/ M]
N - Number of iterations per second with seeds 0,0,0x66,size=2000)
C - Compiler version and flags
P - Parameters such as data and code allocation specifics
- This parameter *may* be omitted if all data was allocated on the heap in RAM.
- This parameter *may not* be omitted when reporting CoreMark/MHz
M - Type of parallel execution (if used) and number of contexts
This parameter may be omitted if parallel execution was not used.
e.g.
> CoreMark 1.0 : 128 / GCC 4.1.2 -O2 -fprofile-use / Heap in TCRAM / FORK:2
or
> CoreMark 1.0 : 1400 / GCC 3.4 -O4
If reporting scaling results, the results must be reported as follows:
CoreMark/MHz 1.0 : N / C / P [/ M]
P - When reporting scaling results, memory parameter must also indicate memory frequency:core frequency ratio.
- If the core has cache and cache frequency to core frequency ratio is configurable, that must also be included.
e.g.
> CoreMark/MHz 1.0 : 1.47 / GCC 4.1.2 -O2 / DDR3(Heap) 30:1 Memory 1:1 Cache
如果移植的CoreMark能够正确运行,你应该可以看到串口会打印出类似如下格式的信息,上述要求的CoreMark标准结果就在打印信息的最后。
2K performance run parameters for coremark. (Run type)
CoreMark Size : 666 (Buffer size)
Total ticks : 25875 (platform dependent value)
Total time (secs) : 25.875000 (actual time in seconds)
Iterations/Sec : 3864.734300 (Performance value to report)
Iterations : 100000 (number of iterations used)
Compiler version : GCC3.4.4 (Compiler and version)
Compiler flags : -O2 (Compiler and linker flags)
Memory location : Code in flash, data in on chip RAM
seedcrc : 0xe9f5 (identifier for the input seeds)
[0]crclist : 0xe714 (validation for list part)
[0]crcmatrix : 0x1fd7 (validation for matrix part)
[0]crcstate : 0x8e3a (validation for state part)
[0]crcfinal : 0x33ff (iteration dependent output)
Correct operation validated. See readme.txt for run and reporting rules. (*Only when run is successful*)
CoreMark 1.0 : 6508.490622 / GCC3.4.4 -O2 / Heap (*Only on a successful performance run*)
2.6 跑分榜
截止到目前(2018.03),EEMBC网站共记录535款微控制器的CoreMark跑分结果(注意并不是所有跑分结果都经过EEMBC核实),所有跑分结果可在这里查询 https://www.eembc.org/coremark/scores.php,下图是跑分榜部分结果(按提交日期排序)。如果是设计人员根据性能选型的话,可以选按得分高低排序。
2.6 结果示例
在上节介绍的跑分榜里可点击微控制器型号查看具体结果,也可选择多个微控制器进行结果对比。最近两家ARM Cortex-M微控制器知名厂商恩智浦半导体和意法半导体在高性能微控制器上正一决雌雄,恩智浦推出的i.MX RT1050和意法半导体推出的STM32H743均是基于Cortex-M7内核,且都在2017.10实现初版量产,我们且来比比看这两款微控制器:
从对比结果来看,i.MX RT1050在性能上完爆STM32H743,其3036的总得分在Cortex-M微控制器里独孤求败,这个跑分结果虽未经过EEMBC认证,但与恩智浦官方给的数据3020基本吻合。