CodeCraft背后的故事:程序究竟有多快?

CodeCraft背后的故事:程序究竟有多快?_第1张图片
华为软件精英挑战赛

华为第二届校园软件大赛的题目,是图论中的寻路算法题 。题目要求在一个有向图中,找出经过特定节点集合并且权重最小的路径。大赛吸引了数万名大学生报名参加,如何判断出优胜者呢?首先看寻找到的路径的权重,权重小的胜出;如果权重一样,那么就要比较程序的运行效率,算法效率更高的胜出。

这到题目相当的高大上,而且极有挑战性,属于一个NP-Complete问题。

作为赛事承办方,我们承担了大赛官网平台以及后台实时判题系统的开发工作,其中一项开发任务就是要度量出选手提交的程序的运行时间,以此来评判其算法的效率。现在赛事已经结束了,可以来扒一扒我们是怎么来度量进程运行时间的故事了。

选手提交的程序都运行在Linux上的docker虚拟容器中,每一个容器都虚拟出一个纯净的Linux环境。我们首先想到的就是使用Linux系统自带的 time 命令,例如:
yuhou@Lenovo:~/$ time ping -c 3 localhost > /dev/null
real 0m2.001s
user 0m0.004s
sys 0m0.004s
yuhou@Lenovo:~/$

这个命令返回三个时间,real是进程存在的总时间(也叫wallclock,等同于墙上的挂钟流逝的时间),user态下进程占用CPU的时间,以及内核态下进程占用CPU的时间。判题程序直接用real时间,来判断程序运行的效率;判题系统在执行每一个选手提交的程序的时候,设置了10秒的超时时间,如果程序运行超过10秒,会被强制Kill掉。并且我们的对外宣传也提出了:程序最长的运行时间是10秒,超时会被判为0分。

判题系统安然度过了两天的挑战。3月24日,为了禁止考生使用多核并发来提速,我们将运行沙箱的容器设置为单核模式,结果,这个小小的改动,却让我们的判题系统很快就遇到了挑战。当天晚上,有考生在向官网提交程序后,向我们反馈:我的程序绝对不可能超时的,但是你们的判题系统却为何说我超时了呢?

凌晨,我们开始紧急排除故障,经过分析发现,原来部分选手为了充分利用这10秒的计算时间,使得他们找到路径的权重尽可能的低,在程序中设置了一段检查超时的代码,只要还没有超时,那么就进行进一步深入的搜索和计算,其逻辑类似如下:
while (true) {
    clock_t  t1= clock();
    /* find the path ,and refine it ... */
    clock_t  t2 = clock();
    if ((t2 - t1)/CLOCKS_PER_SEC > 9) {
        break;
    }
}

我们分析了 clock 函数的说明,原来,clock函数返回的是一个硬件tick数,它描述了本进程真正被调度到CPU上被执行的时间。上面的这段代码,就能够确保程序占用CPU的时间不会超过10秒钟;但是,因为Linux是一个多任务操作系统,进程在执行过程中是有可能被抢占的,所以,这个进程真正生存的时间,是很有可能超过10秒的。如果当判题程序发现进程存在超过10秒后就立刻Kill这个进程,实际上其真正被调度到CPU上执行的时间,是不足10秒的。这就造成了误杀。

这从 time 命令的返回结果可以看出, real 时间一般是大于user时间和sys时间和的。那为什么在docker容器没有修改为单核模式前,就没有这种误杀的情况出现呢?这其实很好理解,如果docker容器中的Linux是多核的,那么具有更多的CPU可被使用,选手提交的程序是很少被抢占的,其real time和 cpu time是几乎相等的,所以就不存在误杀的情况。但是一旦修改为单核,选手提交的程序就会被经常抢占,其CPU时间会远远小于real time,所以就很容易被误杀了。

找到原因之后,我们马上修改了判题程序的逻辑,延长了判断超时的时间长度,并且使用 user time + sys time 的和,来作为程序真正运行时间的标准。这样修改之后,问题就马上消失了。

最后再对这个问题深入探讨一下。使用 time 命令返回的结果作为程序运行时间,也是存在误差的。按照POSIX标准,clock函数的计时精度是10ms,如果我们要较真的话,应该提供微秒级别精度的计时器,那有没有准确到微秒的计时方法呢?答案是可以做到,但是没有像 time命令这样现成的方法,需要自己动手来实现。一种方法是用进程注入技术,以及clock_gettime 函数,来自己写一个类似 micro_time 的命令,用这个命令就可以度量进程的精确到微秒级别的CPU时间消耗。这个实现起来稍有难度,而且进程占用CPU的时间受到很多不确定因素的影响,比如进程的切换,缓存的失效等。精确到微秒级别并无太大的意义。

我们在实际判卷的时候,所有进程的耗时都精确到10ms。实践证明,精确到10ms已经能够很好的区分出选手的算法差异了。

你可能感兴趣的:(CodeCraft背后的故事:程序究竟有多快?)