1.8. sycuricon( 八 ): benchmark

为了对处理器的性能、正确性等进行验证,我们在 buildroot 平台移植了一些简单的 benchmark,后续应该会继续完善。 这部分代码和机制被我们继承在 riscv-rss-sdk 仓库当中,其他细节可以参看 riscv-rss-sdk 的介绍。

1.8.1. Lmbench

1.8.1.1. 简单介绍

Lmbench 的 submodule 定义如下,同步该仓库开始后续操作。正常的编译测试方式如下:

  • 执行 make build,Lmbench 会执行 src 下的 make build,将 src 下的所有 c 文件编译为 elf 文件保存到 bin 目录下

  • 执行 make results/make rerun,Lmbench 会执行 scripts 下面的 config-run 脚本,对 bin 的程序进行执行,并且保存执行结果

config-run 的执行方式是执行 benchmp 对需要测试的函数进行多线程的验证,然后每个线程跑一个测试函数。 该测试函数会对一个测试目标执行若干次,然后统计执行时间,进而计算平均执行时间。 例如执行 2^32 次 kill 操作计算 signal 传递操作的时间等。

[submodule "benchmark/lmbench-3.0-a9"]
	path = benchmark/lmbench-3.0-a9
	url = https://github.com/docularxu/lmbench-3.0-a9.git

config-run 在执行程序的时候会先检查一下处理器的内存大小,如果内存大小满足测试需求,他才会开始进行测试。 这个内存检查过程非常慢,考虑到我们处理器内存一般是完全够用的,我们可以将对应的内存检查代码注释掉,节约内存检查的时间。

echo $ECHON "MB [default $MB]: $ECHOC"
read TMP
if [ X$TMP != X ]
then	MB=$TMP
fi
# Certain machines tend to barf when you try and bcopy 8MB.
# Figure out how much we can use.
echo "Checking to see if you have $MB MB; please wait for a moment..."
MB=`../bin/$OS/memsize $MB`
MB=`../bin/$OS/memsize $MB`
MB=`../bin/$OS/memsize $MB`
if [ `expr $SYNC_MAX \* $MB` -gt `expr $TOTAL_MEM` ]
then
	MB=`expr $TOTAL_MEM / $SYNC_MAX`
	MB=`expr $MB / 2`
fi
if [ $MB -lt 8 ]
then    echo $0 aborted: Not enough memory, only ${MB}MB available. 
	exit 1
fi
if [ $MB -lt 16 ]
then	echo Warning: you have only ${MB}MB available memory. 
	echo Some benchmark results will be less meaningful. 
fi

1.8.1.2. 移植过程

现在我们需要将 benchmark 进行 riscv 交叉编译,因此需要修改 Lmbench 的一些配置。

修改编译依赖的 config 选项:

  • 在 scripts/compiler 中设置 CC 为对应的交叉编译器路径 riscv64-unknown-linux-gnu-gcc

+CC=${RISCV}/bin/riscv64-unknown-linux-gnu-gcc
 echo $CC
  • 在 scripts/OS 中设置 OS 为对应的 linux 类型 riscv-OS。之后编译的结果会出现在 bin/riscv-OS 中(不然默认是宿主机的 x86_64)

+OS=riscv-linux
 echo $OS

之后想要执行对应的测试,我们执行程序的环境需要 libtirpc,因此我们需要为交叉编译环境和执行环境配置 libtirpc 库。 我们可以回忆一下,我们的交叉编译链接库被保存在 toolchain/lib 中,因此我们需要在这里安装交叉编译的 libtirpc。

  • 安装 libtirpc 库,这个可以从对应的官网直接下载解压到 benchmark 中,并且命名为 libtirpc

  • 然后用 riscv 工具链交叉编译,然后 install 到 toolchian/lib 中

之后我们的交叉编译器就有了链接 libtirpc 的能力。 链接分为动态链接和静态链接,静态连接的程序拥有所有的代码,但是非常庞臃肿; 动态链接不需要链接对应的库代码,尺寸小巧,但是要求执行环境有对应的链接库。 因为我们的环境需要通过 SD 卡将程序读如内存进行执行,这个过程很慢,所以我们希望程序尽可能的小巧,因此我们采用动态链接编译。 然后我们需要额外的执行 install,将 libtirpc 保存到 sysroot 的 lib 目录下。 我们在 makefile 中编写了如下的代码,可以一键执行安装。

