浅析帧同步定点数设计

2316 字
12 分钟
浅析帧同步定点数设计

引入#

在我们开发单机游戏时,使用 float 类型来计算是非常常见的,但是当涉及到帧同步网络开发时,传统帧同步不传状态,但是必须可被确定性推导/哈希校验,此时float 类型的精度问题可能会导致不同客户端之间的计算结果不一致。

为了解决这个问题,我们可以使用定点数(Fixed-Point)来代替 float 类型。

为什么浮点运算在不同客户端计算的结果可能不同#

IEEE 754#

IEEE 754 规定了计算机里浮点数如何表示、如何做基本运算、如何舍入等内容。

例如我们常说的 float(32 位单精度)和 double(64 位双精度)通常就对应 IEEE 754 的 binary32 和 binary64。

欸,你可能会问,IEEE 754 不是已经规定了浮点运算的统一语义了吗,为啥还要额外使用定点数?

原因有很多,例如:编译器可能不严格按照标准执行,中间计算时也可能使用更高精度,编译器可能重排表达式等等。

硬件指令与中间精度#

不同的硬件平台可能使用不同的浮点运算指令,这些指令在执行过程中可能会使用更高的内部精度进行计算,然后再将结果舍入到目标精度。这种中间精度的差异会导致在不同硬件上运行相同的浮点运算时产生不同的结果。

例如:x86 早期 x87 浮点单元用 80 位扩展精度做中间计算,再存回 64 位;SSE/AVX 则按 32/64 位计算。同时,是否使用 FMA(融合乘加)也会影响结果。

a*b+c 一次舍入和两次舍入相比,结果可能差 1 ulp。

编译器优化#

编译器在优化过程中可能会对浮点运算进行重排、合并等操作。

一个简单的例子是,在开启 fast-math/-ffast-math 后,编译器可假定浮点满足结合律、交换律,重排 (a+b)+c 为 a+(b+c),结果就会变。

数学库#

sin/cos/exp/log/pow 等超越函数在 IEEE 754 中通常不要求“正确舍入”,各平台 libm、CRT、JIT 运行时实现不同,最后几位可能不同。

不过 sqrt 通常有较好的约束性。

其他#

还有很多因素都能造成浮点运算在不同客户端计算的结果不同,例如:运行时差异、浮点环境状态不同、并行/归约顺序不同等等。

Q 格式#

定点数通常用 Q 格式表示。

比较通用的记法是 Qm.n ,其中 m 和 n 分别表示有 m 位整数部分和 n 位小数部分,它的实际值等于底层整数除以 2^n。例如,Q16.16 表示 16 位整数部分和 16 位小数部分。范围约为 ±32767.99998,精度为 1/65536 ≈ 0.000015.

另一种常见的记法是 Qn ,指总位宽为 N ,其中 n 表示小数部分的位数。例如Q15通常指16位总宽中,1位符号,15位小数。

运算规则#

定点数的运算本质上是整数运算,但必须时刻跟踪二进制小数点的位置。

加法 / 减法#

必须保证两个操作数的Q格式完全一致。结果保持相同的Q格式。运算本身是直接的整数加减,但需要警惕溢出。例如,两个Q15的数相加,结果可能超出16位能表示的范围。

乘法#

两个Q格式的数相乘,小数位数会相加。Qm1.n1 × Qm2.n2 → Q(m1+m2).(n1+n2)。

例如,Q16.16 × Q16.16 会得到一个 Q32.32 的结果(在64位整数中)。如果最终要存回32位的Q16.16,就需要将结果右移16位,并配合舍入操作。

除法#

除法是乘法的逆运算,小数位数会相减。为了保持精度,标准做法是先将被除数左移足够的位数,再执行整数除法。

一个保持最大精度的通用方法是:将被除数左移 n2 + i2 位(假设除数为 U(i2.f2) 格式),然后进行整数除法。这可以避免因过早截断而丢失小数精度。除法的结果格式取决于移位量。

乘除法的移位#

原理#

设两个操作数:

a = A / 2^na(A 是 a 的底层整数,格式 Q?.na)

b = B / 2^nb

相乘:

a × b = (A / 2^na) × (B / 2^nb) = (A × B) / 2^(na + nb)

乘积的底层整数是 A × B,但它的小数位数变成了 na + nb。

如果目标格式要求小数位为 nc,就需要把小数点从 na + nb 挪回 nc:

C = (A × B) / 2^((na + nb) - nc) 当 (na + nb) > nc
C = (A × B) × 2^(nc - (na + nb)) 当 (na + nb) < nc

游戏中最常见的是 同格式相乘、结果保持同格式,此时 na = nb = nc,移位量 = na,即:

