“如何快速捏曲线公式”的代码优化
本文提供常用的数值曲线公式在代码优化方面的小技巧,例子比较简单,抛砖引玉。
对于一些规则不稳定的需求,如某匹配机制的高级动态调整函数、某些积分计算,在调试迭代完成之前,往往需要频繁重构。此时以脚本实现一个公式,相比配置表是一个更为灵活便捷的方式。这里介绍一个常用函数,使用python编写。
《新手数值策划如何快速捏曲线公式》
文中提到的曲线拟合公式在项目中的代码实现如下,其中FP为帧同步使用的定点数类型。
public static FP CalEvalAttr_Old(FP x, FP b, FP c, FP r){
c = -c;
FP a = -FP.One / (FP.Atan(b * c) - FP.Atan(b * c + r * b));
FP d = FP.Atan(b * c) / (FP.Atan(b * c) - FP.Atan(b * c + r * b));
return a * FP.Atan(b * (x + c)) + d;
}
由于公式是由python用遗传算法生成的,里面有大量冗余计算,例如 FP.Atan(b*c) ,以及a和d的分母部分是完全一样。从计算机基础原理可知,反三角函数会带来较大的性能开销。项目中FP.Atan使用的是查表法(注,查表之前需要做变换,还要处理正负无穷),但是多次重复查表依然有巨大的优化空间。
尝试进行数学上和代码上等价变换移除冗余计算(善用宇宙第一IDE VisualStudio代码重构提取公共表达式)
public static FP CalEvalAttr(FP x, FP b, FP c, FP r)
{
FP f = b * (-c);
FP atanf = FP.Atan(f);
return (atanf - FP.Atan(f + x * b)) / (atanf - FP.Atan(f + r * b));
}
反三角函数计算从6次减少为3次,其他四则运算也一定程度减少。考虑计算复杂度,预估应该会获得双倍的性能。
那么事实上是如此么?
且慢,不做任何单元测试是重构大忌,先撸一段单元测试测一下!参考数据来自项目真实代码。
PS:还不会在Unity里面做单元测试的小伙伴可以等我的视频课。
using FixMath;
using NUnit.Framework;
class TestCalEvalAttr
{
[Test]
public void Test1()
{
var a1 = (double)CalEvalAttr(3, 0.1f, 5, 30) * 15;
var a2 = (double)CalEvalAttr_Old(3, 0.1f, 5, 30) * 15;
Assert.AreEqual(a1, a2, 1e-8);
var c1 = (double)CalEvalAttr(20f / 60, 0.2f, 10, 21) * 10;
var c2 = (double)CalEvalAttr_Old(20f / 60, 0.2f, 10, 21) * 10;
Assert.AreEqual(c1, c2, 1e-8);
}
}
注意,浮点数是不能直接比较相等的,一般使用Assert.AreEqual第三个参数控制允许的误差精度,示例为小数点后第8位。
测试通过,比较稳了!
现在来测一下性能有多少提升,同样也可以用单元测试来简单跑个分,测试负载为循环计算10000次。
[Test]
public void Test2()
{
FP a1 = FP.Zero;
FP a2 = FP.Zero;
long t0 = RealTime.ticks;
{
for (int i = 0; i < _loopCount; i++)
a1 = CalEvalAttr(3, 0.1f, 5, 30);
}
long t1 = RealTime.ticks;
Debug.Log($"CalEvalAttr*{_loopCount} {t1 - t0}ms");
t0 = RealTime.ticks;
{
for (int i = 0; i < _loopCount; i++)
a2 = CalEvalAttr_Old(3, 0.1f, 5, 30);
}
t1 = RealTime.ticks;
Debug.Log($"CalEvalAttr_Old*{_loopCount} {t1 - t0}ms");
Assert.AreEqual((float)a1, (float)a2); // 小数点后8位都ok,试试直接转成单精度浮点比较
}
private const int _loopCount = 10000;
流弊的结果出来了,23毫秒 vs 47毫秒,与预估的性能提升几乎一致,计算性能提升了100%。
Test2 (0.076s)
---
CalEvalAttr*10000 23ms
CalEvalAttr_Old*10000 47ms
可能有人会说10000次调用才快了20多毫秒?
实际项目中大量数值拟合计算,特别是用在做随机决策的评估函数,极端情况下一帧调用上千次也是正常现象。那么问题来了,如果要跑满60fps,一帧内的所有计算和渲染不能超过16ms,区区1000次计算从5ms优化到2ms就是巨大的提升。
最后,总结一下如何做这类密集计算型的小优化:
- 公式等价变换,中小学数学要过关
- 代码等价变换,善用重构工具
- 重构后要用单元测试验证
- 用实际的性能分析来印证计算复杂度估算
评论已关闭