北大侠客行MUD论坛

 找回密码
 注册
搜索
热搜: 新手 wiki 升级
查看: 344|回复: 8

杰哥胡搞搞之mudlib性能卡点分析

[复制链接]
发表于 2024-11-16 23:53:45 | 显示全部楼层 |阅读模式
拿我手头的mudlib做基础分析的。

虽然和北侠的从底层就不同,但是几十年前应该都是一家,毕竟一些远古的一些解密,比如华山的金蛇,少林读石桌这些远古代码都是一模一样的。

当然,我这份mulib已经是20年前的了,北侠经过这么多年应该已经很多地方都优化过了。我就胡搞搞,玩一又用是好时,如果没用,也正常,就当水一帖。

以这个mudlib来说,几百个id挂机,几秒一个任务,连遍历带战斗,一般情况下都不带卡的。所以我觉得是一个很好的参考对象。

以我在北侠的经验来说,容易卡(上top cmd)的点有几个

1.移动/遍历
2.学习
3.一些特殊指令
4.一些特殊的群战场景。

其中3 4 还好理解,1是最特殊的。北侠这样走两步卡几步,走几分钟休息半小时的模式,是我之前从来没接触过的状态,所以也是我这次瞎琢磨的重点。
北大侠客行Mud(pkuxkx.com),最好的中文Mud游戏!
 楼主| 发表于 2024-11-17 00:15:36 | 显示全部楼层
首先让我们找到移动的代码,这个我熟悉,cmd/std/go.c

把拉巴拉一段战斗逃跑的判断,然后
  1.         me->move(dest);
复制代码
恩,好理解,把当前对象移动到dest下。

这里mudos是这样的,所有的东西都是obejct,都是平级。obecjt下可以有子元素,就是道具。这样,一个道具即可以在room里,也可以在玩家身上,也可以在玩家身上的容器里,也可以在房间的容器里。大家都是对象,平级,都能装。

然后找move方法的定义

恩,在feature/move.c里

一堆负重的判断,然后调用了一个 代码
  1. 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里的
  1. #ifdef F_MOVE_OBJECT
  2. void f_move_object() {
  3.   object_t *o1, *o2;

  4.   /* get destination */
  5.   if (sp->type == T_OBJECT) {
  6.     o2 = sp->u.ob;
  7.   } else {
  8.     if (!(o2 = find_object(sp->u.string)) || !object_visible(o2)) {
  9.       error("move_object failed: could not find destination\n");
  10.     }
  11.   }

  12.   if ((o1 = current_object)->flags & O_DESTRUCTED) {
  13.     error("move_object(): can't move a destructed object\n");
  14.   }

  15.   move_object(o1, o2);
  16.   pop_stack();
  17. }
复制代码
然后move_object方法是src/internal/base/simulate.cc里
  1. void move_object(object_t *item, object_t *dest) {
  2.   object_t **pp, *ob;

  3.   save_command_giver(command_giver);

  4.   /* Recursive moves are not allowed. */
  5.   for (ob = dest; ob; ob = ob->super) {
  6.     if (ob == item) {
  7.       error("Can't move object inside itself.\n");
  8.     }
  9.   }
  10. #ifndef NO_SHADOWS
  11.   if (item->shadowing) {
  12.     error("Can't move an object that is shadowing.\n");
  13.   }
  14. #endif

  15.   if (!CONFIG_INT(__RC_NO_RESETS__) && CONFIG_INT(__RC_LAZY_RESETS__)) {
  16.     try_reset(dest);
  17.   }
  18. #ifndef NO_LIGHT
  19.   add_light(dest, item->total_light);
  20. #endif
  21.   if (item->super) {
  22.     int okay = 0;

  23.     remove_sent(item->super, item);
  24.     remove_sent(item, item->super);
  25. #ifndef NO_LIGHT
  26.     add_light(item->super, -item->total_light);
  27. #endif
  28.     for (pp = &item->super->contains; *pp;) {
  29.       if (*pp != item) {
  30.         remove_sent(item, *pp);
  31.         remove_sent(*pp, item);
  32.         pp = &(*pp)->next_inv;
  33.         continue;
  34.       }
  35.       /*
  36.        * unlink object from original inventory list
  37.        */
  38.       *pp = item->next_inv;
  39.       okay = 1;
  40.     }
  41. #ifdef DEBUG
  42.     if (!okay) {
  43.       fatal("Failed to find object /%s in super list of /%s.\n", item->obname, item->super->obname);
  44.     }
  45. #endif
  46.   }
  47.   /*
  48.    * link object into target's inventory list
  49.    */
  50.   item->next_inv = dest->contains;
  51.   dest->contains = item;
  52.   item->super = dest;

  53.   setup_new_commands(dest, item);
  54.   restore_command_giver();
  55. }
