【问题标题】:Calculating vs. lookup tables for sine value performance?计算正弦值性能与查找表?
【发布时间】:2026-01-31 14:00:01
【问题描述】:

假设您必须计算域介于 0.01 和 360.01 之间的正弦(余弦或正切 - 随便)。 (使用 C#)

什么会更高效?

  1. 使用 Math.Sin
  2. 使用具有预先计算值的查找数组

我预计在给定域的情况下,选项 2 会快得多。在域的精度(0.0000n)中,计算性能超过查找的哪一点。

【问题讨论】:

  • 在这里计算 360000 个不同的 sinus 值大约需要 35 毫秒......在单个线程上。 (AMD Quadcore 3 Ghz。)您是否认为这是一个性能问题?您能否将工作分散到多个线程(和 CPU 内核)上?
  • 应用程序是不平凡的数学......我们需要实现光谱信号的傅立叶变换 - 例如核磁共振。可能有 10^5-10^8 次测量……多次重新计算相同的值似乎很浪费……
  • 这里有一个类似的问题,还有更多选择:*.com/questions/1164492/sine-table-interpolation
  • @mson:很高兴你问了这个问题。我有一个 Windows Mobile 应用程序,它对 WAV 文件执行 FFT 以呈现特定类型的可视化,看起来使用基于数组的 Sin 函数近似值将大大加快此操作(请参阅我关于在 WinMo 设备上进行测试)。像这样为音频目的进行近似是行不通的(它会产生太多噪音),但为了可视化目的,它是完美的。
  • @MusiGenesis,我的正弦逼近不使用LUT,可以内联完成,通常足够快,最高噪声峰值低于信号70dB。它是为音频设计的。当然,不保证它会起作用。 :-) musicdsp.org/showArchiveComment.php?ArchiveID=241

标签: c# performance math signal-processing


【解决方案1】:

Math.Sin 更快。写作的人很聪明,在准确和快速时使用表格查找,在更快时使用数学。并且该域并没有使其特别快,大多数 trig 函数实现所做的第一件事就是映射到一个有利的域。

【讨论】:

  • 一个包含 36000 个可能值的域与包含 3600000000000000 个值的域有很大不同。
  • 并非每种情况都需要相同的精度。编写函数的人很聪明,但并不神奇。
  • 啊,我明白了,您说的是域的精度,而不是范围。
  • 嗯,就是Domain的精度,而不是Domain的范围。
【解决方案2】:

对于性能问题,唯一正确的答案是您在测试后得出的答案。但是,在测试之前,您需要确定测试的努力是否值得您花时间 - 这意味着您已经发现了性能问题。

如果您只是好奇,可以轻松编写一个测试来比较速度。但是,您需要记住,为查找表使用内存会影响较大应用程序中的分页。因此,即使在您的小型测试中分页速度更快,它也可能会在使用更多内存的大型应用程序中减慢速度。

