由online judge上的Time Limit Exceeded引发的对虚拟内存的思考

首先来看一段最简单的代码,在两个oj平台上的表现:

#include <iostream>
using namespace std;
char c[1000*1000*100];
int main() {
    int a, b;
    while (cin >> a >> b) { cout << a+b << endl;}
    return 0;
}

大家一眼就可以看出,这是各大oj有名的1000问题——a+b problem 有木有!?

在zoj上,报Memory Limit Exceed.

而在bnuoj上,Accepted!

思考了两天,在各大论坛和qq群里发贴讨论,同学们给出了各种猜测,例如:

1.有同学说:各大oj的栈/堆限制不同吧?(程序中的全局变量不是存在栈或者堆上的好么,再说了程序中声明的是100MB,是必然会超的啊)。

2.还有同学说:zoj是linux系统,bnu是win? (先不论bnu是不是win,为何两个操作系统对内存的判别会有差异?关于win和linux的内存管理策略的差异,我专门去图书馆找了两本书对比,发现两者在虚拟内存和页式内存管理策略上,非常相似,似乎很难找到根源)

3.还有很多同学扯到什么ulimit之类的(显然oj不会用这么粗糙的内存监测方式来对大家的进程作内存监视)

4.还有同学说:zoj是唯物主义,bnu是唯心主义(我想说你这是在调戏我么?)


于是我直接download了两个oj平台的源码进行分析,誓要找出问题的根源。

1.zoj (http://acm.zju.edu.cn/onlinejudge/) 上对memory limit exceeded的判断逻辑:

/****************
**native_runner.cc**
****************/
void NativeRunner::UpdateStatus() {
    int ts = ReadTimeConsumption(pid_);
    int ms = ReadMemoryConsumption(pid_);
    if (ts > time_consumption_) {
        time_consumption_ = ts; 
    }   
    if (ms > memory_consumption_) {
        memory_consumption_ = ms; 
    }   
    if (time_consumption_ > time_limit_ * 1000) {
        result_ = TIME_LIMIT_EXCEEDED;
    }   
    if (result_ == TIME_LIMIT_EXCEEDED && time_consumption_ <= time_limit_ * 1000) {
        time_consumption_ = time_limit_ * 1000 + 1;
    }   
    if (memory_consumption_ > memory_limit_) {
        result_ = MEMORY_LIMIT_EXCEEDED;
    }   
    if (result_ == MEMORY_LIMIT_EXCEEDED && memory_consumption_ <= memory_limit_) {
        memory_consumption_ = memory_limit_ + 1;
    }   
    DLOG<<time_consumption_<<' '<<memory_consumption_;
    if (SendRunningMessage() == -1) {
        result_ = INTERNAL_ERROR;
    }   
}

第六行的函数 ReadMemoryConsumption(pid)是关键函数,我们来看一下它的实现:

/********
**util.cc**
********/
int ReadMemoryConsumption(pid_t pid) {
    char buffer[64];
    sprintf(buffer, "/proc/%d/status", pid);
    FILE* fp = fopen(buffer, "r");
    if (fp == NULL) {
        return -1;
    }
    int vmPeak = 0, vmSize = 0, vmExe = 0, vmLib = 0, vmStack = 0;
    while (fgets(buffer, 32, fp)) {
        if (!strncmp(buffer, "VmPeak:", 7)) {
            sscanf(buffer + 7, "%d", &vmPeak);
        } else if (!strncmp(buffer, "VmSize:", 7)) {
            sscanf(buffer + 7, "%d", &vmSize);
        } else if (!strncmp(buffer, "VmExe:", 6)) {
            sscanf(buffer + 6, "%d", &vmExe);
        } else if (!strncmp(buffer, "VmLib:", 6)) {
            sscanf(buffer + 6, "%d", &vmLib);
        } else if (!strncmp(buffer, "VmStk:", 6)) {
            sscanf(buffer + 6, "%d", &vmStack);
        }
    }
    fclose(fp);
    if (vmPeak) {
        vmSize = vmPeak;
    }
    return vmSize - vmExe - vmLib - vmStack;
}

可以看出,zoj是直接读取/proc/pid/status 这个文件,获取vmPeak, vmExe, vmLib, vmStack后计算出用户所使用的虚拟内存空间大小(减去代码,库,栈所占空间)。



2.bnuoj (http://www.bnuoj.com/bnuoj/) 上对memory limit exceeded的判断逻辑:

(我摘选出关键代码如下:)

/**judger_main.cpp**/
int mem_used = 0;
int runstat;
struct rusage rinfo;
while(1) {
      wait4(pid,&runstat,NULL,&rinfo);  //pid is the child process id(program submitted by acmers)
      if (mem_used<getpagesize()*rinfo.ru_minflt) mem_used=getpagesize()*rinfo.ru_minflt;
      if (mem_used>head->memory_limit*1024) {
                    sprintf(templog,"Run status: %d\n",runstat);
                    generate_result(MLE_STATUS,head->runid,mem_used,total_time);
                    ptrace(PTRACE_KILL,pid,NULL,NULL);
                    return;
       }
}


发现bnuoj是通过wait4内核调用,获取子进程的rusage信息中的ru_minflt字段。

那么这个ru_minflt字段是什么意思呢?

"The ru_minflt value is the number of page faults that required no I/O activity. A page fault occurs when the kernel needs to retrieve a page of memory for the process to access."    ----  http://www.khmere.com/freebsd_book/html/ch07.html

恍然大悟。我们知道操作系统页式内存管理策略是,物理内存是宝贵的,因此不到需要使用的时候,不把虚拟内存映射到物理内存上。换句话说,只有当程序对某一页虚拟内存进行访问时,程序发生缺页错误(page faults),操作系统才会建立那页虚拟内存和物理内存的映射。因此我们可以通过page faults的次数,来判断程序对物理内存的使用量。


至此,这个问题算是找到根源了。为了确定这个结论,我联系了hustoj目前的代码负责人。非常巧的是,他告诉我,旧版的hustoj采用的是bnuoj的判别方式,而新版的改用了zoj的判别方式。他说这么做有两点考虑:

1.总有人跑来问他为何申明了很大的全局数组,还能ac。

2.鼓励c/c++程序员使用动态内存分配,用多少申请多少。


可是我觉得bnuoj的评测方式其实也是有道理的。因为物理内存的使用量更能反应程序对服务器硬件资源的占用情况。

总之仁者见仁智者见智吧。


最后我想说的是,遇到这样的问题,作为程序员,应该努力去解释它。那些不经思考随口猜测的同学,还有那位唯心论者,希望以后做技术严谨一些。

国内就是因为太多人在网络上猜测式地回答,导致国内的技术论坛和国外相比有如此大的差距。

你可能感兴趣的:(由online judge上的Time Limit Exceeded引发的对虚拟内存的思考)