Listing 1.105 tirpc 变量定义
libtirpc_srcdir  := $(benchmark_srcdir)/libtirpc
libtirpc_wrkdir  := $(wrkdir)/libtirpc
libtirpc_lib	 := $(buildroot_initramfs_sysroot)/lib/libtirpc.so
Listing 1.106 tirpc 编译安装
$(libtirpc_lib):$(libtirpc_srcdir)
	rm -rf $(libtirpc_wrkdir)
	mkdir -p $(libtirpc_wrkdir)
	cd $(libtirpc_wrkdir); $(libtirpc_srcdir)/configure --host=riscv64-unknown-linux-gnu --prefix=$(toolchain_dest)/sysroot/usr --disable-gssapi
	make -C $(libtirpc_wrkdir)
	make -C $(libtirpc_wrkdir) install
	cd $(libtirpc_wrkdir); $(libtirpc_srcdir)/configure --host=riscv64-unknown-linux-gnu --prefix=$(buildroot_initramfs_sysroot) --disable-gssapi
	make -C $(libtirpc_wrkdir) install
  • 在 scripts/build 中修改编译选项 LDLIBS,加入 tirpc 和 pthread 的链接

-LDLIBS=-lm
+LDLIBS="-ltirpc -lpthread -lm"
  • 在 Makefile 中修改编译选项 COMPILE,加入对 tirpc 的头文件 include 支持

-COMPILE=$(CC) $(CFLAGS) $(CPPFLAGS) $(LDFLAGS)
+COMPILE=$(CC) $(CFLAGS) -Wall -O2 -ffunction-sections -Wl,--gc-sections -s -I$(RISCV)/sysroot/usr/include/tirpc $(CPPFLAGS) $(LDFLAGS)

之后执行编译就可以得到最后的结果。但是我们不方便把所有的 elf 和执行的 script 拷贝到我们的嵌入式环境 starship 中进行执行, 首先我们不需要执行所有的测试程序(太多了);其次,我们很多测试程序的移植需要做调整,比较麻烦。 因此,我们额外写了一个 C 程序 execute.c 用 system 操作执行对应的测试程序,然后计算测试结果。 每个测试程序的执行命令被记录在 scripts/lmbench 当中,我们可以解析这个文件, 让 execute 程序执行需要测试的测试目标在 lmbench 中对应的执行命令。 在 Makefile 中加入对 execute 的编译命令即可。

@@ -74,6 +74,7 @@ SRCS =  bw_file_rd.c bw_mem.c bw_mmap_rd.c bw_pipe.c bw_tcp.c bw_udp.c	\
 	lib_udp.c lib_unix.c lib_sched.c				\
 	line.c lmdd.c lmhttp.c par_mem.c par_ops.c loop_o.c memsize.c 	\
 	mhz.c msleep.c rhttp.c seek.c timing_o.c tlb.c stream.c		\
+	execute.c \
 	bench.h lib_debug.h lib_tcp.h lib_udp.h lib_unix.h names.h 	\
 	stats.h timing.h version.h
 
@@ -103,7 +104,7 @@ EXES =	$O/bw_file_rd $O/bw_mem $O/bw_mmap_rd $O/bw_pipe $O/bw_tcp 	\
 	$O/msleep $O/loop_o $O/lat_fifo $O/lmhttp $O/lat_http		\
 	$O/lat_fcntl $O/disk $O/lat_unix_connect $O/flushdisk		\
 	$O/lat_ops $O/line $O/tlb $O/par_mem $O/par_ops 		\
-	$O/stream
+	$O/stream $O/execute

上述所有的修改,被我们打包到 benchmark/patch/lmbench.patch 当中,可以用 git apply 一键修改。

1.8.2. Unixbench

现在我们移植 unixbench,比 lmbench 简单一些。

