圈优于弧度

Hacker News Top 新闻

摘要

文章认为,在编程中使用圈代替弧度可以简化代码并提高性能,通过避免三角函数实现中的不必要转换。

暂无内容
查看原文
查看缓存全文

缓存时间: 2026/08/20 04:11

# 圈数优于弧度 来源:https://www.computerenhance.com/p/turns-are-better-than-radians [](https://substackcdn.com/image/fetch/$s_!mvXW!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F20e598bd-cac0-4b10-8222-b432167c9159_5616x3744.jpeg) 曾几何时,人们费尽心力说服大众用"tau" \(6.28318...\) 的近似值替代"pi" \(3.14159...\) 的近似值。众多博客文章和YouTube视频 (https://www.youtube.com/results?search_query=tau+pi) 传递的理念是:常见公式因此变得更简洁,且使用描述*完整*圆周而非*半*圆周的常数更便于计算。 总体而言,我赞同此观点。虽然这是个次要细节,但值得强调。若将pi替换为tau,大多数代码确实会稍有改善。 然而,在这场喧嚣中,一个影响更深远的机会被忽视了。多数情况下,我们可以*完全移除*pi,而非用tau替换它。 首先,考虑*pi*和*tau*在代码中的常见用途:为三角函数调用进行弧度转换。如果你曾使用这些常数,你所写的绝大部分代码可能类似这样: ``` y = center.y + (center.y * Math::sin(h * Math_TAU) * s) - (cursor->get_height() / 2); ``` 这并非我构造的例子——而是我随意打开GitHub上的Godot引擎 (https://godotengine.org/) 源码并搜索"tau"的结果。上述代码片段 (https://github.com/godotengine/godot/blob/61c0cb712d854881def4f266ca419de8c6fc6f37/scene/gui/color_picker.cpp#L786) 及数十处类似用法便是搜寻结果。 这并非Godot特例。若你打开任意游戏引擎代码库,执行相同搜索都会看到相同类型的用法。 请注意其中的奥秘:程序员手中已有在[0,1]区间周期性变化的值*h*,却因为要调用sin函数而*乘以*tau。 若仅看到表层,这似乎合情合理。但*sin的*实现*方式如何? sin有多种实现,但无论查看哪一种,在函数入口点附近都会看到类似这样的代码: ``` _PS256_CONST(cephes_FOPI, 1.27323954473516); ... y = _mm256_mul_ps(x, *(v8sf*)_ps256_cephes_FOPI); ``` 再次声明,这非我杜撰——它源自这个广为引用的AVX2 sin实现 (https://github.com/reyoung/avx_mathfun/blob/be617bbcf66993c4f7e7d267ed9926cd8e7d4f02/avx_mathfun.h#L346)。这不奇特也不反常——几乎每个快速三角函数库都会做类似操作。 这行代码的作用是什么?它将输入乘以常数1.27323954473516。 该值恰好等于*4/pi*。 因此调用方代码执行的操作是: ``` sin(h * 2 * pi) ``` 但库代码立即执行: ``` y = (4 / pi) * x ``` 这意味着调用方代码特意乘以pi*仅仅是为了让库代码能立即将其除回*。这本质上是毫无意义的弧度转换再转换。若双方程序员约定不使用弧度,而是沿用*h*原有的[0,1]定义域,双方工作都会简化:调用方节省一次乘法,库则获得更易理解且*精确*的常数。 "精确"这部分尤其有趣。除了徒增乘法开销外,还需注意:除0外,所有常见弧度值都*难以精确表示*。想用弧度存储90度?无论使用多少位,它永远无法精确表示。 而在[0,1]区间,90度仅是0.25——这个位模式*甚至不需要任何尾数位*!0.5同理!0.75仅需*一位*尾数即可精确表示。 因此[0,1]范围不仅比弧度更高效,在表示实践中常见典型值时也更紧凑精确。 我能理解部分人对此转换的顾虑。即便你相信我所说的——所有用户侧代码乘以pi或tau,而所有库侧代码立即将其除回,你仍可能产生"数学课式的不安",觉得放弃弧度似乎做错了事。 但数学从未规定正弦余弦必须接受弧度参数! 将圆周参数化从0到1(而非0到tau)并非我为本文随意编造的想法。这是个合理的、既存的数学构造,甚至有专门名称:称为*圈数 (turn)* (https://en.wikipedia.org/wiki/Turn_(angle)#:~:text=A%20turn%20is%20a%20unit,rot.)%20or%20full%20circle.)。 在*圈数*单位中,0对应0度,0.5对应180度,1对应360度,2对应720度,依此类推。这正是我们所需。 若担心数学老师生气,大可不必。只需告诉他们:你经过深思熟虑,决定对当前问题采用*圈数*而非弧度进行角度参数化是最高效的方法! 如果你编写了自己的数学库,或复制了他人库到项目中,希望接下来的内容能清晰展示如何从弧度切换并消除代码中的pi和tau。你只需修改sin/cos函数,使其接受*圈数*而非弧度——这通常只需调整单个常数。 若要支持旧代码,可为新的圈数三角函数另命名。对于旧代码,仍可通过包装函数支持基于弧度的sin/cos,在调用过程中进行圈数转换。 转换过程非常简单——仅需几行代码。 然而,尽管我认为圈数是最便捷的重参数化方式,它并非唯一选择。尤其当*你*没有自定义数学库时(即便有自定义库),或许可以考虑使用*半圈数*,即完整圆周对应[0,2]区间。这稍显复杂,但... 事实上(双关!)若留心寻找,在某些数学库中你会发现基于半圈数而非弧度的sin/cos函数。例如,CUDA sincospi内建函数 (https://docs.nvidia.com/cuda/cuda-math-api/group__CUDA__MATH__SINGLE.html#group__CUDA__MATH__SINGLE_1gab8978300988c385e0aa4b6cba44225e) 会计算输入值*乘以pi*(即半圈数)的正弦余弦值。 这很实用。若目标平台已提供sincospi函数,你现在就可停止在代码中使用pi和tau常数,且无需修改库代码。只需改用sincospi传递半圈数,替代原先基于弧度的sin/cos调用即可。 在管理过停止使用弧度的整个代码库后,我可以明确表示:我从未怀念过它们。所有多余的tau和pi消失了,代码可读性大幅提升。 修改sin/cos的逻辑同样适用于其他标准三角函数,因此你可选择在所有地方消除弧度。库通常会在内部进行弧度转换,输出时再转换回弧度,因此全面改用圈数或半圈数通常只需*删除*相关代码,别无他法。

相似文章

尝试使循环自动向量化

Hacker News Top

本文讨论了在代码循环中实现自动向量化的尝试,重点关注优化技术以提升软件性能。

当编译器让你惊喜

Lobsters Hottest

Matt Godbolt 探讨了编译器优化如何将 O(n) 求和循环转换为 O(1) 的闭式解,突出了 Clang 和 GCC 如何采用循环展开和数学简化等复杂技术来大幅提升代码性能。

当浮点数除法胜过整数除法

Lobsters Hottest

一篇博客文章,解释了一个反直觉的优化现象:在现代CPU上,使用浮点数除法(DIVSD)比整数除法(IDIVQ)性能更佳,并附有基准测试和汇编分析。

信任你的编译器:现代C++

Hacker News Top

本文对比了旧的C++性能技巧与现代编译器的能力,表明编译器现在能够将朴素代码优化得比手工调整的技巧更好。包含在AMD Zen 5上使用Clang 21的基准测试。