Q16.16 × Q16.16 → 64位中间结果 Q32.32 → 右移 16 位 → Q16.16

示例:

a = 1.5 → A = 1.5 × 65536 = 98304
b = 2.25 → B = 2.25 × 65536 = 147456
A × B = 98304 × 147456 = 14495514624 //这是 Q32.32
右移 16 位 → 14495514624 / 65536 = 221184
221184 / 65536 = 3.375 //1.5 × 2.25 = 3.375

舍入#

直接 >> 16 会丢掉低 16 位,产生系统性偏差。标准做法是先加半个最低位,再右移:

// Q16.16 乘法,Round half up(向 +∞ 方向四舍五入)
int32_t qmul(int32_t a, int32_t b) {
int64_t prod = (int64_t)a * b; // Q32.32
int64_t rounded = prod + (1LL << 15); // 加 2^(shift-1)
return (int32_t)(rounded >> 16); // 算术右移
}

+ (1 << (shift-1)) 的直觉:低 shift 位若 ≥ 一半(2^(shift-1)),加法会向上进位,右移后就“入”;否则“舍”。

负数要注意:上面写法对负数是“round half up”(朝 +∞ 方向),不是“round half away from zero”。若要对称的四舍五入(远离零):

int64_t rounded = (prod >= 0)
? prod + (1LL << 15)
: prod - (1LL << 15);

位宽分配#

如何确定所需位宽?#

一个实用的量化公式是:所需的整数位数 ≈ ceil(log2(总范围 / 精度))。

在游戏开发中,有一些常见的选择:

  • 32位 (Q16.16):适合中小规模场景(如RTS、MOBA),精度约 1.5e-5。

  • 64位 (Q32.32):一些确定性框架会采用 32.32 定点数 (FP64),以获得极大的范围和极高的精度,代价是64位整数运算的开销。

  • 64位全整数缩放:另一种思路是使用 int64,并约定 1单位 = 2^-32。这样 i64 的范围可达 ±2^31,分辨率高达 2^-32,在整个范围内精度均匀,且溢出行为完全可预测。

溢出、舍入和三角函数#

溢出#

定点数运算必须显式处理溢出。常见策略:

  1. 饱和运算:结果超出范围时,钳制到该类型的最大/最小值,并设置溢出标志。

  2. 使用更宽的中间类型:例如,两个32位数相乘,结果先用64位存放,再做舍入和截断,可以避免乘法过程中的溢出。

舍入#

此处舍入和上文乘除法的移位中的舍入要点相同

三角函数和超越函数#

如本文最开头所说的,sin, cos, sqrt 等函数没有直接的整数运算指令。为了保证确定性,通常的做法有如下两种:

  1. 查找表 (LUT):预计算固定精度的结果表,通过索引和插值来近似。

  2. 确定性近似算法:实现一套仅使用整数和移位操作的近似算法(如CORDIC),并确保所有平台执行完全相同的指令序列。

状态同步需要定点数吗#

通常来说,状态同步以服务器为权威,不依赖客户端确定性,客户端以服务器下发的状态为准进行渲染和表现。虽然客户端本地的浮点数运算、操作系统或第三方库可能带来不确定性,但这并不影响核心逻辑,因为最终的正确性由服务器保证,客户端之间无需达成计算上的一致。

但是这并不绝对,使用定点数仍是一种提升代码健壮性的一种选择。

什么时候该加入定点数#

  1. 需要客户端预测(Client-side prediction):为了掩盖网络延迟,用户本地在收到服务器确认前往往会使用本地逻辑来预测状态的变化。此时使用浮点数进行预测,而服务器或者后续的矫正使用的不同的精度来进行计算,可能导致预测结果与服务器权威状态出现不匹配的问题,此时会造成一定的抖动。在这种情况下,使用定点数来写预测逻辑可以提升预测的准确性和一致性。
  2. 回放系统、离线验证:如果游戏需要跨平台保持状态快照一致性,使用定点数可以保证快照数据在任何平台都能准确无歧义地解读,避免因为浮点数表示差异带来的微小偏差。

其他#

定点数数学库现已有非常成熟的内容,我们正常开发时不需要手写。

INFO#

https://www.sciencedirect.com/topics/computer-science/fixed-point-number#4

https://esengine.cn/modules/network/fixed-point/

浅析帧同步定点数设计
https://blog.rouming-fei.top/posts/analysis-of-fixed-point-number-design-in-frame-synchronization/
作者
废江流
发布于
2026-10-19
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
废江流
提灯寻影,灯到影灭。
分类
标签
站点统计
文章
19
分类
3
标签
16
总字数
62,531
运行时长
0 天
最后活动
0 天前

文章目录