复制代码

恩,明显道具是个链表,里面有个不起眼的 setup_new_commands
让我再找一下,在src/packages/core/add_action里(话说从内部包引用efun包,这合理么?):
  1. void setup_new_commands(object_t *dest, object_t *item) {
  2.   object_t *next_ob, *ob;

  3.   /*
  4.    * Setup the new commands. The order is very important, as commands in
  5.    * the room should override commands defined by the room. Beware that
  6.    * init() in the room may have moved 'item' !
  7.    *
  8.    * The call of init() should really be done by the object itself (except in
  9.    * the -o mode). It might be too slow, though :-(
  10.    */
  11.   if (item->flags & O_ENABLE_COMMANDS) {
  12.     save_command_giver(item);
  13.     (void)apply(APPLY_INIT, dest, 0, ORIGIN_DRIVER);
  14.     restore_command_giver();
  15.     if (item->super != dest) {
  16.       return;
  17.     }
  18.   }
  19.   /*
  20.    * Run init of the item once for every present user, and for the
  21.    * environment (which can be a user).
  22.    */
  23.   for (ob = dest->contains; ob; ob = next_ob) {
  24.     next_ob = ob->next_inv;
  25.     if (ob == item) {
  26.       continue;
  27.     }
  28.     if (ob->flags & O_DESTRUCTED) {
  29.       error("An object was destructed at call of " APPLY_INIT "()\n");
  30.     }
  31.     if (dest != ob->super) {
  32.       error("An object was moved at call of " APPLY_INIT "()\n");
  33.     }
  34.     if (ob->flags & O_ENABLE_COMMANDS) {
  35.       save_command_giver(ob);
  36.       (void)apply(APPLY_INIT, item, 0, ORIGIN_DRIVER);
  37.       restore_command_giver();
  38.       if (dest != item->super) {
  39.         return;
  40.       }
  41.     }
  42.     if (item->flags & O_DESTRUCTED) { /* marion */
  43.       error("The object to be moved was destructed at call of " APPLY_INIT "()\n");
  44.     }
  45.     if (ob->flags & O_DESTRUCTED) { /* Alaron */
  46.       error("An object was destructed at call of " APPLY_INIT "()\n");
  47.     }
  48.     if (item->flags & O_ENABLE_COMMANDS) {
  49.       save_command_giver(item);
  50.       (void)apply(APPLY_INIT, ob, 0, ORIGIN_DRIVER);
  51.       restore_command_giver();
  52.       if (dest != item->super) {
  53.         return;
  54.       }
  55.     }
  56.   }
  57.   if (dest->flags & O_DESTRUCTED) { /* marion */
  58.     error("The destination to move to was destructed at call of " APPLY_INIT "()\n");
  59.   }
  60.   if (item->flags & O_DESTRUCTED) { /* Alaron */
  61.     error("The object to be moved was destructed at call of " APPLY_INIT "()\n");
  62.   }
  63.   if (dest->flags & O_ENABLE_COMMANDS) {
  64.     save_command_giver(dest);
  65.     (void)apply(APPLY_INIT, item, 0, ORIGIN_DRIVER);
  66.     restore_command_giver();
  67.   }
  68. }
