微观基准-可能会出错的地方

2020-06-20 12:14:59

我需要不时地比较Genode的硬件内核与Genode支持的硬件平台之一的基准(通常使用Linux)之间的执行时间和内存吞吐量。大多数情况下,这是在将硬件内核移植到新硬件上,或者我们在平台上遇到不寻常的工作负载时完成的,就像最近在Raspberry PI 1上研究无法工作的USB驱动程序时一样。

在深入研究复杂驱动程序的实现细节之前,我首先想排除HW内核不适当地初始化硬件,这就是驱动程序没有足够的CPU时间或经历过高延迟的原因。

在过去,我经常使用一个简单的bogomips例程,它可以在Linux和Genode之上执行:

void bogomips()__ATTRIBUTE__((Optimize(";O0";);void bogomips(){for(注册无符号i=0;i<;1000000000;i++){;}};

当在执行循环时观察到两个系统上的运行时间大致相同时,我确信至少CPU以相同的频率运行,并且关于CPU没有大的配置问题。当然,我很懒,以为Linux内核开发人员已经正确配置了硬件,编译器会生成不会触及循环内内存的指令。否则,我将测量内存访问延迟,而不是CPU执行时间。在最初编写和编译上面的简单循环时,我当然已经检查了生成的二进制文件,并查看了不同体系结构的反汇编代码,它产生了假定的结果。这意味着退出条件值被转移到一个寄存器中,而另一个零初始化的寄存器被递增,直到它达到第一个寄存器的值。

现在,在使用较新的Genode工具链测试Raspberry PI 1时,我可以观察到编译器生成了以下代码:

01000084<;_Z8bogomipsv>;:1000084:e92d0810推送{r4,fp}1000088:e28db004添加fp,sp,#4 100008c:e3a04000移动r4,#0 1000090:e59f301c ldr r3,[pc,#28];10000b4<;_Z8bogomipsv+0x30>;1000094:e1540003 cmp。10000a4:e320f000 NOP{0}10000a8:e24bd004 subsp,fp,#4 10000ac:e8bd0810 POP{r4,fp}10000b0:e12fff1e bx LR 10000b4:3b9ac9ff.word 0x3b9ac9ff。

可以看到,存储在地址0x10000b4的退出条件值在每个循环周期期间加载,这更适合测量存储器访问时间。

因此,我不得不咬紧牙关,为每个体系结构提供一个汇编例程,而不是使用高级语言函数进行测量。我写了这样的东西:

.global bogomips bogomips:Push{R4}mov R4,#0 1:CMP R4,R0 addne R4,R4,#1 NOP bne 1b 2:POP{R4}mov PC,LR。

当在Linux和Genode上执行相同的例程时,我可以测量到Genode执行相同循环所需的时间是Linux的两倍,大约30秒。

在对平台初始化代码进行了简短的回顾之后,我很快发现这个特定CPU上的分支预测器从未启用过,因为Raspberry PI 1支持进入了Genode。幸运的是,我的发现让我确信,我们现在的行为将完全像Linux一样,但我测量到,我们的速度仍然要慢得多。

好的,我的第一个想法是:可能Linux内核改变了CPU的时钟速度。但在对Linux内核进行了几轮调查和检测之后,我找不到任何相关的东西。在此平台上,您必须调用运行在GPU上的固件来更改CPU时钟频率。但是调用GPU端固件的Linux驱动程序没有被调用来更改它。我在Genode中动态提高时钟速度的所有尝试都失败了,直到我了解到您必须在固件中配置加载时的最大和最小时钟速度值才能在运行时更改它。不管怎么说,Linux和Genode的时钟速度是一样的,这是一条死胡同。

为了找出Linux何时启动了正确的开关,我在Linux内核一开始就移动了那个bogomips&34;循环,结果发现:它和Genode下一样慢!

我一步一步地把它移到内核初始化的末尾,但它仍然很慢。当在普通的Linux用户程序中执行相同的汇编循环时,它需要13秒多一点的时间,但是无论我在内核中执行它,它都需要17秒多一点的时间-与我在Genode下测量的时间相同。即使当我将该例程放入内核模块并动态加载时,它仍然保持较慢的速度。

因此,我深入研究了Linux内核如何将页表属性用于不同的内存区域。我认为他们可能会在这个平台上使用ARM Tex重映射方法(不会),或者有其他差异化属性。在这样做的同时,我了解到Linux在这个平台上对IO、Kern和User使用了甚至不同的内存域,这在某种程度上可以与x86上的分段进行比较。然而,无论我如何处理Genode下的分页属性,它都和以前一样慢。只有在我拼命将分页转换成ARMv5兼容格式-有效地关闭了一些访问权限标志-它才变得更快。这对我来说是意想不到的。上面的循环非常适合指令高速缓存-不需要再次从内存中取出它。我还在内核内执行了它,但禁用了中断。它从来没有被打断过。但尽管如此,当使用ARMv5页表格式关闭与MMU和TLB使用相关的某些权限位时,整个循环的速度加快了约3秒。在这里,整个几乎看不见的、投机性的行刑情结的影响变得显而易见。

最后,我丢弃了所有的实验(当然,我保留了启用的分支预测器;-),并重新研究了Linux userland程序和Genode组件的编译。当然,汇编程序是一样的,但是.。你可能已经猜到了..。呃不,它链接到不同对齐的地址。在Genode上,它是单词对齐的,就像它需要在ARM上一样。但在Linux上,该例程是256字节对齐的。当通过在例程开始时添加一些指令来人工移动Linux例程时,它变得与Genode上一样慢。

顺便说一句,这可能是因为我使用了两个不同的编译器(是的,Sebastian,您是对的,在进行基准测试时千万不要这么做!),因为我没有适用于arm Linux的Genode工具链,而是使用了Raspbian包中的GCC。我觉得没关系,因为我测量的10G指令都是用汇编语言手写的。无论如何,即使由于汇编器例程链接到的环境不同而使用相同的编译器,您也必须明确定义对齐方式。否则,您可能会比较称为现代CPU的黑盒内部完全不同的硬件代码路径。