1.8.2.1. 简单介绍

  • 执行 make program 可以编译 src 的所有程序,然后保存在 pgms 当中

  • 执行 make run 可以 Run 脚本,这个脚本会依次执行 pgms 中的程序,然后将对应的执行结果和输出 log 保存在 result 中

和 lmbench 不同的是,unixbench 统计的不是每个测试的执行时间,而是执行时间的倒数。 unixbench 每个 elf 接受一个额外参数:执行时间 latency,然后这个 elf 会执行两个 thread。 主 thread 做 sleep 操作,sleep latency 的时间长度;另一个 thread 会执行测试函数,每执行一次给一个全局变量 +1。 当主 thread 从 sleep 醒来就会输出对应的副 thread 执行轮数作为执行分数,也就是单位时间内测试程序执行的次数, 也就是测试程序执行时间的倒数。这个分数越高说明执行越快,性能越好。

[submodule "benchmark/byte-unixbenchmark"]
	path = benchmark/byte-unixbenchmark
	url = https://github.com/kdlucas/byte-unixbench.git

1.8.2.2. 移植过程

修改编译依赖的 config 选项:

  • 在 Makefile 中设置 CC 为对应的交叉编译器路径 riscv64-unknown-linux-gnu-gcc

-CC=gcc
+CC=$(RISCV)/bin/riscv64-unknown-linux-gnu-gcc
  • 在 Makefile 中设置 ARCH 为对应的 linux 类型 riscv64

-  ARCH := $(shell uname -m)
+  ARCH := riscv64

之后我们编写一个类似的 execute 程序来执行我们想测试的程序。

Listing 1.113 加入 execute
   ifeq ($(OSNAME),Linux)
     # Not all CPU architectures support "-march" or "-march=native".
     #   - Supported    : x86, x86_64, ARM, AARCH64, riscv64, etc..
@@ -142,7 +142,8 @@ SOURCES = arith.c big.c context1.c \
 	fstime.c hanoi.c \
 	pipe.c spawn.c \
 	syscall.c looper.c timeit.c time-polling.c \
-	dhry_1.c dhry_2.c dhry.h whets.c ubgears.c
+	dhry_1.c dhry_2.c dhry.h whets.c ubgears.c \
+   execute.c
 TESTS = sort.src cctest.c dc.dat large.txt
 
 ifneq (,$(GRAPHIC_TESTS))
@@ -157,7 +158,8 @@ BINS = $(PROGDIR)/arithoh $(PROGDIR)/register $(PROGDIR)/short \
 	$(PROGDIR)/hanoi $(PROGDIR)/syscall $(PROGDIR)/context1 \
 	$(PROGDIR)/pipe $(PROGDIR)/spawn $(PROGDIR)/execl \
 	$(PROGDIR)/dhry2 $(PROGDIR)/dhry2reg  $(PROGDIR)/looper \
-	$(PROGDIR)/fstime $(PROGDIR)/whetstone-double $(GRAPHIC_BINS)
+	$(PROGDIR)/fstime $(PROGDIR)/whetstone-double $(GRAPHIC_BINS)\
+   $(PROGDIR)/execute
 ## These compile only on some platforms...
 # $(PROGDIR)/poll $(PROGDIR)/poll2 $(PROGDIR)/select
 
@@ -293,6 +295,8 @@ $(PROGDIR)/dhry2reg: $(SRCDIR)/dhry_1.c $(SRCDIR)/dhry_2.c \
                      $(SRCDIR)/dhry.h $(SRCDIR)/timeit.c
 	$(CC) -o $@ ${CFLAGS} $(SRCDIR)/dhry_1.c $(SRCDIR)/dhry_2.c
 
+$(PROGDIR)/execute: $(SRCDIR)/execute.c
+

上述所有的修改,被我们打包到 benchmark/patch/unixbench.patch 当中,可以用 git apply 一键修改。

1.8.3. 文件系统调整

执行 benchmark_patch,将 benchmark/patch 的 lmbench.patch、unixbench.patch apply 到 Lmbench 和 Unixbench 中,完成上述的调整。

