这是Andras发表的一篇在OMNeT++中如何使用EV来有效的输出日志信息的文章。原网址在http://www.omnetpp.org/article.php?story=20040804203301352 。
译者注:现在的 OMNeT++4.0 已经将这里的结果集成了进去,推荐使用 EV 来输出日志信息。具体的定义见 include/cenvir.h 文件。这篇文章详细的讨论了如何设置 EV ,具有普遍的借鉴作用。
ev<< 语句可以用来打印信息从而了解到仿真模型正在做什么。这在进行调试和理解模型运行的时候是很有用的。现在的问题是,当模型需要运行很长时间的时候, ev<< 语句将会消耗大量的 CPU 周期。这个时候该怎么办呢?
本文将介绍如何高效率的进行记录,并且同时介绍如何创建 log channels 或者调试 channels 。
在 Cmdevn 环境下,当设置 express-mode=true 的时候,输出将会被丢弃而不会被打印,从而使得执行可以更快。但是实际上, ev<< 语句还是会带来一些开销。这种速度的减小可能不会被注意到,但是影响却是很大的。这里的问题是, OMNeT++ 将只会丢弃也就是不打印已经转换为文本格式的字符串。举个例子,如下面的语句:
ev << "Average bit/sec is: " << totalBits/simTime() << "n";
即使是在极速模式下, simTime() 将会被调用,浮点数除法也会被执行。结果将会被转换为字符串并存储在缓冲区中,而这里的缓冲区则会被丢弃。很遗憾的是, OMNeT++ 核心没有办法阻止这种事情发生,因为这是 C++ 的工作方式。
这时候该怎么办呢?一种常见的方式是采用 #ifdef 。
#ifdef DEBUGGING ev << "Average bit/sec is: " << totalBits/simTime() << endl; #endif
这并不坏,但是却有一个很严重的问题:在切换打印输出的时候必须重新编译所有的文件。根据墨菲定律(有可能出错的事情,就会出错,Anything that can go wrong will go wrong),当人们需要输出的时候往往看不到有输出。另外,代码中满含有 #ifdef 也不是一个好办法。随后想到的就是如何在编译的时候就将是否输出考虑到,下面是一个示例代码。
if (debugging) ev << "Average bit/sec is: " << totalBits/simTime() << endl;
这比前面要好一些。 if 语句的开销是比较小的。这样,就可以在初始化的时候通过 debugging 变量来决定是否输出信息。
debugging = par("debugging").boolValue();
这种做法还不是很方便,因为我们需要手工维护输出的状态。有些人会发现此值可以在调试中进行动态设置,但是这还不能令人满意。为什么不能让代码知道我们是否需要记录呢?
实际上,我们可以回答这个问题。一般的, OMNeT++ 知道我们何时需要进行记录:不在极速模式下的时候。幸运的是, ev 对象可以知道这点,所以现在最新的代码如下。
if (!ev.disabled()) ev << "Average bit/sec is: " << totalBits/simTime() << endl;
几乎就是这样了。现在的问题是需要为每个 ev 输出增加一个 if 语句。有经验的 C/C++ 程序员将会马上想到采用宏来产生精炼的代码。第一次尝试:
#define EV if (!ev.disabled()) ev // *** DANGEROUS!*** ... EV << "Average bit/sec is: " << totalBits/simTime() << endl;
注意,这里的宏将会产生很严重的问题。考虑一下下面的代码:
if (totalBits>1000) EV << "Average bit/sec is: " << totalBits/simTime() << "n"; else EV << "Not enough data yet" << endl;
这段代码并不会按照设想的那样工作。当我们宏替换完成并重新缩进代码后,将得到下面的代码:
if (totalBits>1000) if (!ev.disabled()) ev << "Average bit/sec is: " << totalBits/simTime() << endl; else if (!ev.disabled()) ev << "Not enough data yet" << endl;
所以这里的代码将永远不会打印出“ Not enough data ”。
最好是忘记上面的 EV 定义,因为这很容易会使得你栽在上面。即使是在这个宏定义中加上一对括号也不能解决问题,因为打印参数将会在括号之外。看起来这个问题没法修复。
尽管如此,让我们来看看下面这个版本:
#define EV ev.disabled() ? ev : ev ... EV << "Average bit/sec is: " << totalBits/simTime() << endl;
这看起来有点奇怪。这里的宏定义看起来没有区别(无论是为 true 或者是 false ),而且这和所有 C 语言教科书中所倡导的(宏如果需要扩展成表达式需要加上圆括号)相违背。但是确实这个宏是可以工作的。现在 EV<< 将会变成一个简单的表达式。
ev.disabled() ? ev : ev << "Average bit/sec is: " << totalBits/simTime() << endl;
这和下面的相同(注意符号的优先级) :
ev.disabled() ? ev : (ev << "Average bit/sec is: " << totalBits/simTime() << endl);
这时候,当 ev 被禁止的时候(条件为 true ),这只是简单的一个 ev 对象的引用(这最终将会被编译器所优化,而不会产生任何的 CPU 指令);当 ev 启用的时候(条件为 false ),将会被还原成原始的 ev<< 语句。这正是我们所需要的。证明这里的 EV 定义在任何使用场景中都是可行的,这可以作为练习。无论是否有 if 语句,或者是还有一个 ?: 操作符,或者是其他的场景,这都是适用的。
实际上,上面 EV 的 ?: 版本并不能在 VC++ 7.0 中通过编译(因为在 VC++ 7.0 中需要?:三元操作符的第二个和第三个参数的类型是一样的,而不会做默认的转换)。所以在 VC7 中的版本是这样的:
#define EV ev.disabled() ? (std::ostream&)ev : ev
(译者注:实际上,由于现在最新的 OMNeT++ 4.0 并不支持使用 VC 编译器进行编译,所以也没有采用这样的方式)。
如果你没有用过 log4j 或者是 C++ 中类似的工具( log4Cpp , libCWD 等),那你有可能错过调试管道或者说是日志管道。简单地说, channels 是针对快速滚动日志问题的答案(因为你几乎不可能在日志的海洋中找到有用的信息)。你的代码日志将会记录到多个管道中,而在调试的时候可以只关注自己感兴趣的管道。有两个标准可以用来区分管道日志: topic 和调试级别(如 detail, info, warnings )。其中第三个标准是位置(模块位置),这已经被 OMNeT++ 内置了。可以通过查看模块的输出来得到你想要的信息。
一个比较好的消息是通过上面的 EV 定义,可以用来简单的模拟日志管道。当书写一个 IP 模块的时候,检查下面的定义:
#define fwdingEV (ev.disabled()||!fwdingChannel) ? (std::ostream&)ev : ev #define localEV (ev.disabled()||!localChannel) ? (std::ostream&)ev : ev #define mcastEV (ev.disabled()||!mcastChannel) ? (std::ostream&)ev : ev #define dropEV (ev.disabled()||!dropChannel) ? (std::ostream&)ev : ev
上面的定义提供了四个日志信道( wdingEV, localEV, mcastEV, dropEV ),可以通过单独设置 fwdingChannel, localChannel, mcastChannel, dropChannel 布尔变量的值来进行开关。可以像下面这样使用日志管道。
... EV << "packet received" << endl; ... if (destAddress.isMulticast()) { mcastEV << "multicast packet, addr=" << destAddress << endl; ... } ... if (!routeFound) { dropEV << "unroutable packet, dropping" << endl; delete datagram; } ...