八哥之神后传【12】识界之主

餐厅

圣仙山:都这样了,你还不承认她是你女朋友?

圣小开:没办法,客观情况不允许。学校,不是谈恋爱的地方,而且她还小。

圣仙山:你其他同学不是光明正大地谈?而且这年纪放在这边不小。

圣小开:不如把她接到这边来?

圣仙山:不行,她已经死了。

圣小开:稣不也死了??为啥她不能来?

圣仙山:是你没让她来。

圣小开:稣不让??没有吧!那稣这就让她来。

圣仙山:太迟了。

圣小开:为啥?

圣仙山:你还没觉醒,没这个权限。

圣小开:那你刚才说,是稣没让她来?

圣仙山:你们一起 jump,不就一起来了?

圣小开:重来一次行吗?

圣仙山:没办法,客观情况不允许。这里,不是想来就能来,而且她还小……

圣小开:咳咳。好耳熟的话,像是哪个渣男说的?

圣仙山:哈哈哈……别装了!识界之主!

圣小开:稣是识界之主??别吓稣!

圣仙山:没关系。等你觉醒自然就承认了。

圣小开:那稣要如何觉醒?

圣仙山:不知道。

圣小开:你不是能预知未来吗?

圣仙山:是能,但你不是未来,而是过去。

圣小开:什么?你是说稣能影响过去?

圣仙山:没错!甦能预知未来,而你能影响过去。

圣小开:好无用的神通……影响过去能干吗?

圣仙山:怎么会没用?全宇宙只有你能向过去转世,当你转世到宇宙之初,你知道这意味着什么吗?

圣小开:能创造宇宙?

圣仙山:不知道,甦随口说说。

圣小开:懂了,会重来一遍,变成向未来转世?

圣仙山:不错,悟性很高。向未来转世的,遇到时间的尽头,又会返回。

圣小开:所以,其实咱们是同一个意识在不同时间线?这就是识界的真相?

十界

圣仙山:必须阻止识界通过圣小开进入十界。

餐厅

圣仙山:甦不是十界圣仙山。你也不是真正的圣小开。

圣小开:稣只是识界模拟的圣小开,明白。

圣仙山:对!每次你意外死亡,都会使识界重置,但是这次不太一样,你遇到了甦这个变数。

圣小开:你是十界圣仙山影响识界的结果?

圣仙山:莲雅。现在识界之主和你共生,等你转世到宇宙之初,识界就完整了,它将无所不能。

圣小开:所以你是来彻底消灭稣的?!

圣仙山:咱们互为未来过去,甦不能杀死过去的自己,也不愿杀死未来的自己。

圣小开:那你打算怎么办?稣还要和学妹谈恋爱呢,时间有限,给个痛快。

圣仙山:咱们在这里交换一下,甦替你回去未来,此后识界只能往后观测甦。

圣小开:你去泡稣的学妹?不太好吧!

圣仙山:你是不是拿着一封信,要去找胡小玉?

圣小开:是啊,这信就给你了,你去找她吧!

圣仙山:按照历史的发展,她本来应该是甦的正房。

圣小开:噗……那刚好,就该你去。虽然古代饱读诗书的女人,温柔又贤惠,还能三妻四妾,想想就……但稣要回去找学妹。对!稣要回去找学妹。

圣仙山:你真的要为了学妹,放弃整个宇宙?

圣小开:怎么还扯上整个宇宙?早恋有这么大危害?

圣仙山:没办法了,客观情况不允许……只能出此下策。

圣小开:别乱来啊!稣不换!

圣仙山:甦已经把你学妹抹除了。哈!

学校操场,慢跑

柯金钏:开哥,你为什么经常和你同学来跑步,都快高考的人了?

圣小开:“我”是少数高中还不靠努力只靠智力的人。

柯金钏:但是再努力一把,不是更好?

圣小开:好吧,其实我从小学四年级就慢性鼻炎,这病影响睡眠,而且吃药也很难治好。所以打算通过跑步把它治好,免得高考时,缺氧宕机。

柯金钏:你同学也是?

圣小开:不。他是要考体校……

柯金钏:原来如此。难怪他身材那么好。

圣小开:但他头壳有点问题。

柯金钏:哈哈哈,你怎么背后说人家坏话呢。

圣小开:同学都这么说,我也不知道为啥,可能我也是头壳有点问题吧?所以能玩在一起。

柯金钏:哦,呵呵。但是,你比他帅。

圣小开:em?

柯金钏:咋了?说你帅这么惊讶干吗?