benchmark_patch:$(benchmark_patch)
	cd $(unixbench_srcdir); git apply $(benchmark_patch)/UnixBench.patch
	cd $(lmbench_srcdir); git apply $(benchmark_patch)/LmBench.patch

执行 benchmark,对 lmbench 和 unixbench 做编译,然后将编译的结果 Lmbench/bin/riscv-OS 和 Unixbench/pgms 拷贝到 sysroot 的 Lmbench 和 Unixbench 中。

Listing 1.115 benchmark 安装
benchmark: $(unixbench_wrkdir) $(lmbench_wrkdir) $(buildroot_initramfs_sysroot)
	cp -r $(unixbench_wrkdir) $(buildroot_initramfs_sysroot)/
	cp -r $(lmbench_wrkdir) $(buildroot_initramfs_sysroot)/
$(lmbench_wrkdir): $(lmbench_srcdir) $(libtirpc_lib)
	rm -rf $(lmbench_wrkdir) 
	mkdir -p $(lmbench_wrkdir) 
	make -C $(lmbench_srcdir) clean
	make -C $(lmbench_srcdir)
	cp $(lmbench_srcdir)/bin/riscv-linux/* $(lmbench_wrkdir)
$(unixbench_wrkdir):$(unixbench_srcdir)
	rm -rf $(unixbench_wrkdir) 
	mkdir -p $(unixbench_wrkdir) 
	make -C $(unixbench_srcdir) clean
	make -C $(unixbench_srcdir)
	cp $(unixbench_srcdir)/pgms/* $(unixbench_wrkdir)
	cp $(unixbench_srcdir)/testdir/sort.src $(unixbench_wrkdir)

我们的一个测试程序需要对 /dev/zero、/dev/null 等进行操作, 所以需要我们的 buildroot 支持这些特殊设备, 所以我们需要对 conf/buildroot_initramfs_config 进行修改,加入 BR2_ROOTFS_DEVICE_CREATION_DYNAMIC_MDEV=y 的配置。

然后我们就可以重新构造镜像下板子了。

1.8.4. regvault 简单测试

前面的文章我们讲了添加 regvault 的硬件,然后我们需要执行对应的 regvault 指令测试的对应的正确性。 当然我们可以 linux 内核里面大规模的做插桩,然后直接运行,如果 linux 可以顺利启动,那么 regvault 的硬件实现就成功了。 但是如果执行失败了我们想要调试也是很困难的,所以我们应该先执行一个比较小的 regvault 测试程序做初步的测试。 所以我们设计了一个执行 regvault 的内核模块。

static int __init rgvlt_init(void) {
	text_t plaintext = 0xfb623599da6e8127;
	qkey_t w0 = 0x84be85ce9804e94b;
	qkey_t k0 = 0xec2802d4e0a488e9;
	tweak_t tweak = 0x477d469dec0b8762;
	text_t ciphertext;

	printk(KERN_INFO "QARMA64  Plaintext = 0x%016llx\nKey = 0x%016llx || 0x%016llx\nTweak = 0x%016llx\n\n", plaintext, w0, k0, tweak);
	
	asm volatile (
		"csrw 0x5f0, %[k0]\n"
		"csrw 0x5f1, %[w0]\n"
		:
		:[w0] "r" (w0), [k0] "r" (k0)
		:
	);
	printk(KERN_INFO "k0, w0 write done\n");
	
	qkey_t read_k0 = 0;
	qkey_t read_w0 = 0;
	asm volatile (
		"csrr %[read_k0], 0x5f0\n"
		"csrr %[read_w0], 0x5f1\n"
		:[read_w0] "=r" (read_w0), [read_k0] "=r" (read_k0)
		:
		:
	);
	printk(KERN_INFO "read_w0 = 0x%llx, read_k0 = 0x%llx", read_w0, read_k0);

	asm volatile (
		"csrw 0x5f0, %[k0]\n"
		"csrw 0x5f1, %[w0]\n"
		"mv t0, %[plaintext]\n"
		"mv t1, %[tweak]\n"
		"li t2, 0\n"
		".insn r 0x6b, 0x0, 0x54, t2, t0, t1\n"
		"mv %[ciphertext], t2\n"
		:[ciphertext] "=r" (ciphertext)
		:[tweak] "r" (tweak), [plaintext] "r" (plaintext), [w0] "r" (w0), [k0] "r" (k0)
		:"t0", "t1", "t2"
	);

	printk(KERN_INFO "Ciphertext = 0x%016llx", ciphertext);

	text_t decrypttext;
	asm volatile (
		"mv t0, %[ciphertext]\n"
		"mv t1, %[tweak]\n"
		"li t2, 0\n"
		".insn r 0x6b, 0x0, 0x55, t2, t0, t1\n"
		"mv %[decrypttext], t2\n"
		:[decrypttext] "=r" (decrypttext)
		:[ciphertext] "r" (ciphertext), [tweak] "r" (tweak)
		:"t0", "t1", "t2"
	);
	printk(KERN_INFO "Decrypttext  = 0x%016llx\n", decrypttext);
	return 0;
}

然后是对应的 makefile 文件,需要注意的是,因为需要链接的符号表、代码信息都是 build/linux 中交叉编译的结果,所以 KERNEL_DIR 是 build/linux 的结果。

# General Purpose Makefile for Linux Kernel module

ARCH := riscv # set if cross-compile  
CROSS_COMPILE := $(RISCV)/bin/riscv64-unknown-linux-gnu- # set if cross-compile

KERNEL_DIR := $(CURDIR)/../../build/linux/
		
obj-m := rgvlt_test.o

default:
	$(MAKE) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) -C $(KERNEL_DIR) M=$(CURDIR) modules

clean:                                     
	$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) clean

之后我们编译得到 rgvlt_test.ko,将他保存在文件系统的 regvault 文件夹中。

1.8.5. 软件测试

上述程序的正确性可以在 spike 上进行验证。 我们先构造 spike 的镜像,然后在 spike 上执行这些程序验证。

  • 进入 /Lmbench,然后执行 ./execute,就可以开始测试,测试结果如下:

# cd /LmBench/
# ./execute
execute read
pass 0
Simple read: 8.5936 microseconds
Elapsed time: 0.647 seconds
execute write
pass 0
Simple write: 6.8206 microseconds
Elapsed time: 0.647 seconds
execute open
pass 0
Simple open/close: 192.6923 microseconds
Elapsed time: 0.679 seconds
execute select
pass 0
Select on 500 fd's: 774.1429 microseconds
Elapsed time: 0.658 seconds
...
  • 进入 /Unixbench,然后执行 ./execute,就可以开始测试,测试结果如下:

# cd /UnixBench/
# ./execute
execute dhrystone
pass 0
COUNT|3665097|1|lps
pass 1
COUNT|3671664|1|lps
pass 2
COUNT|3672307|1|lps
pass 3
COUNT|3675095|1|lps
pass 4
COUNT|3676492|1|lps
pass 5
COUNT|3676371|1|lps
pass 6
COUNT|3675313|1|lps
pass 7
COUNT|3673990|1|lps
Elapsed time: 80.639 seconds
execute whetstone
pass 0
Calibrate
    0.11 Seconds          1   Passes (x 100)
    0.52 Seconds          5   Passes (x 100)
...
  • 最后我们进入 /regvault,然后执行 insmod 操作进行验证;不过需要 spike 支持 regvault。

如果三个执行结果都没有问题,那么就说明软件应该没有明显问题,我们可以下板子进行性能测试了。

1.8.6. 下板测试

下板之后的测试方式和 spike 上的测试方式是一样的。

测试结果使用串口输出,打印到机器的 screen 上面,然后可以用如下方式截取:

  • ctrl+A+[:进入浏览模式,可以滚动滚轮上下拉动屏幕

  • 空格:选择要复制的起点

  • 滚轮:滚轮方向的内容都会被复制选中

  • 空格:选择要复制的终点,这些部分会被保存到 copy buffer 中

  • ctrl+A+>:将选择的内容保存到 /tmp/screen-exchange 中

  • 之后在本机器的 /tmp/screen-exchange 获得这部分数据即可

需要注意,screen 不会保存所有的串口输出,只有大概 1000 行左右,所以要及时拷贝需要的数据。