复制代码
还真是互相交互两边啊,这不是性能炸弹么。
再让我们找个文件看看init()一般是干什么的。
  1. void init()
  2. {
  3.         object me;
  4.         
  5.         ::init();

  6.         if (interactive(me = this_player()))
  7.         {
  8.                 remove_call_out("greeting");
  9.                 call_out("greeting", 1, me);
  10.         }

  11.         me->delete_temp("decide_withdraw");
  12.         me->delete_temp("demolish_room");

  13.         add_action("do_stop", "stop");
  14.         add_action("do_answer", "answer");
  15.         add_action("do_desc", "desc");
  16.         add_action("do_show", "show");
  17.         add_action("do_changename", "changename");
  18.         add_action("do_changeid",   "changeid");
  19.         add_action("do_changetype", "changetype");
  20.         add_action("do_changedesc", "changedesc");
  21.         add_action("do_finish", "finish");
  22.         add_action("do_finish", "ok");
  23.         add_action("do_withdraw", "withdraw");
  24.         add_action("do_withdraw", "chexiao");
  25.         add_action("decide_withdraw", "decide");
  26.         add_action("do_demolish", "demolish");

  27.         // 如果来的是有资格处理表单的巫师就增加处理表单的命令
  28.         if (wizardp(me) && wiz_level(me) >= WIZLEVEL)
  29.         {
  30.                 add_action("do_help", "help");
  31.                 add_action("do_list", "list");
  32.                 add_action("do_type", "type");
  33.                 add_action("do_agree", "agree");
  34.                 add_action("do_reject", "reject");
  35.                 add_action("do_delete", "delete");
  36.         }
  37. }
复制代码

好家伙,是为了绑临时指令啊。
话说每走一步,捡个东西,丢个东西/放个东西到包裹里,都要这样来一圈,好扯啊。
这init()函数,不是正常应该都为空,不正常情况采取使用么。
我深度怀疑,北侠的移动指令这么容易上榜,可能就是和init优点重,或者addaction部分太重可能优点关系。
北大侠客行Mud(pkuxkx.com),最好的中文Mud游戏!
 楼主| 发表于 2024-11-17 00:32:05 | 显示全部楼层
这种对象之间随意初始化指令,互相装配的模式,虽然我很喜欢,但能理解,大概相当于开发板

也就是这种东西


一看就是自由扩展,全能,可扩展性高

就是优点触发我的失控癌。

但问题是,成熟的商业产品是怎么样的?



死板,整齐,模块化。

如果init和add_action真的是性能卡点,甚至init和add_action不是吸能卡点,我会尝试怎么做呢?

我一定会在绝大部分obj里不设置init,一定不会在object里随便的给其他object加指令。我会写两个服务,一个MoveD,一个ActionD。

移动后,该做什么,不由房间,对象,房间内的对象判定,这个会失控。
在MoveD里加Move方法

Move(player,to)
根据房间好玩家的情况,决定会发生什么。

然后所有的指令,怎么能让房间里的道具加,这个失控到了极点。肯定把Action注册在ActionD里,所有的动作都是全局动作,再判断当前用户所在的房间和道具是否支持这个指令。

先不说性能,指令文件分散额定分布再几万个文件中,再经过几次升级,不做统一规划,这不出bug才有鬼。

而这么做,对于性能的优点时,出了执行的指令次数变少,还易于缓存。

对于算法能力不行的普通人来说,缓存是优化性能最大的杀器。

当房间的npc没有发生变化时,他对玩家有没有交互时确定的。也就是说,当房间没有发生变化时,同一个玩家再次进入,直接可以通过缓存判定什么都不发生,跳过。

当玩家状态好道具没有发生变化时,同一个指令的结果可以缓存。

我代码能力不行,但大力出奇迹,我内存换性能总行把,毕竟性能没法突破100%,内存加钱还是能不停叠加上去的。

当然,最好是少加init交互,少action,克制是一种美德。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?注册

x
北大侠客行Mud(pkuxkx.com),最好的中文Mud游戏!
 楼主| 发表于 2024-11-17 00:39:09 | 显示全部楼层
主要的想法和观点写完了。

其实,这个很可能是我盲人摸象,看不到北侠的代码和运行状态下的YY。

本来这个事我也不爱参合,lpc我也一直不感冒。

但是我刚目睹了一次Mud性能优化。

优化的工作wiz是把某个simul_efun的渲染算法替换了,把文字输出模板换行部分给优化了下算法,使得mud的cpu占用从99%降低到了70%左右。

我自己玩博客压力测试的时候,也做过将内容压缩一下,因为内存读写速度有限,内容大小影响性能。

而北侠的优化还似乎停留在玩家能少发点指令,多停一会上,似乎还没有优化到比较大的核心痛点上。

正好这两天看过点mud代码,瞎琢磨了点,拿出来抛砖一下,就算不对,万一我野路子思路广,砸出点灵感,找到新的优化方向呢。