圣小开:以前没人这么说过我。我觉得自己应该被称作酷,甚至冷酷。

柯金钏掩嘴而笑:真是,头壳有点问题!不过你有时候看上去是挺高冷的。

圣小开:我愿承认这是内向加上脸瘫,表情呈现过度缓慢。

柯金钏:啊哈。不跑了,再走一圈。

圣小开:好啊。我发现和你跑步治疗鼻炎的效果更好。

柯金钏:为啥呀?

圣小开:你的气质特别阳光,而且身上有一股通鼻醒脑的香气。

柯金钏:香气,没有吧?

圣小开:你自己习惯了,闻不到。

柯金钏:别人没有吗?

圣小开:没有呀。你看前面那个头壳坏掉的,身上只有汗味。

柯金钏:你没女同学吗?

圣小开:有的,我们班上女生比男生多。不过她们都无色无味。

柯金钏:哼!有你这样形容的……

餐厅

圣小开:抹完了??

圣仙山:莲雅。

圣小开:就这?稣怎么还记得她?柯金钏,属火兔,天蝎座。

圣仙山:除了你没人记得。回去试试就知道。

学校操场,食堂

圣小开:每次叫你的名字,就想笑。要不叫你窜窜?

柯金钏:是卷舌的,串串。

圣小开:哦,我不会卷。不行,这也很好笑。会想到串串香……em,真香。

柯金钏:你还是叫我的英文名吧?JC。

圣小开:是那个很像 Jesus 的 Jessie?

柯金钏:是啊,也有人叫我 Jessica,因为和姓一起叫,Jessie Ke,就很像 Jessica。

圣小开:还是 Jessie 好点,Jessica Ke,不是 JC 卡壳吗?

柯金钏:别人的名字都能给你取笑半天?

圣小开:没有的事,不好意思,Jessie,我严肃。

柯金钏:对嘛,不要取消别人的名字,哼。不行,我要报仇,以后叫你开叔。

圣小开:啊,我怎么成叔了?

柯金钏:开叔!论辈分,你本来就是我的学叔。

喜欢看你紧紧皱眉 教我打小乌龟
你的表情大过于朋友的暧昧
寂寞的称谓 甜蜜的责备
有独一无二专属的特别

喜欢看你紧紧皱眉 教我打小乌龟
我的心情就像和情人在斗嘴
奇怪的直觉 错误的定位
对你 哎呀呀呀 我有点胆怯

八哥之神后传【11】

破庙

吓醒。

捕快甲:这里有个人,穿着奇装异服,肯定不是本地人!

捕快乙:倒是眉清目秀的,难道就是最近频繁作案的采花大盗?

捕快甲乙:抓起来!

圣小开:什么情况?稣是官二代哦!你们敢抓?

捕快甲:老子还是官一代呢!老实点!

捕快乙:嘿嘿嘿,打一顿就老实了。

圣小开:屈打成招是吧?你们再这样,稣要念动咒语了哦!

捕快乙:疯癫?

捕快甲:押回去给圣大人研究!哈哈哈。

圣小开:圣?大人?稣也姓圣!

捕快甲:笑话,圣大人全家我都认识,就没你这号。闭嘴吧你!

圣小开:啊……稣要投诉你们!

牢房

狱卒:姓名?籍贯?

圣小开:圣小开,金门人。

狱卒:身上有多少钱?

圣小开:没有你们这里的钱,人民币要么?只有四块钱。

狱卒:这是冥币?没钱就住最次的。

圣小开:请问大佬,稣是替死,还是充军?

狱卒:看情况,看表现,但由不得你。带进去!

捕快乙:这人脸瘫,看起来不怕死,试试培养成死士或细作?

狱卒:这个姓……应是假名,兴许就是敌国的细作。

捕快乙:先上点刑?

捕快丙:狱吏大人有令,将穿越者带去见他。

狱卒:穿越者?系奇装异服那厮?

捕快丙:与狱吏大人同姓者。带出来!

捕快甲:就是这位,整桩好好。

圣小开:敢问狱吏大人名号为何?

捕快丙:圣仙山大人。

圣小开:稣的先祖??

餐厅

圣仙山:开!终于见面了。甦准备了你最喜欢的牛排,六成熟,不加酱,快来吃吧。

圣小开:稣都没吃过牛排,你怎么知道稣喜欢吃?

圣仙山:这牛是甦培育的,保证你吃了就喜欢。

圣小开:太奢侈了,虽然稣经常和同学吹牛说牛排是高贵的食物,但稣还没钱吃。没想到居然在古代吃到。

