You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

为何空耗循环会占用大量内核时间?技术问询

为什么这个程序会消耗大量内核态时间?

这个问题的核心原因是Linux虚拟内存延迟分配机制带来的大量缺页异常(Page Fault),下面一步步拆解来解释:

1. 内存分配的"假象":new并没有立即分配物理内存

你用new int[TABSIZE]申请了1GB左右的内存(25610241024个int,每个4字节刚好1GB),但Linux下的用户态内存分配是延迟分配的:

  • new调用实际上只是向内核申请了一段虚拟地址空间,内核并没有立刻为你分配对应的物理内存页。
  • 这也是你觉得new操作耗时很短的原因——它只做了地址空间的标记,根本没碰物理内存。

2. 循环触发大量硬缺页,内核在后台忙不停

当你进入循环tab[i] *= i时,这是第一次访问每个内存页(每个页大小通常是4KB,1GB对应约262144个页):

  • 第一次访问未映射到物理内存的虚拟地址时,会触发硬缺页异常(Major Page Fault)
  • 此时CPU会切换到内核态,由内核完成一系列操作:分配物理内存页、建立虚拟地址到物理地址的页表映射、更新MMU(内存管理单元)配置等。
  • 每一次缺页处理都需要内核介入,这部分时间全部被统计到sys时间里,这就是你的内核态时间占比极高的原因。

3. 为什么用户态时间(user)这么低?

O2优化后,循环里的tab[i] *= i本身是非常简单的运算:取地址、读值、乘i、写回。但相比内核处理缺页的开销,这些用户态计算的耗时可以忽略不计——大部分时间CPU都在处理内核态的缺页逻辑,用户态代码的执行时间自然占比很小。

验证方法

你可以用perf stat ./a.out查看具体的缺页次数,比如执行:

perf stat ./a.out

输出里会看到major faults(硬缺页)的数量接近26万,这就是内核时间的直接来源。

额外测试:提前分配物理内存

如果想验证这个结论,可以在循环前先"触摸"所有内存页,强制内核分配物理内存:

#include <iostream>
using namespace std;
#define TABSIZE 256*1024*1024
int main(){
    volatile int *tab = new int[TABSIZE];
    cerr << "start" << endl;
    // 提前触摸所有页,触发缺页并完成物理内存分配
    for(int i=0;i<TABSIZE;i+=4096/sizeof(int)) tab[i] = 0;
    // 现在循环的sys时间会大幅降低
    for(int i=0;i<TABSIZE;i+=1) tab[i] *= i;
    cerr << "stop" << endl;
    delete []tab;
    return 0;
}

重新编译运行后,你会发现sys时间明显减少,user时间会相应上升——因为此时内核已经完成了所有物理内存的分配,循环里只有用户态的计算操作了。

内容的提问来源于stack exchange,提问作者bebidek

火山引擎 最新活动