杰哥胡搞搞之mudlib性能卡点分析
拿我手头的mudlib做基础分析的。虽然和北侠的从底层就不同,但是几十年前应该都是一家,毕竟一些远古的一些解密,比如华山的金蛇,少林读石桌这些远古代码都是一模一样的。
当然,我这份mulib已经是20年前的了,北侠经过这么多年应该已经很多地方都优化过了。我就胡搞搞,玩一又用是好时,如果没用,也正常,就当水一帖。
以这个mudlib来说,几百个id挂机,几秒一个任务,连遍历带战斗,一般情况下都不带卡的。所以我觉得是一个很好的参考对象。
以我在北侠的经验来说,容易卡(上top cmd)的点有几个
1.移动/遍历
2.学习
3.一些特殊指令
4.一些特殊的群战场景。
其中3 4 还好理解,1是最特殊的。北侠这样走两步卡几步,走几分钟休息半小时的模式,是我之前从来没接触过的状态,所以也是我这次瞎琢磨的重点。
首先让我们找到移动的代码,这个我熟悉,cmd/std/go.c
把拉巴拉一段战斗逃跑的判断,然后
me->move(dest);
恩,好理解,把当前对象移动到dest下。
这里mudos是这样的,所有的东西都是obejct,都是平级。obecjt下可以有子元素,就是道具。这样,一个道具即可以在room里,也可以在玩家身上,也可以在玩家身上的容器里,也可以在房间的容器里。大家都是对象,平级,都能装。
然后找move方法的定义
恩,在feature/move.c里
一堆负重的判断,然后调用了一个 代码
move_object(ob);
恩,这个我看文档看到过,是一个efun(官方api),让我们打开页面看看,继续水一点
https://www.fluffos.info/zh-CN/efun/move_object.html
移动当前对象到环境 `dest`,移动后会触发当前对象和目标环境及其中对象的 init() 方法。继续水,但是init不是初始化么,为啥移动会初始化一下?不管了,继续爬文档。
熟练的打开文档https://www.fluffos.info/zh-CN/apply/object/init.html
然后,然后,然后,我被硬控了几秒
当 MUDLIB 移动对象 `A` 进入对象 `B`时,游戏驱动的 move_object() 外部函数会做以下行为:1. 如果 `A` 是生物(living),让 `A` 呼叫 `B` 的 init() 方法。2. 不管 `A` 是否是生物,让 `B` 中的所有生物呼叫 `A` 的 init() 方法。3. 如果 `A 是生物,让 `A 呼叫 `B` 中所有对象的 init() 方法。1.我还能理解,让移动入的元素和房间/容器交互下。2和3是什么鬼?和房间/容器所有的容器都交互下,然后再被房间里所有的容器交互下?这是啥,连还尸爆么?
看的我一脸蒙蔽,爬下源代码
efun我知道,直接搜f_move_object()就行
调用的是src/package/core/efuns_main.cc里的#ifdef F_MOVE_OBJECT
void f_move_object() {
object_t *o1, *o2;
/* get destination */
if (sp->type == T_OBJECT) {
o2 = sp->u.ob;
} else {
if (!(o2 = find_object(sp->u.string)) || !object_visible(o2)) {
error("move_object failed: could not find destination\n");
}
}
if ((o1 = current_object)->flags & O_DESTRUCTED) {
error("move_object(): can't move a destructed object\n");
}
move_object(o1, o2);
pop_stack();
}
然后move_object方法是src/internal/base/simulate.cc里void move_object(object_t *item, object_t *dest) {
object_t **pp, *ob;
save_command_giver(command_giver);
/* Recursive moves are not allowed. */
for (ob = dest; ob; ob = ob->super) {
if (ob == item) {
error("Can't move object inside itself.\n");
}
}
#ifndef NO_SHADOWS
if (item->shadowing) {
error("Can't move an object that is shadowing.\n");
}
#endif
if (!CONFIG_INT(__RC_NO_RESETS__) && CONFIG_INT(__RC_LAZY_RESETS__)) {
try_reset(dest);
}
#ifndef NO_LIGHT
add_light(dest, item->total_light);
#endif
if (item->super) {
int okay = 0;
remove_sent(item->super, item);
remove_sent(item, item->super);
#ifndef NO_LIGHT
add_light(item->super, -item->total_light);
#endif
for (pp = &item->super->contains; *pp;) {
if (*pp != item) {
remove_sent(item, *pp);
remove_sent(*pp, item);
pp = &(*pp)->next_inv;
continue;
}
/*
* unlink object from original inventory list
*/
*pp = item->next_inv;
okay = 1;
}
#ifdef DEBUG
if (!okay) {
fatal("Failed to find object /%s in super list of /%s.\n", item->obname, item->super->obname);
}
#endif
}
/*
* link object into target's inventory list
*/
item->next_inv = dest->contains;
dest->contains = item;
item->super = dest;
setup_new_commands(dest, item);
restore_command_giver();
}
恩,明显道具是个链表,里面有个不起眼的 setup_new_commands
让我再找一下,在src/packages/core/add_action里(话说从内部包引用efun包,这合理么?):void setup_new_commands(object_t *dest, object_t *item) {
object_t *next_ob, *ob;
/*
* Setup the new commands. The order is very important, as commands in
* the room should override commands defined by the room. Beware that
* init() in the room may have moved 'item' !
*
* The call of init() should really be done by the object itself (except in
* the -o mode). It might be too slow, though :-(
*/
if (item->flags & O_ENABLE_COMMANDS) {
save_command_giver(item);
(void)apply(APPLY_INIT, dest, 0, ORIGIN_DRIVER);
restore_command_giver();
if (item->super != dest) {
return;
}
}
/*
* Run init of the item once for every present user, and for the
* environment (which can be a user).
*/
for (ob = dest->contains; ob; ob = next_ob) {
next_ob = ob->next_inv;
if (ob == item) {
continue;
}
if (ob->flags & O_DESTRUCTED) {
error("An object was destructed at call of " APPLY_INIT "()\n");
}
if (dest != ob->super) {
error("An object was moved at call of " APPLY_INIT "()\n");
}
if (ob->flags & O_ENABLE_COMMANDS) {
save_command_giver(ob);
(void)apply(APPLY_INIT, item, 0, ORIGIN_DRIVER);
restore_command_giver();
if (dest != item->super) {
return;
}
}
if (item->flags & O_DESTRUCTED) { /* marion */
error("The object to be moved was destructed at call of " APPLY_INIT "()\n");
}
if (ob->flags & O_DESTRUCTED) { /* Alaron */
error("An object was destructed at call of " APPLY_INIT "()\n");
}
if (item->flags & O_ENABLE_COMMANDS) {
save_command_giver(item);
(void)apply(APPLY_INIT, ob, 0, ORIGIN_DRIVER);
restore_command_giver();
if (dest != item->super) {
return;
}
}
}
if (dest->flags & O_DESTRUCTED) { /* marion */
error("The destination to move to was destructed at call of " APPLY_INIT "()\n");
}
if (item->flags & O_DESTRUCTED) { /* Alaron */
error("The object to be moved was destructed at call of " APPLY_INIT "()\n");
}
if (dest->flags & O_ENABLE_COMMANDS) {
save_command_giver(dest);
(void)apply(APPLY_INIT, item, 0, ORIGIN_DRIVER);
restore_command_giver();
}
}还真是互相交互两边啊,这不是性能炸弹么。
再让我们找个文件看看init()一般是干什么的。
void init()
{
object me;
::init();
if (interactive(me = this_player()))
{
remove_call_out("greeting");
call_out("greeting", 1, me);
}
me->delete_temp("decide_withdraw");
me->delete_temp("demolish_room");
add_action("do_stop", "stop");
add_action("do_answer", "answer");
add_action("do_desc", "desc");
add_action("do_show", "show");
add_action("do_changename", "changename");
add_action("do_changeid", "changeid");
add_action("do_changetype", "changetype");
add_action("do_changedesc", "changedesc");
add_action("do_finish", "finish");
add_action("do_finish", "ok");
add_action("do_withdraw", "withdraw");
add_action("do_withdraw", "chexiao");
add_action("decide_withdraw", "decide");
add_action("do_demolish", "demolish");
// 如果来的是有资格处理表单的巫师就增加处理表单的命令
if (wizardp(me) && wiz_level(me) >= WIZLEVEL)
{
add_action("do_help", "help");
add_action("do_list", "list");
add_action("do_type", "type");
add_action("do_agree", "agree");
add_action("do_reject", "reject");
add_action("do_delete", "delete");
}
}
好家伙,是为了绑临时指令啊。
话说每走一步,捡个东西,丢个东西/放个东西到包裹里,都要这样来一圈,好扯啊。
这init()函数,不是正常应该都为空,不正常情况采取使用么。
我深度怀疑,北侠的移动指令这么容易上榜,可能就是和init优点重,或者addaction部分太重可能优点关系。
这种对象之间随意初始化指令,互相装配的模式,虽然我很喜欢,但能理解,大概相当于开发板
也就是这种东西
一看就是自由扩展,全能,可扩展性高
就是优点触发我的失控癌。
但问题是,成熟的商业产品是怎么样的?
死板,整齐,模块化。
如果init和add_action真的是性能卡点,甚至init和add_action不是吸能卡点,我会尝试怎么做呢?
我一定会在绝大部分obj里不设置init,一定不会在object里随便的给其他object加指令。我会写两个服务,一个MoveD,一个ActionD。
移动后,该做什么,不由房间,对象,房间内的对象判定,这个会失控。
在MoveD里加Move方法
Move(player,to)
根据房间好玩家的情况,决定会发生什么。
然后所有的指令,怎么能让房间里的道具加,这个失控到了极点。肯定把Action注册在ActionD里,所有的动作都是全局动作,再判断当前用户所在的房间和道具是否支持这个指令。
先不说性能,指令文件分散额定分布再几万个文件中,再经过几次升级,不做统一规划,这不出bug才有鬼。
而这么做,对于性能的优点时,出了执行的指令次数变少,还易于缓存。
对于算法能力不行的普通人来说,缓存是优化性能最大的杀器。
当房间的npc没有发生变化时,他对玩家有没有交互时确定的。也就是说,当房间没有发生变化时,同一个玩家再次进入,直接可以通过缓存判定什么都不发生,跳过。
当玩家状态好道具没有发生变化时,同一个指令的结果可以缓存。
我代码能力不行,但大力出奇迹,我内存换性能总行把,毕竟性能没法突破100%,内存加钱还是能不停叠加上去的。
当然,最好是少加init交互,少action,克制是一种美德。
主要的想法和观点写完了。
其实,这个很可能是我盲人摸象,看不到北侠的代码和运行状态下的YY。
本来这个事我也不爱参合,lpc我也一直不感冒。
但是我刚目睹了一次Mud性能优化。
优化的工作wiz是把某个simul_efun的渲染算法替换了,把文字输出模板换行部分给优化了下算法,使得mud的cpu占用从99%降低到了70%左右。
我自己玩博客压力测试的时候,也做过将内容压缩一下,因为内存读写速度有限,内容大小影响性能。
而北侠的优化还似乎停留在玩家能少发点指令,多停一会上,似乎还没有优化到比较大的核心痛点上。
正好这两天看过点mud代码,瞎琢磨了点,拿出来抛砖一下,就算不对,万一我野路子思路广,砸出点灵感,找到新的优化方向呢。
反正不要钱,多少YY一点。
从交互原理上来说,目前这种对象一交互就绑定临时指令是最容易理解也最仿真的。至于性能,确实不太高,会有频繁绑定的问题。但是如果变成命令都注册到action_d的形式,相当于用户每次发个命令都要对所处环境的物品判断一圈,有谁支持这个命令,顺序如何,因为你不知道两个命令之间是不是周边物品或者npc有了变化。这样看起来,是不是每次变化时绑定一圈,要比每个命令都查一圈,更节省cpu一些?
北侠移动的cpu消耗问题主要原因更多还是来源于细节计算,相当于原来是800x600,现在是1080p甚至4k,更多的细节带来了更多的变化,包括go,look等常用命令,所以这些命令耗时占了大头。同时北侠玩家组成也和其他mud不同,活人玩家居多,从id/ip来看,平均一个活人带2个左右的大米。活人多,意味着对间歇性的卡顿更敏感,小号少,意味着每个id的平均命令更多。
北侠也是从20年前过来的,那时候的服务器硬件性能,200-300人已经卡顿明显。后来随着硬件提升慢慢能支撑到400-500人,然后单核性能几乎就到头了,于是从底层的efun、架构以及lib代码中优化,以及topcmd带来的全体玩家的自觉优化乃至强制限制,慢慢能支撑到现在的700+仍然很少卡顿。要说强制限制带来不便,那确实是有,但别忘了,现在id 700+了,就算每个人都限制到了原来的一半,那总的消耗也和400+时差不多。
id多、移动多,意味着移动中带来的各种反复绑定也就几何增长,但目前来说没有什么好的解决办法,除非完全重写fluffos,支持多核,或者有更好的思路去支持自定义指令。这也不是北侠一家遇到的问题,应该是长时间开站,并且一直有人维护的mud都会遇到的问题。比如这家:https://bbs.mudbest.com/thread-1444-1-1.html,最终也是借鉴了北侠的思路。
优化的工作wiz是把某个simul_efun的渲染算法替换了,把文字输出模板换行部分给优化了下算法,使得mud的cpu占用从99%降低到了70%左右。我对这个很感兴趣,文字渲染消耗了大量计算资源,如果有更好的推荐,欢迎回帖讨论。
icer 发表于 2024-11-17 03:04 AM
从交互原理上来说,目前这种对象一交互就绑定临时指令是最容易理解也最仿真的。至于性能,确实不太高,会有 ...
action_D的想法是这样的,在现有功能不大调的情况下,他并不是性能变高,而是易于缓存。
举个例子,一般情况下,我们可以认为,在房间npc没有发生变化的情况下,同一个指令,响应指令的npc应该是同一个。在道具没有发生变化的情况下,同一个指令,响应指令的道具也应该是同一个。
那么,如果以玩家id-房间id-指令 和 玩家id-道具指令做缓存,理论上说,在玩家在同一个房间发移动指令,或者重复发一个道具指令的话,能有一定的缓存作用。
没有统一的action_d,可能这个工作的工作量会比较大。
关于渲染的部分,具体应该是觉得strwidth要考虑很多边缘情况,把这个用更不严谨但更快的算法替换了。
这个在北侠不太适用。第一北侠大量适用emoji,这个就是最特殊的边缘情况。另外就是北侠没人对机器兜底,这个变化可能非常影响机器。
其实如果仅仅是渲染压力的话,北侠现有架构感觉按我上一个帖子可能就能部分解决。
就是把主服务器也变成一个副本服务器,开个新的mud服务,去掉各种计算/心跳,仅仅做转发,保留渲染功能。
这样主服务器只需要返回原始数据,在转发服务器上实现渲染就好。
关键是现在的副本机制已经经过实践的考验了,应该不会增加太多的不确定性,也符合奥卡姆剃刀原则,并没有引入新的概念和服务,只是新增了一个主服务器的副本而已。
举个例子,一般情况下,我们可以认为,在房间npc没有发生变化的情况下,同一个指令,响应指令的npc应该是同一个。在道具没有发生变化的情况下,同一个指令,响应指令的道具也应该是同一个。恰恰就是这里有问题,如果两个相同命令之间npc或者道具发生变化了,比如道具被另外一个玩家捡走了,那就得重新计算了,实际也就是在做重新绑定的事情,告诉这个房间的所有npc和玩家:我不在这里了,不要缓存或者绑定我的命令了。
副本代理的优化,北侠目前的主站兼做副本代理的架构确实性能上不是最优的,你上个帖子里是更优化的情况,把逻辑和表示层完全分离,但这需要大量工作重写两层的代码,基本相当于把现在的所有代码拆成两部分,工作量太大。。。
一件事一旦上了规模之后,就会变成一个系统工程,就不再是一个纯粹的技术方案问题。
古代运粮队给前线运粮食,据说 90% 的粮食都被运粮队给吃掉了。 TNND 苹果本来是个挺好的公司,结果现在连个键盘都造不好,每次敲键盘都想打人。
嗯,这个世界就是个巨大的草台班子。
页:
[1]