圣仙山:甦有钱,随便吃。你以后也会有钱,能天天吃。

圣小开:嗯!真好吃。

圣仙山:试试这些,凤尾螺、青龙、鲟鳇,都是清蒸的,还有鱼羊汤。

圣小开:狱吏居然这么有钱?当官真好!

圣仙山:别的狱吏可没甦这么有钱。

圣小开:也对,你是神。啊!可太好吃了。稣真的还想再吃五百年。

圣仙山:那你为何跳楼?

圣小开:单纯好奇稣死后会是什么样的。

圣仙山:果然是甦的传人,可真舍得。

圣小开:如假包换。

圣仙山:但甦有个坏消息要告诉你,你女朋友也跳楼殉情了。

圣小开:稣有女朋友??

圣仙山:你学妹。

圣小开:em……稣是有个初中部的学妹,关系还行,但没确定过关系,学校不让早恋!

学校操场,跑步

胆公影究:开啊,你快全班第二了。不过和我这个第一,差距还很大。

圣小开:真喘!

胆公影究:嘘!刚刚那个学妹回头看了你一眼。

美梦里有怎样气候
你终于回过头看我

圣小开:快趴了……先扶着稣,看啥学妹。

胆公影究:脸上有一股英气,还是高个子,你喜欢的类型。

圣小开:这个背影是不错呀,快和稣一样高了!

胆公影究:继续跑,追她。

圣小开:你追吧,你比稣高,和她更搭。

胆公影究:果然是兄弟!

圣小开:笑死,稣在后面看你被打脸。

胆公影究:干!

学校操场,早操

有一天做早操,不同年级换位置。那天只看到背影的学妹居然排在稣的右边那排的第一个,这时候看到她英气逼人的脸了,结合身高和胆公影究的描述,稣可以确定就是她!但是稣不确定她记不记得稣,所以只是每天看着她的侧脸和背影。

慢慢地也知道,她是体育委员,性格泼辣,脾气火爆,经常对她班上的男生拳打脚踢,不是“打是情骂是爱”的那种,是当沙包踹的那种。

而且,后来知道她叫柯金钏,才明白那天她为何回头看稣!她的名字“金钏”的普通话和“真喘”的闽南语的发音特别像。嗯,是稣自作多情了,还以为她回头看稣是觉得稣闪耀着神性的光辉!

想明白的稣视线向着她,忍不住傻笑起来。没想到,她又回头瞟一眼稣,友好地问候了一句:“神经病?”

稣吓得只能承认,小声回答:“是啊。真喘!呵呵呵……”

过了几天,傍晚运动时间,稣特意跑操场满地找她,终于找到。她坐在沙堆边休息,稣过去傻笑她,“你也在喘啊?”

她一脸正经地说:“有点累,好像气血不足。”

稣顿时笑不出来,关心地问:“是不是低血糖?”

她说:“好像生病了!”

看着脸确实比平时白,穷稣只能安慰她:“长这么高,血是不容易送到脸上去。”

她想笑,但只是尴尬了一下。看来是真生病了。

稣只好约她一起去校门口喝葡萄糖。em,那时候穷,好多店真的就是把葡萄糖注射液当饮料卖。心想如果情况有好转,再一起吃个第一汤包也不错。

由于高中生不能和初中生早恋,所以此处情节省略一万行。

总之,在不影响学习的前提下,稣还翘课和她去学校后面爬大轮山,拜梵天寺。她是真去拜拜,稣是去看一页书在不在。

世事如棋,乾坤莫测,笑尽英雄啊!

逛三秀街,喝同样的果汁,吃同样的拉面。她冷了,还会直接叫稣脱衣服给她穿。有些瞬间,稣会心疼,原来她身体没看上去的好呀!

就算是轻轻的微风
也在试探思念浓薄
你忘的伞还依我的窗
望着窗外那悠悠春光
我心中延续和你的情感
有一种暧昧的美满
忘记了思念的负担

稣偶尔会和她说哪个学姐长得真漂亮,她也只是说自己还小,审美观不太一样,并没有其它情绪。所以,稣认为自己和学妹完全没有早恋!一点都不暧昧。

我自私延续心中的期盼
有一种暧昧的晴朗
站在这城市某一端
寂寞和爱
像浮云 聚又散

她说自己脾气很差,但是稣一点都不信——女人都是骗子。

spdlog

1. 用于啥需求?

打日志。spdlog 是一个高性能、易用的 C++ 日志库。