反正不要钱,多少YY一点。
北大侠客行Mud(pkuxkx.com),最好的中文Mud游戏!
发表于 2024-11-17 03:04:46 | 显示全部楼层
从交互原理上来说,目前这种对象一交互就绑定临时指令是最容易理解也最仿真的。至于性能,确实不太高,会有频繁绑定的问题。但是如果变成命令都注册到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,最终也是借鉴了北侠的思路。
  1. 优化的工作wiz是把某个simul_efun的渲染算法替换了,把文字输出模板换行部分给优化了下算法,使得mud的cpu占用从99%降低到了70%左右。
复制代码
我对这个很感兴趣,文字渲染消耗了大量计算资源,如果有更好的推荐,欢迎回帖讨论。
北大侠客行Mud(pkuxkx.com),最好的中文Mud游戏!
 楼主| 发表于 2024-11-17 03:48:19 | 显示全部楼层
icer 发表于 2024-11-17 03:04 AM
从交互原理上来说,目前这种对象一交互就绑定临时指令是最容易理解也最仿真的。至于性能,确实不太高,会有 ...

action_D的想法是这样的,在现有功能不大调的情况下,他并不是性能变高,而是易于缓存。

举个例子,一般情况下,我们可以认为,在房间npc没有发生变化的情况下,同一个指令,响应指令的npc应该是同一个。在道具没有发生变化的情况下,同一个指令,响应指令的道具也应该是同一个。

那么,如果以玩家id-房间id-指令 和 玩家id-道具指令做缓存,理论上说,在玩家在同一个房间发移动指令,或者重复发一个道具指令的话,能有一定的缓存作用。

没有统一的action_d,可能这个工作的工作量会比较大。

关于渲染的部分,具体应该是觉得strwidth要考虑很多边缘情况,把这个用更不严谨但更快的算法替换了。

这个在北侠不太适用。第一北侠大量适用emoji,这个就是最特殊的边缘情况。另外就是北侠没人对机器兜底,这个变化可能非常影响机器。

其实如果仅仅是渲染压力的话,北侠现有架构感觉按我上一个帖子可能就能部分解决。

就是把主服务器也变成一个副本服务器,开个新的mud服务,去掉各种计算/心跳,仅仅做转发,保留渲染功能。

这样主服务器只需要返回原始数据,在转发服务器上实现渲染就好。

关键是现在的副本机制已经经过实践的考验了,应该不会增加太多的不确定性,也符合奥卡姆剃刀原则,并没有引入新的概念和服务,只是新增了一个主服务器的副本而已。
北大侠客行Mud(pkuxkx.com),最好的中文Mud游戏!
发表于 2024-11-17 04:00:48 | 显示全部楼层
  1. 举个例子,一般情况下,我们可以认为,在房间npc没有发生变化的情况下,同一个指令,响应指令的npc应该是同一个。在道具没有发生变化的情况下,同一个指令,响应指令的道具也应该是同一个。
复制代码
恰恰就是这里有问题,如果两个相同命令之间npc或者道具发生变化了,比如道具被另外一个玩家捡走了,那就得重新计算了,实际也就是在做重新绑定的事情,告诉这个房间的所有npc和玩家:我不在这里了,不要缓存或者绑定我的命令了。

副本代理的优化,北侠目前的主站兼做副本代理的架构确实性能上不是最优的,你上个帖子里是更优化的情况,把逻辑和表示层完全分离,但这需要大量工作重写两层的代码,基本相当于把现在的所有代码拆成两部分,工作量太大。。。

北大侠客行Mud(pkuxkx.com),最好的中文Mud游戏!
发表于 2024-11-17 04:10:59 | 显示全部楼层
一件事一旦上了规模之后,就会变成一个系统工程,就不再是一个纯粹的技术方案问题。

古代运粮队给前线运粮食,据说 90% 的粮食都被运粮队给吃掉了。
北大侠客行Mud(pkuxkx.com),最好的中文Mud游戏!
发表于 2024-11-17 04:15:54 | 显示全部楼层
TNND 苹果本来是个挺好的公司,结果现在连个键盘都造不好,每次敲键盘都想打人。


嗯,这个世界就是个巨大的草台班子。
北大侠客行Mud(pkuxkx.com),最好的中文Mud游戏!
您需要登录后才可以回帖 登录 | 注册

本版积分规则

Archiver|手机版|小黑屋|北大侠客行MUD ( 京ICP备16065414号-1 )

GMT+8, 2024-12-27 12:10 AM , Processed in 0.011980 second(s), 16 queries .

Powered by Discuz! X3.4

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表