跳到主内容
@wquguru
精选82Hacker News Best(web_list)技巧与观点

用 Turns 替代 Radians:消除冗余乘除提升精度与性能

Turns 优于 Radians:角度单位之争

原文
发到 X

Turns are Better than Radians

角度制优于弧度制

Switching away from radians makes code simpler, faster, and more precise.

放弃使用弧度制能让代码更简单、更快、更精确。

Casey Muratori

Casey Muratori

Sep 26, 2022

2022年9月26日

148

148

47

47

1

1

Share

分享

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…)的近似值。根据众多博客文章和 YouTube 视频的说法,这一想法是:常用公式会变得更简洁,而且使用一个描述整个圆的常数比使用描述半个圆的常数更容易处理。

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.

一般来说,我同意这个观点。虽然这是一个小细节,但值得指出。如果你将 pi 替换为 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,不如直接将 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 的常见用途:为了调用三角函数而进行弧度之间的转换。如果你曾经使用过这些常量,你写的绝大多数代码可能都类似于这样:

代码 · 2
y = center.y + (center.y * Math::sin(h * Math_TAU) * s) -
    (cursor->get_height() / 2);
代码 · 2
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 Engine 的源代码并搜索“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.

注意这里发生了什么:程序员有一个值 h,它已经在 0 到 1 的范围内具有周期性,但他们乘以 tau,因为他们需要调用 sin。

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 有许多实现,但无论你查看哪一个,在函数的入口点附近,你都会看到类似这样的代码:

代码 · 3
_PS256_CONST(cephes_FOPI, 1.27323954473516);
...
y = _mm256_mul_ps(x, *(v8sf*)_ps256_cephes_FOPI);
代码 · 3
_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.

这一行做了什么?它将输入乘以常数 1.27323954473516。

Which just so happens to be 4/pi.

而这恰好等于 4/pi。

So the calling code is doing this:

所以调用代码正在这样做:

代码 · 1
sin(h * 2 * pi)
代码 · 1
sin(h * 2 * pi)

but the library code immediately does this:

但库代码立即这样做:

代码 · 1
y = (4 / pi) * x
代码 · 1
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. 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.

我能理解为什么有些人会担心做出这种改变。即使你相信我所说的所有用户端代码都乘以 π 或 τ,而所有库端代码都将其除回去,你可能仍然会有那种“上数学课时的不安感”,觉得如果不使用弧度就会做错什么。

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.

将圆从零到一进行参数化而不是从零到 τ,并不是我为了这篇博客文章凭空捏造的想法。这实际上是一个合法且现有的数学结构,它甚至有一个名字:称为“转”(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 函数,让它们接受角度(turns)而不是弧度,这通常只需对单个常量进行快速调整即可。

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 的正弦和余弦,而 pi 正好对应一个半圈。

This is great. If you’re targeting a platform with sincospi already available, you can stop using pi and tau constants in your code right now without touching your libraries at all. Just start calling sincospi with half-turns instead of sin and cos with radians, and you’re good to go.

这太棒了。如果你的目标平台已经提供了 sincospi,你现在就可以停止在代码中使用 pi 和 tau 常量,完全无需触碰你的库。只需开始用半圈调用 sincospi 代替用弧度调用 sin 和 cos,你就可以顺利使用了。

The less tau and pi, the better.

tau 和 pi 越少越好。

Having now managed entire codebases where I stopped using radians, I can safely say I never miss them. All the superfluous tau’s and pi’s disappear, and everything reads more clearly.

既然我已经成功管理过整个代码库并停止使用弧度,我可以肯定地说我从未怀念过它们。所有多余的 tau 和 pi 都消失了,一切阅读起来更加清晰。

The same logic for modifying sin and cos applies to the rest of the standard trig functions as well, so you can eliminate radians everywhere if you choose. Libraries almost always convert away from radians internally anyway, and then convert back to radians on the way out, so switching to turns or half-turns everywhere is usually just a matter of deleting code and not much else.

修改 sin 和 cos 的相同逻辑也适用于其余的标准三角函数,因此如果你愿意,可以在任何地方消除弧度。无论如何,库内部几乎总是会将弧度转换为其他单位,然后在输出时再转换回弧度,因此将所有地方切换到角度或半圈通常只需删除代码,除此之外没多少工作要做。

Thanks for reading Computer, Enhance! Subscribe for free to receive new posts.

感谢阅读 Computer, Enhance!免费订阅以接收新文章。

Subscribe

订阅

148

148

47

47

1

1

Share

分享

PreviousNext

上一篇下一篇

更进一步:量化金融体系

看懂新闻只是起点——沿量化金融路径,把它变成能交付的工程能力

进入量化体系 →

相似阅读

另一事件,读法相近