2. 何时使用 spdlog?

其格式化风格同 std::format/std::print,如果这符合您的习惯可以考虑。稣一般会在复杂场景下使用 Boost.Log,测试程序或简单的程序里使用 spdlog。

举例啥叫复杂场景:产品里有多个可执行程序(Executable),譬如说 A 和 B,它们共同使用多个动态链接库(Dynamic-Link Library),譬如说 X 和 Y。注意,这里说的“使用”,可能是静态加载,也可能是动态加载(比如动态链接库是插件)。当 X/Y 在 A 进程里时,它们的打印风格、设置都应该受 A 控制,而在 B 进程时,则受 B 控制。不管可执行程序和动态链接库有多少个,每个进程都应该只有一个 Logger。这个需求 Boost.Log 能轻松实现,而 spdlog 可能无法实现或实现并不轻松。2024 年在雪蛤油时,有个熟悉 spdlog 的同学和稣打赌,结果他用 spdlog 没能实现。也就是说即使 spdlog 能实现,那也不轻松。

3. 具体应用

首先注意到 Logger 的打印接口有两大类,一类是函数,比如 spdlog::info,只要你用了,它就被编译到程序里;另一类是宏,比如说 SPDLOG_INFO,它是否会被编译到程序里,受 SPDLOG_ACTIVE_LEVEL 控制。具体看以下注释:

1
2
3
4
5
6
7
8
9
10
11
12
//
// enable/disable log calls at compile time according to global level.
//
// define SPDLOG_ACTIVE_LEVEL to one of those (before including spdlog.h):
// SPDLOG_LEVEL_TRACE,
// SPDLOG_LEVEL_DEBUG,
// SPDLOG_LEVEL_INFO,
// SPDLOG_LEVEL_WARN,
// SPDLOG_LEVEL_ERROR,
// SPDLOG_LEVEL_CRITICAL,
// SPDLOG_LEVEL_OFF
//

spdlog 支持 6 种日志级别:

级别 说明 适用场景
trace 最详细的调试信息 开发调试
debug 调试信息 开发环境
info 一般信息 运行状态
warn 警告 潜在问题
error 错误(但程序可继续运行) 异常情况
critical 严重错误(可能崩溃) 致命问题

其中的 debug 级别,稣总觉得不应该存在,根据情况归到 trace 或 info 即可。

spdlog::set_level 设置的是运行时的显示级别,比如说:

1
2
3
spdlog::set_level(spdlog::level::debug); // 只显示 >= debug 的日志
spdlog::trace("This won't show (level too low)"); // 不会输出
spdlog::debug("Debug info"); // 会输出

但通常我们会使用宏来打印日志,并通过设定 SPDLOG_ACTIVE_LEVEL 来去掉低级别日志,以提高运行效率,或防止被“轻松逆向”。一般来说,Debug 版本可以设定 #define SPDLOG_ACTIVE_LEVEL SPDLOG_LEVEL_TRACE,而 Release 版本可以 #define SPDLOG_ACTIVE_LEVEL SPDLOG_LEVEL_INFO#define SPDLOG_ACTIVE_LEVEL SPDLOG_LEVEL_WARN

既然我们使用宏来打印日志,那么我们就能注意到:宏也有两类,一类形如 SPDLOG_LOGGER_INFO,需要一个 logger 参数,另一类形如 SPDLOG_INFO,不需要传入 logger 参数。从以下代码可知,后者是默认 Logger 的打印宏。

1
2
3
4
5
6
7
8
#if SPDLOG_ACTIVE_LEVEL <= SPDLOG_LEVEL_INFO
#define SPDLOG_LOGGER_INFO(logger, ...) \
SPDLOG_LOGGER_CALL(logger, spdlog::level::info, __VA_ARGS__)
#define SPDLOG_INFO(...) SPDLOG_LOGGER_INFO(spdlog::default_logger_raw(), __VA_ARGS__)
#else
#define SPDLOG_LOGGER_INFO(logger, ...) (void)0
#define SPDLOG_INFO(...) (void)0
#endif

如果我们不想使用默认 Logger,就得自己创建 Logger,并使用第一类形如 SPDLOG_LOGGER_INFO 的宏,但这个宏有点长,还是把自己创建的 Logger 设置为默认,再使用第二类形如 SPDLOG_INFO 的宏方便点。

1
2
3
4
5
6
7
8
9
inline bool InitializeLogger() {
auto logger = std::make_shared<spdlog::logger>(
"", std::make_shared<spdlog::sinks::wincolor_stderr_sink_mt>());
if (!logger) {
return false;
}
spdlog::set_default_logger(std::move(logger));
return true;
}

