Turns are Better than Radians (2022)

Turns are Better than Radians (2022)

“圈”(Turns)优于弧度(Radians)

Switching away from radians makes code simpler, faster, and more precise. 放弃弧度制会让代码更简洁、更快速、更精确。

Casey Muratori | Sep 26, 2022 Casey Muratori | 2022年9月26日

Some time ago, much effort was expended to convince people to replace approximations of “pi” (3.14159…) with approximations of “tau” (6.28318…). The idea, according to numerous blog posts and YouTube videos, was that common formulas become simpler, and it’s easier to work with a constant describing an entire circle instead of half a circle. 前些年,人们花费了大量精力去说服大众用“tau”(6.28318…)的近似值来取代“pi”(3.14159…)的近似值。根据众多的博客文章和视频,其核心观点是:使用描述整个圆的常数比描述半圆的常数更符合直觉,也能简化常用公式。

Generally, I agree. While it’s a minor point, it’s worth making. Most code does get slightly better if you replace pi with tau. However, in all the fanfare, a far more impactful opportunity was overlooked. Instead of replacing pi with tau, most of the time pi can be removed entirely. 总的来说,我表示赞同。虽然这只是一个小细节,但确实有其价值。如果用 tau 替换 pi,大多数代码确实会稍微好一点。然而,在这一片喧嚣中,一个影响深远得多的机会却被忽视了。与其用 tau 替换 pi,不如在大多数情况下直接彻底移除 pi。

Here’s how that works. 具体原理如下。

First, consider the common case for pi and tau in code: converting things to and from radians for calls to trigonometric functions. If you’ve ever used these constants, the vast majority of what you wrote probably did something like this: 首先,考虑代码中 pi 和 tau 的常见用法:在调用三角函数时进行弧度转换。如果你曾经使用过这些常数,你写的大部分代码可能都是这样的:

y = center.y + (center.y * Math::sin(h * Math_TAU) * s) - (cursor->get_height() / 2);

That’s not me constructing an example, that’s me randomly opening the source code for the Godot Engine on github and searching for “tau”. The piece of code above, and dozens of similar uses, is what comes up. 这不是我编造的例子,而是我随机打开 GitHub 上的 Godot 引擎源代码并搜索“tau”得到的结果。上面那段代码以及几十个类似的用法,就是这样出现的。

There is nothing special here about Godot. If you opened any random game engine codebase, you could do the exact same search and see the exact same kind of usage. Godot 在这方面并没有什么特殊之处。如果你打开任何一个随机的游戏引擎代码库,进行同样的搜索,你也会看到完全相同的用法。

Notice what is going on here: the programmer has a value h which is already periodic on the range 0 to 1, but they multiply by tau because they need to call sin. 注意这里发生了什么:程序员拥有一个已经在 0 到 1 范围内呈周期性的值 h,但因为需要调用 sin 函数,他们不得不乘以 tau。

This may seem very sensible if that’s as far as you look. But what about the implementation of sin? 如果只看到这一步,这似乎非常合理。但 sin 函数的底层实现又是怎样的呢?

There are many implementations of sin, but no matter which one you look at, near the entry point of the function you’ll see something like this: sin 函数有很多种实现方式,但无论你看哪一种,在函数入口附近你都会看到类似这样的代码:

_PS256_CONST(cephes_FOPI, 1.27323954473516); ... y = _mm256_mul_ps(x, *(v8sf*)_ps256_cephes_FOPI);

Again, not me making up an example - that’s from this commonly referenced AVX2 implementation of sin. It’s not unusual or weird - pretty much every fast trig library is going to do something very similar. 再次声明,这不是我编造的例子——它来自那个被广泛引用的 AVX2 版 sin 实现。这并不罕见也不奇怪——几乎每一个高性能三角函数库都会做非常类似的事情。

What does this line do? It multiplies the input by the constant 1.27323954473516. Which just so happens to be 4/pi. 这一行代码做了什么?它将输入乘以常数 1.27323954473516。而这个数恰好就是 4/pi。

So the calling code is doing this: sin(h * 2 * pi) but the library code immediately does this: y = (4 / pi) * x which means the calling code is multiplying by a factor of pi just so the library code can immediately divide it back out again. It’s literally a conversion to radians and back for no reason. 所以,调用方的代码在做:sin(h * 2 * pi),而库代码紧接着做了:y = (4 / pi) * x。这意味着调用方乘以 pi 仅仅是为了让库代码立刻把它除掉。这纯粹是毫无意义的“转为弧度再转回来”。

If both programmers had just agreed not to use radians, and instead used the original [0, 1] domain that h was already on, both their jobs get simpler: the caller saves a multiply, while the library gets a simpler-to-understand, exact constant. 如果双方程序员都约定不使用弧度,而是直接使用 h 原本所在的 [0, 1] 定义域,那么双方的工作都会变得更简单:调用方省去了一次乘法,而库函数则获得了一个更易理解且精确的常数。

And the “exact” part is actually quite interesting. Not only do you pay for an extra multiply when you spuriously convert to radians, but it’s also worth noting that all common radian angles besides 0 are difficult to represent. Want to store 90 degrees in radians? No matter how many bits you use, it will never be exact. 关于“精确”这一点其实很有趣。当你为了转换弧度而进行多余的乘法运算时,不仅付出了性能代价,还值得注意的是,除了 0 以外,所有常见的弧度角都难以精确表示。想用弧度存储 90 度吗?无论你使用多少位来存储,它永远无法精确。

