画圆
发表于更新于阅读时长 4 分钟和一点中学三角学知识还有一点 arch 常识
画圆和其他一些基础几何图形是常见的图形学需求,然而现代的图形 API 几乎只提供了绘制折线的功能(GDI 之类操作系统图形层的 API 可能会给,但是现在已经没人用了)。因此,有时候我们需要自己对圆进行插值,转化成折线再画到屏幕上。让我们先定义一些基础的结构体
rs
使用 f64 仅仅是因为现代 CPU 上 f32 和 f64 几乎一样快,实际的图形 API 几乎只有 f32 可以用。
写个插值并不难,如果你没把中学数学知识全忘掉的话
rs
然而这种写法在每次循环时都需要计算一次三角函数(一般都会有同时计算 sin 和 cos 的方法),这会带来一定的性能损 -- x87 里提供的fsincos指令非常慢,而 libc 里提供的 sincos 尽管经过了深度优化,但肯定还是会比简单的加减乘除慢。能不能去掉这次三角运算呢?
让我们观察第 n 次和第 n + 1 次的计算,可以看到连续的两次计算中,origin和radius都没有变,只有theta角发生了变化,每次固定增加delta,也就是说我们有
rs
这时,如果你还能多回忆起一点中学数学课知识的话,就能意识到我们可以用和差化积公式展开这堆三角函数
rs
观察这段代码,可以看到对theta_i的三角计算,被换成了对theta_i_1和delta的计算。再重复一遍,delta是个固定值,而theta_i_1则是前一次迭代中已经算好了的值。突然之间,每次循环所需的三角运算被消除掉了,只需要做几次加减乘除即可得到结果。这实际上相当于对这次运算做了个微分。
实现起来也没多麻烦
rs
跑个分看看
快了三倍。
这也告诉了我们要如何战胜编译器的自动优化 -- 通过编译器没掌握的领域知识。
然而这个优化能不能适用于对于直线的线性插值呢?(先别问为什么不用 GPU 插值)让我们也试试看
rs
跑分结果几乎没有区别
这是因为现代 CPU 都实现了乱序执行,如果两条指令之间没有依赖关系,而 CPU 内又有多余的计算单元,那么两条指令就可以并发执行,这叫做指令级并发(ILP)。在普通版本中,两次循环之间没有任何关系,因此可以并发执行;而“优化”过的版本中,后一次循环依赖前一次的结果。因此,即使后者比前者少掉一个乘法(SSE 这样的 SIMD 指令可以一次计算两个 f64 乘法),前者却能靠更高的指令并发度扳回性能劣势。
这也警示我们,任何性能优化的基础都是观测和跑分,不进行实际测量而空谈性能是毫无意义的。