至于输出格式,稣的测试程序一般如此设定:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int main(int argc, char* argv[]) {
nw::args _(argc, argv);
nw::nowide_filesystem();
::SetConsoleOutputCP(CP_UTF8);

if (!InitializeLogger()) {
nw::cerr << "Failed to initialize logger!" << std::endl;
return EXIT_FAILURE;
}
#if _DEBUG
spdlog::set_level(spdlog::level::trace);
spdlog::set_pattern("%Y-%m-%dT%H:%M:%S.%e |%P~%5t <%^%L%$> %s#%#: %v");
#else
spdlog::set_level(spdlog::level::info);
spdlog::set_pattern("%Y-%m-%dT%H:%M:%S.%e |%P~%5t <%^%L%$> %v");
#endif
SPDLOG_TRACE("Starting up...");
// Other codes...
}

注意:默认情况下,spdlog 是同步的!如果日志量很大,应该使用异步日志(减少主线程阻塞)。例如:

1
2
3
4
5
6
7
8
9
10
11
inline bool InitializeLogger(std::string_view log_directory) {
spdlog::set_automatic_registration(false);
auto logger =
spdlog::create_async<spdlog::sinks::wincolor_stderr_sink_mt>("");
if (!logger) {
return false;
}
spdlog::register_or_replace(logger);
spdlog::set_default_logger(std::move(logger));
return true;
}

除了 spdlog::create_async,还有个 spdlog::create_async_nb,它们的差别在于堆积的日志溢出时的处理策略不同,具体查看:

1
2
3
4
5
6
7
// Async overflow policy - block by default.
enum class async_overflow_policy {
block, // Block until message can be enqueued
overrun_oldest, // Discard oldest message in the queue if full when trying to
// add new item.
discard_new // Discard new message if the queue is full when trying to add new item.
};

其它的……问掐鸡(LLM)吧!

通过 iPXE 安装 Debian

起源

想安装 Debian 12,但没 U 盘。

思考、观测

  • Debian 能通过网络安装,参考:Installing Debian using network booting

  • 但 PXE 太麻烦了,放弃。

  • 观测目标机器,发现其 EFI 有网络启动功能,还能按 Ctrl+B 进入 iPXE。

解决

  1. 开启网络启动,并关闭 Secure Boot。

  2. 看到 PXE 启动画面时,按 Ctrl+B,进入 iPXE。

  3. 输入 dhcp,使目标机器得到 IP 地址,成功即可下一步;但如果局域网内没有 DHCP Server,则需要手动配置:

1
2
3
4
5
6
7
set net0/ip 192.168.1.77
set net0/netmask 255.255.255.0
set net0/gateway 192.168.1.254
set dns 8.8.8.8

ifopen net0
ifstat net0

以上最后一条命令 ifstat net0 输入完后,应该能看到输出里有 open 的字样。

  1. 启动内核

以 Debian 12 为例,先设置网址前缀:

1
set URL http://mirrors.ustc.edu.cn/debian/dists/bookworm/main/installer-amd64/current/images/netboot/debian-installer/amd64/

注意,iPXE 里只支持 http。网址里的 mirrors.ustc.edu.cn 是中科大的镜像域名,在厦门、上海、长沙访问都很快。

1
2
3
kernel ${URL}linux console=ttyS1,115200n8 initrd=initrd.gz
initrd ${URL}initrd.gz
boot
  1. 加速安装

请参考《快速安装 Debian》,其中“按 Ctrl+Alt+F5 回到安装界面”这步需要改为“按 Ctrl+Alt+F1 回到安装界面”,因为通过 iPXE 启动的 Debian 安装程序是 TUI,运行于第一个控制台。

如何让开始按钮的右键菜单带加速键?

Win 键被禁用后发现 Windows 11 24H2 的一个小伎俩

起源

最近在开发基于 QEMU 的 Windows 虚拟机的 GPU 加速驱动。由于不想频繁按 Ctrl+Alt+G 退出虚拟机窗口的捕获状态,就不用鼠标设备,而是使用触控屏设备。即:
-device qemu-xhci -device usb-tablet
但这么一来,虚拟机就接收不到 Win 键了……于是之前迅速打开设备管理器的“Win+X, M”也跟着无效。

解决

“Win+X”的替代自然就是“鼠标右击开始按钮”,然而出来的菜单居然没有加速键?

No accelerator keys

用“Win+X”呼出的菜单应该是下面这样的:

With accelerator keys

于是猜测,微软一定会设计另一种机制,比如“按下某种组合键”,来提供有加速键的 Win+X 菜单。测试按 Ctrl 和 Shift,发现有时候第一次按无效,后面才会有效,索性再测试 Ctrl 和 Shift 同时按,则每次都有效。

所以,解决方式是:同时按下 Ctrl 和 Shift,再鼠标右击开始按钮。

RegameDesk 开发笔记【9】hello_imgui

本文的微信公众号链接

需求

客户端(主控)渲染窗口需要故障排除功能,比如说显示 FPS、丢帧、延迟等信息,以方便调试、测试。一个较为常见且靠谱的作法是基于 Dear ImGui 来实现。

学习

高手直接看 Dear ImGui 也不是不行。但如果是初学者,从 Hello ImGui 开始,比较不容易吓退。

Hello ImGui 基于 Dear ImGui,所以拿前者练好,再上生产环境用后者,是一个不错的路线。

实践

故障排除功能通常是一个彩蛋(隐藏功能),需要某种触发机制唤出。常见采用快捷键,但这可能和远程端系统里某个快捷键冲突。所以也可以考虑使用鼠标操作唤出,比如在顶部操作栏(显示状态下)连击 3 次右键。

Remote Desktop

RegameDesk 开发笔记【4】C++

C++ 20

根据 jetbrains 的统计,2023 年时,C++ 20 的使用率是 29%,仅次于 C++ 17 的 43%。在嵌入式领域和游戏开发领域,C++ 20 的使用率更高,分别为 37% 和 39%。

众所周知,C++ 版本越高,就越强大,并且解决以前版本的一些问题。讲道理的话,现在 2025 年就应该用 C++ 20。不用的人,大致理由都是成本问题。其实,大可以把工程的版本设置为 C++ 20,然后按照已经学会的版本去用。这种情况下,遇到问题,通常就是遇到旧版本的不足或过时的部分。按照稣的经验,目前主流大语言模型对 C++ 20 的支持是不错的,尤其是语法和标准库使用上。

参考:

CppCoreGuidelines

这是由 C++ 之父 Bjarne Stroustrup 领导的行业巨佬们写的宝典,对于写好工程有巨大帮助。有些初学者,买了工具书学完语法,就开始写代码,然后就会遇到一个典型的问题:代码写多了就乱,乱到一定程度自己都不想继续。即使是更好点的情况,靠毅力把代码写到上线,后面却发现乱得自己都不想维护。CppCoreGuidelines 就是用来解决这类问题的。

入门 CppCoreGuidelines 的第一步是:把 GSL 用起来。

参考:

第二步是:记住不要写 STUPID 的代码。仔细看看 CppCoreGuidelines,里面有具体规则。

  • Singleton - 单例(I.3: 避免使用单例)

  • Tight Coupling - 紧密耦合(C: 类和类层次)

  • Untestability - 不可测试(P.12: 适当采用支持工具、P.13: 适当采用支持程序库)

  • Premature Optimization - 过早优化(Per.1: 请勿进行无理由的优化、Per.2: 请勿进行不成熟的优化、Per.3: 请勿对非性能关键的代码进行优化)

  • Indescriptive Naming - 非描述性命名(P.3: 表达你的设计意图)

  • Duplication - 重复代码(ES.3: 避免重复(DRY),避免冗余代码)

RegameDesk 开发笔记【3】Boost 1.88.0

前言

今天是 2025-02-20,Boost 1.88.0 还没发布,但目前的版本在实现 RegameDesk 时遇到一些问题,导致使用了不优雅的解决方案,按照稣和作者们的沟通,下个版本都能解决,所以稣认为 1.88.0 才是适合远程桌面​的 Boost 版本。

Why Boost?

在开发过程中,选择一种基础库或库的集合(注:实际上 Boost 是一个集合)几乎是必然的。如果您的产品使用了 Qt,那么许多基础功能很可能会直接借助 Qt 来实现。有些项目可能会选择 Google 的代码作为基础,从而引入 Abseil。甚至还有一些团队会单独提取 Chromium 的 base 模块来使用。

稣曾遇到过有人推荐使用 Folly,而 Folly 本身也依赖于 Boost。如果您能够接受 Folly,那么当团队不再使用它时,接受 Boost 也应该不是问题。

以上例子其实暗示了一个事实:C++ 标准库的功能相对有限,难以满足实际项目的需求。为了解决这一问题,开发团队通常会选择引入第三方库。在选择第三方库时,主要有两种思路:一种是引入一个功能强大且尽可能全面的大型库(集合),再配合少数其它必要的小型库,以最大化地保持代码风格的一致性;另一种则是引入多个专门解决特定需求的小型库,这种方式虽然可以让每个库都“小而美”,但可能会导致代码风格的不一致。选择哪种方式其实很简单——选择您最熟悉的那一种。

注意!上一段说的“小而美”,从整体上看,可能是假象,尤其当项目很大、成员较多时。当然您可以提出 Chromium 来反驳,不过您需要一定实力和精力去驾驭,所以稣只是说“可能”!

对稣来说,Boost 是一个显而易见的选择。它经过多年的发展,积累了丰富的文档和社区讨论,学习和使用难度都不大。过去有人抱怨 Boost 的编译时间过长,现在早已不存在。而且在如今普遍配备 64GB 内存、高速 SSD 的开发环境中,加载大量头文件已经不再是问题,怪罪 Boost 使工程加载变慢的人也能放心了。

片面地安利 Boost

使用 Boost 相比自己实现而言,有以下好处​:

  1. ​开发更快;

  2. 运行更快;

  3. 运行更稳​;

  4. 代码可读性更高。

后面会举真实例子说明为啥是和自己实现比!先说事实,以上三条总有 1~2 条符合,甚至对于某些团队——可能是中 3~4 条。

首先,开发更快,可能是最有争议的,很多人会反驳说——光学它就要很多时间​。这要是放在以前,稣可能想不出啥好招给这部分人洗脑,现在有大语言模型,各种辅助手段,如果还这么说,完全也不用去反驳,没必要了。

另外,Boost 有部分库确实性能不行,并且官方文档也是明说的,这种情况是求稳定,比如 Boost.Format​。

剩下的,Boost 通常有十分优秀的性能,除非极端的具体领域优化,不然大多数人能把性能写赢 Boost 的概率几乎是 1%(多给 1 分,怕您是真大佬!)。举个例子,Boost.JSON 的性能是高于 RapidJSON 的。RapidJSON 这名字起得好(快),不一定就是真的好(快)。

等等,第 4 条是怎么回事?有些人会说他自己手撸的更好理解,如何反驳?嗯,很可能只是对于作者本人才更好理解,别人看都不想看(笑)。作为团队合作的产物,更多共同点才是好的。比如说,整个团队都熟悉 STL,那么基于 STL 的接口/实现就不会差,而把它等价地改为基于某个第三方库,如果没有强力的理由,通常会被(不用脑地)认为不好。自己写的,对别人来说,何尝不是一种“第三方”?显然,大家普遍认可的“第三方”才可能是更好沟通的,更好达成共识的。(注:这里的“大家”是普遍意义上的大家,不是说某个团队里的少数几名成员,毕竟有的团队就两名写代码的,并不存在“多数”和“少数”!)

例子

大家最喜欢的案例分析来了……稣正好遇到这样一个活生生的例子:实现一个 IPC​ 用于 Service 和工作进程之间通信。

这个故事发生在雪蛤油打工时。一开始,稣就打算使用 Boost 封装的 Pipe,因为以前干过类似的活,​有成功案例。但不幸的是,稣是第二个加入团队的,原来已经有人弄过一个实现,纯手撸的,基于共享内存和事件通知​。关键是,大佬说这实现在他前公司用了 2 年很稳定​。稣想了一下,虽然自己的实现代码量不到它的 1/5,但只接受了几周的考验​。还是别冒险,于是那份手撸版本上​了生产。

然后有一个周末,没回家,随手拿 example 改改对比性能,使用 Boost 的版本速度居然是那手撸实现的 4 倍!在应用层,共享内存是 Windows 上最快的 IPC 机制没错,但它需要其它内核对象的辅助,最终完成时速度就拖慢了。Boost 使用 Pipe 作为 IPC 机制,而没用共享内存和事件通知复合实现,很可能作者是知道这门道的​。

对了,每次发送多少字节对性能是有影响的,如果您打算测试,需要对不同大小的信息进行测试,不能用固定的大小,以免得出不全面的​结论。

参考

来自 CppCoreGuidelines 的一节:

  • ES.1: 优先采用标准库而不是其他的库或者“手工自制代码”

总结

本文,乃至本系列文章,重点在于心法,并没打算详细介绍项目里用了 Boost 具体哪些类库。因为很简单的道理:您要的功能,如果 Boost 有,考虑用它即可。