90 degrees on [0, 1], however, is just 0.25 - a bit pattern that doesn’t even require any bits of mantissa at all! 0.5? Same! 0.75? Just one bit of mantissa to represent exactly. 然而,在 [0, 1] 范围内,90 度仅仅是 0.25——这个位模式甚至不需要任何尾数位就能精确表示!0.5 也是如此!0.75?只需要一位尾数就能精确表示。

So the [0, 1] range is not only more computationally efficient than radians, it is also more compact and precise when representing typical values that frequently occur in practical use. 因此,[0, 1] 范围不仅比弧度制计算效率更高,而且在表示实际应用中频繁出现的典型值时,也更加紧凑和精确。

Math doesn’t require radians. 数学并不要求必须使用弧度。

I can understand why some people would be worried about making this switch. Even if you believe me that all user-side code multiplies by pi or tau, and all library-side code divides it back out, you still may have that sinking “math class feeling” that you’d be doing something wrong if you stopped using radians. 我能理解为什么有些人会担心做出这种改变。即使你相信我所说的——用户侧代码在乘以 pi 或 tau,而库侧代码又立刻将其除掉——你可能仍然会有那种“数学课后遗症”,觉得如果不使用弧度制,自己就是在做错误的事情。

But math never decreed that sine and cosine have to take radian arguments! 但数学从未规定正弦和余弦函数必须以弧度作为参数!

The idea of parameterizing a circle from zero to one instead of from zero to tau is not a random idea I made up for this blog post. It’s actually a legitimate, existing mathematical construct, and it even has a name: it’s called a turn. 将圆的参数化范围从 0 到 1 而不是 0 到 tau,并不是我为了这篇博客随手编造的想法。它实际上是一个合法且现有的数学概念,甚至有一个专门的名称:它被称为“圈”(Turn)。

In turns, 0 is 0 degrees, 0.5 is 180 degrees, 1 is 360 degrees, 2 is 720 degrees, and so on. It’s exactly what we wanted. 在“圈”的定义下,0 是 0 度,0.5 是 180 度,1 是 360 度,2 是 720 度,以此类推。这正是我们想要的。

So if you are worried that your math teacher will get mad at you, there is no cause for concern. Just tell them that you considered the matter carefully, and decided that parameterizing your angles in turns instead of in radians was the most efficient method for the problem at hand! 所以,如果你担心数学老师会生气,完全不必担心。只需告诉他们,你经过深思熟虑,认为对于当前的问题,使用“圈”而不是弧度来参数化角度是最有效的方法!

Making the switch is easy. 进行这种转换很容易。

If you wrote your own math library, or you copied someone else’s into your project, hopefully it is quite clear how you can switch away from radians and eliminate pi and tau from your codebase. All you have to do is take your sin and cos functions and make them take turns instead of radians, which usually involves nothing but a quick adjustment to a single constant. 如果你编写了自己的数学库,或者将别人的库复制到了项目中,你应该很清楚如何放弃弧度制并从代码库中消除 pi 和 tau。你所要做的就是修改 sin 和 cos 函数,让它们接收“圈”作为参数而不是弧度,这通常只需要对一个常数进行简单的调整即可。

If you want to support legacy code, pick a different name for the new turn-based trig functions. Then, for legacy code, you can still support the old radian-based sin and cos by making those routines thunk through to the new routines, doing the divide-by-tau along the way. 如果你想支持遗留代码,可以为新的基于“圈”的三角函数起一个不同的名字。然后,对于遗留代码,你仍然可以通过让旧的基于弧度的 sin 和 cos 函数调用新的函数,并在中间执行除以 tau 的操作来保持兼容。

It’s very simple - just a few lines of code to make the switch. 这非常简单——只需几行代码就能完成转换。

However, although I find turns to be the most convenient reparameterization, it’s not the only alternative. Especially if you don’t roll your own math routines (and perhaps even if you do), you may instead want to consider using half turns, where a full circle is [0, 2]. It’s a bit more confusing, but… 然而,虽然我认为“圈”是最方便的重新参数化方式,但它并不是唯一的选择。特别是如果你不打算自己编写数学例程(即使你打算编写),你可能也会考虑使用“半圈”(half turns),即一个完整的圆对应 [0, 2]。这稍微有点令人困惑,但是……

It already exists in some libraries! 它已经存在于某些库中了!

It turns out (pun intended!) that if you go looking for it, in some math libraries you will already find sin and cos functions parameterized on half-turns instead of radians. For example, the CUDA sincospi intrinsic computes the sine and cosine of the input multiplied by pi, which is a half-turn. 事实证明(一语双关!),如果你去寻找,会在一些数学库中发现已经存在以“半圈”而不是弧度为参数的 sin 和 cos 函数。例如,CUDA 的 sincospi 内置函数计算的就是输入乘以 pi(即半圈)后的正弦和余弦值。

This is great. If you’re targeting a platform with… 这太棒了。如果你正在针对某个平台进行开发……