我一直在从事一个爱好项目(用 C 编写),但它还远未完成。它的速度非常重要,所以我最近决定做一些基准测试来验证我解决问题的方法不会低效。
$ time ./old
real 1m55.92
user 0m54.29
sys 0m33.24
我重新设计了程序的某些部分,以显着删除不必要的操作,减少内存缓存未命中和分支错误预测。出色的 Callgrind 工具向我展示了越来越多令人印象深刻的数字。大多数基准测试都是在没有分叉外部流程的情况下完成的。
$ time ./old --dry-run
real 0m00.75
user 0m00.28
sys 0m00.24
$ time ./new --dry-run
real 0m00.15
user 0m00.12
sys 0m00.02
显然,我至少在做正确的事情。然而,真正运行该程序却讲述了一个不同的故事。
$ time ./new
real 2m00.29
user 0m53.74
sys 0m36.22
您可能已经注意到,时间主要取决于外部流程。我不知道是什么导致了回归。没有什么特别奇怪的。只是由单个线程完成的传统 vfork/execve/waitpid,以相同的顺序运行相同的程序。
一定是有什么东西导致分叉变慢,所以我做了一个小测试(类似于下面的测试),它只会产生新进程并且没有与我的程序相关的开销。显然,这必须是最快的。
#define _GNU_SOURCE
#include <fcntl.h>
#include <sys/wait.h>
#include <unistd.h>
int main(int argc, const char **argv)
{
static const char *const _argv[] = {"/usr/bin/md5sum", "test.c", 0};
int fd = open("/dev/null", O_WRONLY);
dup2(fd, STDOUT_FILENO);
close(fd);
for (int i = 0; i < 100000; i++)
{
int pid = vfork();
int status;
if (!pid)
{
execve("/usr/bin/md5sum", (char*const*)_argv, environ);
_exit(1);
}
waitpid(pid, &status, 0);
}
return 0;
}
$ time ./test
real 1m58.63
user 0m68.05
sys 0m30.96
我猜不是。
这时候我决定为州长投票,而且时间变得更好了:
$ for i in 0 1 2 3 4 5 6 7; do sudo sh -c "echo performance > /sys/devices/system/cpu/cpu$i/cpufreq/scaling_governor";done
$ time ./test
real 1m03.44
user 0m29.30
sys 0m10.66
似乎每个新进程都安排在一个单独的核心上,并且需要一段时间才能切换到更高的频率。我不能说为什么旧版本跑得更快。也许是幸运的。也许它(由于它的低效率)导致CPU更早地选择更高的频率。
改变调控器的一个很好的副作用是编译时间也得到了改善。显然编译需要分叉许多新进程。但这不是一个可行的解决方案,因为该程序必须在其他人的台式机(和笔记本电脑)上运行。
我发现改善原始时间的唯一方法是通过在开头添加以下代码将程序(和子进程)限制为单个 CPU:
cpu_set_t mask;
CPU_ZERO(&mask);
CPU_SET(0, &mask);
sched_setaffinity(0, sizeof(mask), &mask);
尽管使用了默认的“ondemand”调控器,但实际上是最快的:
$ time ./test
real 0m59.74
user 0m29.02
sys 0m10.67
它不仅是一个骇人听闻的解决方案,而且在启动的程序使用多个线程的情况下效果不佳。我的程序无法知道这一点。
有谁知道如何让生成的进程以高 CPU 时钟频率运行?它必须是自动化的,并且不需要 su 特权。虽然到目前为止我只在 Linux 上对此进行了测试,但我打算将其移植到或多或少所有流行和不受欢迎的桌面操作系统(它也将在服务器上运行)。欢迎任何平台上的任何想法。