如何在C语言中测量mmap()这类快速函数的用户时间与系统时间
我希望只统计mmap()函数本身的用户时间与系统时间,排除错误检查、munmap()以及循环逻辑的耗时影响。目前我的初始实现如下:
void measure_mmap_time(double *user_time, double *system_time) { struct rusage start, end; int pagesize = sysconf(_SC_PAGESIZE); double total_user_time = 0; double total_system_time = 0; for (int i = 0; i < NUM_ITERATIONS; ++i) { getrusage(RUSAGE_SELF, &start); // Map a page of memory void *addr = mmap(NULL, pagesize, PROT_READ, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0); getrusage(RUSAGE_SELF, &end); if (addr == MAP_FAILED) { perror("mmap"); exit(EXIT_FAILURE); } if (munmap(addr, pagesize) == -1) { perror("munmap"); exit(EXIT_FAILURE); } // Calculate user and system time for this iteration total_user_time += get_time_diff(start.ru_utime, end.ru_utime); total_system_time += get_time_diff(start.ru_stime, end.ru_stime); } *user_time = total_user_time / NUM_ITERATIONS; *system_time = total_system_time / NUM_ITERATIONS; }
但我想把getrusage(RUSAGE_SELF, &start)和getrusage(RUSAGE_SELF, &end)移到循环外,同时保留munmap()在循环内,于是编写了新的实现,试图通过空循环抵消循环本身的开销:
void measure_mmap_time(double *user_time, double *system_time) { struct rusage start, end; int pagesize = sysconf(_SC_PAGESIZE); double total_user_time = 0; double total_system_time = 0; void *addresses[NUM_ITERATIONS]; getrusage(RUSAGE_SELF, &start); for (int i = 0; i < NUM_ITERATIONS; ++i) { // Map a page of memory addresses[i] = mmap(NULL, pagesize, PROT_READ, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0); } getrusage(RUSAGE_SELF, &end); for (int i = 0; i < NUM_ITERATIONS; ++i) { if (munmap(addresses[i], pagesize) == -1) { perror("munmap"); exit(EXIT_FAILURE); } } // Calculate user and system time for this iteration total_user_time = get_time_diff(start.ru_utime, end.ru_utime); total_system_time = get_time_diff(start.ru_stime, end.ru_stime); getrusage(RUSAGE_SELF, &start); for (int i = 0; i < NUM_ITERATIONS; ++i) { } getrusage(RUSAGE_SELF, &end); double total_user_time2 = get_time_diff(start.ru_utime, end.ru_utime); double total_system_time2 = get_time_diff(start.ru_stime, end.ru_stime); *user_time = (total_user_time - total_user_time2) / NUM_ITERATIONS; *system_time = (total_system_time - total_system_time2) / NUM_ITERATIONS; }
请问这种新方案是否合理?
你的新方案核心思路是成立的,但存在几个需要修正的细节,以下是具体分析:
1. 核心思路的合理性
将批量mmap操作放在统一计时区间内,再通过空循环耗时抵消循环本身的用户态开销,这个做法能有效减少getrusage的调用次数(原方案每次循环都调用两次getrusage,本身会引入额外开销),同时避免munmap和错误检查干扰计时,整体逻辑是合理的。
2. 存在的问题与优化点
(1)空循环被编译器优化的风险
现代编译器会识别空循环并直接优化掉,导致total_user_time2和total_system_time2几乎为0,无法真实抵消循环本身的耗时。解决方法是给空循环添加一个无意义但能阻止优化的操作:
volatile int dummy = 0; getrusage(RUSAGE_SELF, &start); for (int i = 0; i < NUM_ITERATIONS; ++i) { dummy++; // 强制编译器执行循环逻辑 } getrusage(RUSAGE_SELF, &end);
也可以在编译时添加-O0禁用优化,但这会影响整体代码执行效率,不如添加volatile变量灵活。
(2)缺失mmap的即时错误检查
新方案中mmap后没有立即判断是否映射成功,如果某次mmap返回MAP_FAILED,后续的munmap操作会传入无效地址,引发未定义行为。应该在mmap循环内就做错误检查,并释放已成功映射的内存:
for (int i = 0; i < NUM_ITERATIONS; ++i) { addresses[i] = mmap(NULL, pagesize, PROT_READ, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0); if (addresses[i] == MAP_FAILED) { perror("mmap"); // 清理已成功映射的内存 for (int j = 0; j < i; j++) { munmap(addresses[j], pagesize); } exit(EXIT_FAILURE); } }
(3)批量mmap的缓存效应
多次连续mmap会让内核的内存分配器、页表等数据结构处于"热"状态,测量出的耗时会比单次冷启动的mmap更短。如果需要测量真实场景中单次mmap的平均耗时(包括冷启动),原方案每次mmap后立即munmap的方式更接近实际情况;如果是测量稳态下的mmap耗时,新方案的批量方式更合适。
3. 两种方案的对比
- 原方案:每次循环单独计时,能避免缓存效应,但
getrusage的调用开销会被平均到每次迭代中,当NUM_ITERATIONS很大时,这个开销占比可以忽略,但迭代次数较少时影响明显。 - 新方案:减少了
getrusage的调用次数,降低了这部分开销,通过空循环抵消循环逻辑的耗时,测量精度理论上更高,但需要处理编译器优化和缓存效应的问题。
内容的提问来源于stack exchange,提问作者union77

