首先来看一段最简单的代码,在两个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的评测方式其实也是有道理的。因为物理内存的使用量更能反应程序对服务器硬件资源的占用情况。
总之仁者见仁智者见智吧。
最后我想说的是,遇到这样的问题,作为程序员,应该努力去解释它。那些不经思考随口猜测的同学,还有那位唯心论者,希望以后做技术严谨一些。
国内就是因为太多人在网络上猜测式地回答,导致国内的技术论坛和国外相比有如此大的差距。