前几天策划要求给任务系统加上日常任务的功能,这个需求很简单:象 wow 一样,日常任务在每天某个时候定时刷新。在思考添加这个功能时,很直接想到的方案是服务器取出 3 个时间:任务上次完成时间( pre ),当前时间( now )和当天刷新时间( refresh ),然后经过一些较复杂的逻辑完成(此处省略 xx 字)。由于该方案需要涉及一些时间操作,而在 lua 和 c++ 里面做日期时间的操作又是比较麻烦的一件事。水古给出了一个解法,很好的解决了这个问题:首先用当前时间(比如现在是 18:00 )减去当天刷新时间(比如 wow 的日常每天 15:00 刷新),这样就把时间对齐到前一天的 0 点,然后分别用当前时间和上次完成时间除以一天的秒数( 24*60*60 ),就分别得到了当前时间和上次完成时间的天数,如果上次完成时间的天数小于当前时间的天数,则日常任务刷新。 lua 代码大致如下:
local now = os.time()
local pre = task.completed_time
local refresh = (15-8)*(60*60) -- 减 8 是因为 os.time() 得到的是 UTC 时间,而北京时间是 UTC+8
local pre_days = (pre-refresh)/(24*60*60)
local now_days = (now-refresh)/(24*60*60)
if(now_days > pre_days) then
-- 刷新任务
end
很简洁,整个算法只需要一个时间函数—— os.time() 。
本以为这个功能就这样完成了,但经过反复测试,悲剧发生了:服务器的时间正确,客户端却有 1 分钟左右的偏差。同样的库,同样的脚本,却得到不同的时间。掘地三尺后发现在客户端中 lua 中的双精度浮点数经过加减运算后变成了单精度浮点数,由于精度问题导致产生一定的时间偏差——和谐的阳光照在杯具上,每一个杯具都笑开颜。
首先想到的还是 lua 库的问题,毕竟是在 lua 脚本里面精度变了,但我直接在 c++ 里面写了一个简单的测试,发现精度也改变了。再次和几个朋友讨论这个问题,水古提到: DX 会改变浮点精度,确实,我记忆中好像在哪看到过,搜索 D3D sdk ,在 CreateDevice 中发现 D3DCREATE 有这么一个选项
D3DCREATE_FPU_PRESERVE | Set the precision for Direct3D floating-point calculations to the precision used by the calling thread. If you do not specify this flag, Direct3D defaults to single-precision round-to-nearest mode for two reasons:
|
很明显了, d3d 在未指定该参数的时候会把 FPU 设置为单精度模式,其原因是因为处理单精度数据比双精度具备更好的性能。由于 D3D 默认修改了 FPU ,影响到了 lua 的精度( lua 里面 number 只有 double ),造成了一定时间的误差。
至于性能问题,我是这样想的,以单个浮点运算来看,现代 cpu 对 double 和 float 的运算效率非常接近了,更多的效率考虑可能是 cpu 高速缓存的命中和内存带宽的暂用吧,毕竟 double 比 float 多一倍的数据量,而在 3D 环境中浮点运算大量存在。
最后说一下,该问题只是一个中间过程,最后把客户端时间修改成服务器的时间,在客户端就不用很精确的时间控制,所以这里暂时无需修改客户端代码,客户端仍然以默认的 24 位 FPU 精度模式运行,如何兼顾性能和精度,也就没去深究了。