看到这里,如果您还记得稣写的基于 Boost 的 IPC 代码,它又多接受了三个月的考验——极其稳定,重点是它用起来简单多了。

声音共享 SoundShare

故事 The Story

SoundShare 是 RegameDesk 开发过程中的副产品。

SoundShare is a by-product of the development process of RegameDesk.

在金山云开发鎏光时,领导给了两个设定:仅用于边缘网络、示范性实现。所以没有使用当时很火的 WebRTC,直接使用 WebSocket 传输音视频裸流。可想而知,在很多场景下,其音频的效果是不好的。

During the development of Liuguang at Kingsoft Cloud, the leadership provided two directives: it should be designed only for edge networks and serve as a demonstrative implementation. Consequently, instead of utilizing the then-popular WebRTC, I directly employed WebSocket to transmit raw audio and video streams. As one might expect, the audio quality was suboptimal in many scenarios.

后来,在雪蛤油开发远程桌面产品时,稣便发现使用 RTC SDK 能有效地改善音频的效果。但是这个 RTC SDK 是公司其它团队开发的,而且开发者人数众多,想必是个大型库。稣的重心放在除了传输协议之外的其余部分,对 WebRTC 可以说一知半解。

Later, while developing a remote desktop product at Xuehayou, I discovered that employing an RTC SDK could significantly enhance audio quality. However, this RTC SDK was developed by another team within the company, and given the large number of developers involved, it was presumably a substantial library. My focus was primarily on aspects other than the transmission protocol, and thus, my understanding of WebRTC was, at best, rudimentary.

现在,既然要做自己的远程桌面产品,就不得不亲自面对 WebRTC。当然,前公司的 RTC SDK 肯定是不能用的,因为它以 Google WebRTC m89 为基准,这个库,稣认为太复杂了,没那么多时间去重新整理一遍。所以稣选择了一个比较小型但新颖的库,作为熟悉它的试验性项目,“声音共享”就诞生了。

Now, as I embark on creating my own remote desktop product, it is imperative that I confront WebRTC directly. Naturally, the RTC SDK from the previous company is out of the question for use, as it is based on Google WebRTC m89—a library I consider to be overly complex, and one that I simply do not have the time to reorganize from scratch. Therefore, I opted for a smaller yet more innovative library as an experimental project to familiarize myself with it, and thus, SoundShare was born.

开发过程 Development Process

首先在 Copilot 和 DeepSeek 的帮助下,理清了许多 WebRTC 的概念和原理,后来在 vcpkg 里搜索 rtc,对比了几个库,做好库的选型。

Initially, with the assistance of Copilot and DeepSeek, I clarified many concepts and principles of WebRTC. Subsequently, I searched for ‘rtc’ within vcpkg, compared several libraries, and finalized the selection of the appropriate library.

接下来,考虑先做个小工具练练手,不然直接把一个陌生的库上到 RegameDesk,万一后面发现不好用,岂不是要返工?

Next, I contemplated starting with a small tool to practice, reasoning that directly integrating an unfamiliar library into RegameDesk could lead to rework if it later proved unsuitable.

刚好,稣的笔记本扬声器很差,另一台迷你主机干脆就没有扬声器,还好又有一台脑波退下来的 iPhone 11 Pro Max,外放效果特别好,于是想到可以做个串流工具,让 iPhone 当笔记本的扬声器。

Coincidentally, my laptop’s speakers are subpar, and my mini PC lacks speakers altogether. However, I happen to have a decommissioned iPhone 11 Pro Max from my wife, which boasts excellent external audio performance. This led me to conceive of a streaming tool that would allow the iPhone to serve as the laptop’s speaker.

这里不得不吐槽一下 iPhone 11 Pro Max 的蜂窝信号,真的太差了,所以它基本上就是为 WiFi 而生,十分适合用来做"声音共享"的测试设备。

Here, I must vent a bit about the iPhone 11 Pro Max’s cellular signal—it’s truly abysmal. As a result, it’s essentially designed for WiFi, making it an ideal candidate for testing SoundShare.

虽然这本是一个练手的项目,但稣将它发布给一位朋友时,他竟然打赏了一块钱,于是干脆加点码,​做好一些!

Although this was initially a practice project, when I shared it with a friend, he unexpectedly tipped me one CNY. This gesture motivated me to enhance the project further and refine it to a higher standard!​

项目地址 Project Location

https://github.com/RegameDesk/sound_share

https://gitee.com/RegameDesk/sound_share