【讨论】:

    【解决方案3】:

    过去,数组查找是执行快速三角计算的一个很好的优化。

    但由于缓存命中、内置数学协处理器(使用表查找)和其他性能改进,最好自己确定特定代码的时间以确定哪个性能更好。

    【讨论】:

    • 我猜查找所需的处理比实际计算 sin 值要少得多。您确定计算 sin(90.00001) 比从小数组中读取 sin(90.0) 为 0 更快吗?先验 - 这似乎是胡扯......
    • 我曾经一直使用记忆化(预先计算的查找表)来加速图形例程(大量的正弦/余弦)。当他们将数学协处理器添加到 CPU(使用表查找)时,计算都可以在硬件中完成,不再是问题。现在,有了板载缓存,更小的代码块可以显着提升性能。如果用于存储表的内存导致缓存未命中,则性能损失可能很大。这不再是一个明确的问题。您几乎必须测试您的特定代码以找出答案。
    • 对于我的 DSP 用途,内置 sin 只是我在初始化代码中的选择,从不在运行时选择。相反,我使用 LUT 和各种近似值。我必须为每个应用程序决定哪个是更好的选择。
    • @Nosredna - 是的。如果我有一组有限的、定义明确的数字,可以在初始化时预先计算,那么将它们放在查找表中以获得更快的运行时性能通常是一个不错的选择。为您的实际应用程序计时是关键。
    • @henk 大声笑 - 所以我问这个问题的原因是为了得到遇到这个问题的人的回应,而不是猜测......我解释说“去测量它”的答案因为 - 我对这件事没有什么想法或经验。罗伯特回答的有用部分是他以前做过这种类型的计算,而且它可能会以任何一种方式进行。
    【解决方案4】:

    更新:通读到底。看起来查找表毕竟比 Math.Sin 快。

    我猜查找方法会比 Math.Sin 更快。我还会说它会快很多,但罗伯特的回答让我觉得我仍然想确定这个基准。我做了很多音频缓冲区处理,我注意到这样的方法:

    for (int i = 0; i < audiodata.Length; i++)
    {
        audiodata[i] *= 0.5; 
    }
    

    执行速度明显快于

    for (int i = 0; i < audiodata.Length; i++)
    {
        audiodata[i] = Math.Sin(audiodata[i]);
    }
    

    如果 Math.Sin 和简单乘法之间的差异很大,我猜 Math.Sin 和查找之间的差异也会很大。

    不过,我不知道,我的装有 Visual Studio 的计算机在地下室,我太累了,无法花 2 分钟时间来确定这一点。

    更新:好的,测试这个需要超过 2 分钟(更像是 20 分钟),但看起来 Math.Sin 的速度至少是查找表的两倍(使用字典)。这是使用 Math.Sin 或查找表执行 Sin 的类:

    public class SinBuddy
    {
        private Dictionary<double, double> _cachedSins
            = new Dictionary<double, double>();
        private const double _cacheStep = 0.01;
        private double _factor = Math.PI / 180.0;
    
        public SinBuddy()
        {
            for (double angleDegrees = 0; angleDegrees <= 360.0; 
                angleDegrees += _cacheStep)
            {
                double angleRadians = angleDegrees * _factor;
                _cachedSins.Add(angleDegrees, Math.Sin(angleRadians));
            }
        }
    
        public double CacheStep
        {
            get
            {
                return _cacheStep;
            }
        }
    
        public double SinLookup(double angleDegrees)
        {
            double value;
            if (_cachedSins.TryGetValue(angleDegrees, out value))
            {
                return value;
            }
            else
            {
                throw new ArgumentException(
                    String.Format("No cached Sin value for {0} degrees",
                    angleDegrees));
            }
        }
    
        public double Sin(double angleDegrees)
        {
            double angleRadians = angleDegrees * _factor;
            return Math.Sin(angleRadians);
        }
    }
    

    这是测试/计时代码:

    SinBuddy buddy = new SinBuddy();
    
    System.Diagnostics.Stopwatch timer = new System.Diagnostics.Stopwatch();
    int loops = 200;
    
    // Math.Sin
    timer.Start();
    for (int i = 0; i < loops; i++)
    {
        for (double angleDegrees = 0; angleDegrees <= 360.0; 
            angleDegrees += buddy.CacheStep)
        {
            double d = buddy.Sin(angleDegrees);
        }
    }
    timer.Stop();
    MessageBox.Show(timer.ElapsedMilliseconds.ToString());
    
    // lookup
    timer.Start();
    for (int i = 0; i < loops; i++)
    {
        for (double angleDegrees = 0; angleDegrees <= 360.0;
            angleDegrees += buddy.CacheStep)
        {
            double d = buddy.SinLookup(angleDegrees);
        }
    }
    timer.Stop();
    MessageBox.Show(timer.ElapsedMilliseconds.ToString());
    

    使用 0.01 度的步长值并在整个值范围内循环 200 次(如本代码所示)使用 Math.Sin 大约需要 1.4 秒,使用字典查找表大约需要 3.2 秒。将步长值降低到 0.001 或 0.0001 会使查找对 Math.Sin 的性能更差。此外,这个结果更倾向于使用 Math.Sin,因为 SinBuddy.Sin 会在每次调用时将角度(度)转换为弧度(弧度),而 SinBuddy.SinLookup 只是进行直接查找。

    这是在便宜的笔记本电脑上(没有双核或任何花哨的东西)。罗伯特,你这个男人! (但我仍然认为我应该得到支票,因为我做了这项工作)。

    更新 2:事实证明,停止和重新启动秒表并不会重置经过的毫秒数,因此查找速度似乎只有一半,因为它的时间包括 Math.Sin 调用的时间.另外,我重新阅读了这个问题,并意识到您正在谈论将值缓存在一个简单的数组中,而不是使用字典。这是我修改后的代码(我将保留旧代码作为对后代的警告):

    public class SinBuddy
    {
        private Dictionary<double, double> _cachedSins
            = new Dictionary<double, double>();
        private const double _cacheStep = 0.01;
        private double _factor = Math.PI / 180.0;
    
        private double[] _arrayedSins;
    
        public SinBuddy()
        {
            // set up dictionary
            for (double angleDegrees = 0; angleDegrees <= 360.0; 
                angleDegrees += _cacheStep)
            {
                double angleRadians = angleDegrees * _factor;
                _cachedSins.Add(angleDegrees, Math.Sin(angleRadians));
            }
    
            // set up array
            int elements = (int)(360.0 / _cacheStep) + 1;
            _arrayedSins = new double[elements];
            int i = 0;
            for (double angleDegrees = 0; angleDegrees <= 360.0;
                angleDegrees += _cacheStep)
            {
                double angleRadians = angleDegrees * _factor;
                //_cachedSins.Add(angleDegrees, Math.Sin(angleRadians));
                _arrayedSins[i] = Math.Sin(angleRadians);
                i++;
            }
        }
    
        public double CacheStep
        {
            get
            {
                return _cacheStep;
            }
        }
    
        public double SinArrayed(double angleDegrees)
        {
            int index = (int)(angleDegrees / _cacheStep);
            return _arrayedSins[index];
        }
    
        public double SinLookup(double angleDegrees)
        {
            double value;
            if (_cachedSins.TryGetValue(angleDegrees, out value))
            {
                return value;
            }
            else
            {
                throw new ArgumentException(
                    String.Format("No cached Sin value for {0} degrees",
                    angleDegrees));
            }
        }
    
        public double Sin(double angleDegrees)
        {
            double angleRadians = angleDegrees * _factor;
            return Math.Sin(angleRadians);
        }
    }
    

    还有测试/计时码:

    SinBuddy buddy = new SinBuddy();
    
    System.Diagnostics.Stopwatch timer = new System.Diagnostics.Stopwatch();
    int loops = 200;
    
    // Math.Sin
    timer.Start();
    for (int i = 0; i < loops; i++)
    {
        for (double angleDegrees = 0; angleDegrees <= 360.0; 
            angleDegrees += buddy.CacheStep)
        {
            double d = buddy.Sin(angleDegrees);
        }
    }
    timer.Stop();
    MessageBox.Show(timer.ElapsedMilliseconds.ToString());
    
    // lookup
    timer = new System.Diagnostics.Stopwatch();
    timer.Start();
    for (int i = 0; i < loops; i++)
    {
        for (double angleDegrees = 0; angleDegrees <= 360.0;
            angleDegrees += buddy.CacheStep)
        {
            double d = buddy.SinLookup(angleDegrees);
        }
    }
    timer.Stop();
    MessageBox.Show(timer.ElapsedMilliseconds.ToString());
    
    // arrayed
    timer = new System.Diagnostics.Stopwatch();
    timer.Start();
    for (int i = 0; i < loops; i++)
    {
        for (double angleDegrees = 0; angleDegrees <= 360.0;
            angleDegrees += buddy.CacheStep)
        {
            double d = buddy.SinArrayed(angleDegrees);
        }
    }
    timer.Stop();
    MessageBox.Show(timer.ElapsedMilliseconds.ToString());
    

    这些结果完全不同。使用 Math.Sin 大约需要 850 毫秒,Dictionary 查找表大约需要 1300 毫秒,而基于数组的查找表大约需要 600 毫秒。 所以看起来(正确编写的 [gulp])查找表实际上比使用 Math.Sin 快一点,但不会快很多。

    请自己验证这些结果,因为我已经证明了我的无能。

    【讨论】:

    • 不只是懒惰——下面的猫砂盆也满了。虽然我想这只是我的更多懒惰。
    • 大声笑 - 这就是你娶妻的原因......要么她会为你做这件事,要么她会唠叨你做这件事......
    • 这应该是选择的答案。
    • 好的,我确实运行了你的基准测试,但没有所有的谷壳:没有单独的类来封装查找表,没有方法调用,没有从 double 到 int 的转换等。然后,带有表查找的基准测试需要15 毫秒,基准计算正弦波需要 415 毫秒(在 3.0 Ghz Pentium III 上)。因此,您的基准测试的问题在于它测量了很多开销。我的基准测试的问题(正如其他人已经指出的那样)是在整个基准测试期间,查找表很好地位于缓存中。这两个基准都太简单了。
    • @Accipitridae,干得好。这就是为什么如果您需要速度,您需要编写两次代码并原位进行测试。基准只能给出一个提示。我在现实世界的 dsp 应用程序中的经验(我的代码是在主机中运行的 dll,报告我正在使用的 %time)是库 sin() 不可用,但它总是值得测试以确保。跨度>
    【解决方案5】:

    这个问题的答案完全取决于查找表中有多少值。您说“域在 0.01 和 360.01 之间”,但您没有说明可以使用该范围内的多少个值,或者您需要的答案有多准确。 请原谅我没想到会看到显着用于在非科学环境中传达隐含含义的数字。

    仍然需要更多信息来回答这个问题。 0.01 和 360.01 之间的值的预期分布是什么?除了简单的 sin( ) 计算之外,您还在处理大量数据吗?

    36000 个双精度值占用内存超过 256k;查找表太大而无法放入大多数机器上的 L1 缓存中;如果您直接在表中运行,则每次 sizeof(cacheline)/sizeof(double) 访问都会错过 L1 一次,并且可能会命中 L2。另一方面,如果您的表访问或多或少是随机的,那么您几乎每次进行查找时都会丢失 L1。

    这也很大程度上取决于您所在平台的数学库。例如,sin 函数的常见 i386 实现范围从 ~40 个周期到 400 个周期甚至更多,具体取决于您的确切微架构和库供应商。我还没有为 Microsoft 库计时,所以我不知道 C# Math.sin 实现的确切位置。

    由于在一个健全的平台上,来自 L2 的加载通常快于 40 个周期,因此人们有理由期望查找表在单独考虑时会更快。但是,我怀疑您是在孤立地计算 sin( ) ;如果你对 sin() 的论点越过整个表格,你就会将计算的其他步骤所需的其他数据从缓存中吹出;尽管 sin( ) 计算变得更快,但计算其他部分的减速可能超过加速。只有仔细测量才能真正回答这个问题。

    我是否从您的其他 cmets 中了解到您正在将其作为 FFT 计算的一部分?您是否有理由需要推出自己的 FFT,而不是使用已经存在的众多极高质量实现中的一种?

    【讨论】:

    • 这里有一个关于有效数字的链接...en.wikipedia.org/wiki/Significant_figures
    • 我也不认为有效数字对问题有任何意义。在编程上下文中,除非另有说明,否则数字的精度由其类型决定。
    【解决方案6】:

    既然您提到傅立叶变换是一个应用程序,您也可以考虑使用方程式计算正弦/余弦

    sin(x+y) = sin(x)cos(y) + cos(x)sin(y)

    cos(x+y) = cos(x)cos(y) - sin(x)sin(y)

    即您可以从 sin((n-1) * x), cos((n-1) * x) 迭代计算 n = 0, 1, 2 ... 的 sin(n * x), cos(n * x)和常数 sin(x), cos(x) 4 次乘法。 当然,只有在等差数列上计算 sin(x)、cos(x) 时才有效。

    在没有实际实现的情况下比较这些方法是很困难的。这在很大程度上取决于您的表与缓存的匹配程度。

    【讨论】:

    【解决方案7】:

    由于您的查找表中可能有数千个值,您可能想要做的是有一个字典,当您计算一个值时,将其放入字典中,因此您只计算每个值一次,并使用进行计算的 C# 函数。

    但是,没有理由一遍又一遍地重新计算相同的值。

    【讨论】:

    • 你必须小心。在某些情况下,字典查找可能比 sin 计算慢。
    • 了解的唯一方法是通过分析查看问题开始出现的位置。例如,如果您使用的是 WindowsCE,那么您可能会发现 sin 计算要慢得多,但没有一种解决方案适用于所有硬件。
    • 在某些系统上,字典可以击败库 sin(),但很难想象它会击败数组,除非数组被实现为字典。同意您必须实施并确定时间。
    • @Nosrenda,关于您对此答案的第一条评论:您还必须小心,因为您的正弦计算器可能比散列函数慢......评论的意义何在?
    • 我的观点是,您不能假设避免使用字典进行计算会加快速度。您必须尝试并测试它。说“没有理由一遍又一遍地重新计算相同的值”并不一定正确,因为有时这样做会更快。
    【解决方案8】:

    抱歉挖坟,但是有一个很好的解决方案可以快速索引查找表: https://jvm-gaming.org/t/fast-math-sin-cos-lookup-tables/36660

    它是用 Java 编写的,但只需几分钟即可将其移植到 C#。 我做了测试,经过 100000 次迭代得到了以下结果:

    Math.Sin: 0.043 sec
    Mathf.Sin: 0.06 sec (Unity`s Mathf lib)
    MathTools.Sin: 0.026 (lookup tables static class).
    

    可能在 Java 中它会提供 50 倍的提升(或者在 2011 年,哈哈,但在 2021 年的 C# 中,差异只有大约 2 倍)。

    【讨论】: