返回值优化没了 / FPU爆栈 / 线程池疯长 / 连接被重置

标题之所以那么复杂是因为我把本来应该分开写的东西弄在一起了……
这些事情都是在公司里工作(或者摸鱼?)的时候遇到的。有的记下来了有的没有。记下来的这几个,本来说要写到博客上的,结果丢在桌面的便签上这么久了,现在已经从那家公司离职,又入职新的公司做了一年半,这便签上记录的东西一个都还没写。都是两年前的事了吧

1、全局优化下消失的返回值

那个时候在做的项目是把最终幻想10移植到PC平台上。有个奇怪的地方是玩家放了“探查”技能,出来的显示敌人信息的界面,在Release下什么键都不按、敌人的模型也会在一直缩放到最小才停下;但是Debug下没有这个问题。

因为能单步跟踪调试,所以事情就好办很多。它里面有个函数会在调用的时候返回用户押了哪些按键。但是在Debug和Release下,这个函数的返回值不太一样。函数内容很简单,就大概长这样(大概意思。具体函数名我已经不记得了):

int GetPressedKey()
{
    GetPressedKeyImpl();
}

GetPressedKeyImpl 是返回 int 的,而且在那之后什么东西都没有执行。所以如果 GetPressedKeyImpl 在返回的时候把返回值存入了 eax 寄存器,然后 GetPressedKey 又什么事都没干,那么在它返回的时候外面拿到的返回值是 GetPressedKeyImpl 的。

但是在开启了全局优化之后,我猜测它检测到了这个 GetPressedKeyImpl 只有这个地方有调用,然而调用了又不拿返回值,所以生成代码的时候,就跳过了存入返回值这个阶段,所以生成的代码在 GetPressedKeyImpl 里面,也找不到和 return 语句相关的部分(反汇编跟踪看到的)。没开全局优化的时候,谁知道它有没有其他地方用到,所以在单个编译单元里面不敢做这样的优化。

当然说到底,这个问题是因为 GetPressedKey 函数里没写 return 。然后编译的时候警告又是几万个那种、没人去看。PS3 和 PS4 的编译器没有到这种程度(链接时)还能优化,所以在 PS3、PS4 上这个游戏运行没有问题。但是到了 PC 上,用了 VC 编译,这问题就出来了。

 

2、FPU爆栈

对于 C 语言来说,寄存器怎么用、栈怎么用是编译器给你决定的,在使用 80x87 协处理器计算浮点数的时候,栈怎么操作也是。但是项目里就遇到了个问题,游戏跑起来画面各种诡异,到处都是一块块黑的,但是又不是整个屏幕完全黑。我知道这个事情的时候,从同事那边听说来的,已经是一个函数里下着断点,看局部变量的话,两个里面数据都正常的浮点数,运算出来的结果……是错的。肉眼可见的错误,算出来的结果是奇怪的值。

这个问题我调查没结果,这边的 Leader 程序员调查也没结果,最后是交给技术总监了。后来技术总监给 Leader 回了邮件,我专门她抄送一份给我,然后知道了这个问题出在哪里。

在 VC 里,如果一个函数的返回值是浮点数,那么这个返回值将会放在 FPU 的栈顶来返回的,由调用者从栈顶取出这个值。但是如果,这个函数没有声明就直接调用,在 C 语言里是默认这个函数返回 int 的。这样一来,函数调用完之后,被调者把返回值放在 FPU 的栈顶,而调用者却没有把这个返回值出栈。这样一来要不了几次,栈就到了极限容量。再往里面压新的数据,FPU 就直接报异常了。

 

 

3、线程池中疯长的线程

也是那个项目。游戏用到的数据,无数多的小文件给文件系统带来负担,读取速度也会受影响,而且很多数据其实信息量不大,浪费了不少空间。所以就打算压缩一下打包成“大文件”。研发打包工具的任务在我和另一个同事身上。我是做打包的,他是做解包的。

最初的版本做出来之后,顺序读取、单线程压缩,没有办法很好地发挥打包专用服务器的性能。所以接下去就是再优化一下。

我那会儿想到了Windows的线程池:源文件数据读出来之后,因为是一个个数据块分块压缩的,所以可以把压缩任务用多个线程并行执行。因为之前看过一本《Windows并发编程指南》的书,里面提到了Vista的线程池和遗留线程池,说可以由系统来管理它们,CPU满的时候就不会再开新线程了,看起来正是我所需要的东西;同时为了兼容性,选用了旧的遗留线程池。

我用了信号量来控制最大同时执行的任务数,其中用到了 RegisterWaitForSingleObject 这个 API 来等待信号量。这个 API 有个参数,可以设定等待的模式, WT_EXECUTEONLYONCE 表示只等待一次。我用了这个 Flags,然后执行的时候发现一下子就卡得不行动不了了。

在 Visual Studio 里用调试方式运行,中断程序之后看线程列表,看到有无数个线程在里面,根本不想书上说的“系统会控制线程的数量”。这么明显和严重的 Bug,怎么可能呢……我觉得,有那么多的服务器软件在 Windows 下,会要用线程池。

因为开头说的时间久远的缘故,具体问题在哪里是怎么发现的已经不太记得了,可能是仔细阅读的时候看到了 MSDN 上这个函数下面的 Remark,也可能是网上找的示例代码,发现了这个 RegisterWaitForSingleObject 函数就算指定了 ONCE 的标志位,它还是需要 UnregisterWait 才行。加上这个 UnregisterWait 的调用,出来一大堆线程的问题消失了。

 

4、重置的套接字

有忙的时候也有闲着的时候。闲着的时候想学学看怎么 C++ 写服务器程序,因为 CGI 每次要开进程影响性能,FastCGI 看了一圈发现协议还有点小复杂,正好那会儿在尝试 Fossil 版本控制工具能以 SCGI 形式挂在 Web 服务器上用,然后就顺势找了一下 SCGI 是什么东西,发现它协议的实现特别简单,就决定用它来实验了。

实验过程中发现我这边能收到 NginX 的连接,能收到请求,能收到数据也发送了回复,但是浏览器访问 NginX 却总抓不到我 C++ 写的 SCGI 的输出。在 NginX 的日志里面,只见它说 Connection Reset。这个 Connection Reset 对于经常上外国网站的人来说简直不要太常见,但是这是同一台机器、本地环回网络啊。因为网络编程经验不足,遇到这样的错误也没什么思路。

过程不太记得了,最终结论是我收 SCGI 请求的时候,还收了长度和数据,但是最后的逗号漏掉接收了。也就相当于我关闭 Socket 的时候,它接收缓冲区里面还有数据。此时我把连接关掉,它就 Connection Reset 了……算是一个经验。

发表评论