正弦值性能的计算与查找表?

mso*_*son 18 c# math performance signal-processing

假设您必须计算域在0.01到360.01之间的正弦(余弦或正切 - 无论如何).(使用C#)

什么会更高效?

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

我会反驳说,鉴于域名,选项2会快得多.在域精度(0.0000n)的什么时刻,计算的性能超过了查找.

Mus*_*sis 20

更新:直读到最后.看起来查找表比Math.Sin更快.

我猜测查找方法会比Math.Sin更快.我也说,这将是一个很大更快,但罗伯特的回答让我觉得,我还是会想基准对此予以肯定.我做了很多音频缓冲处理,我注意到这样的方法:

for (int i = 0; i < audiodata.Length; i++)
{
    audiodata[i] *= 0.5; 
}
Run Code Online (Sandbox Code Playgroud)

将执行速度明显快于

for (int i = 0; i < audiodata.Length; i++)
{
    audiodata[i] = Math.Sin(audiodata[i]);
}
Run Code Online (Sandbox Code Playgroud)

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

不过,我不知道,我的Visual Studio电脑在地下室,我太累了,不能花2分钟来确定这一点.

更新:好的,测试它花了超过2分钟(更像是20),但看起来Math.Sin的速度至少是查找表的两倍(使用Dictionary).这是使用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);
    }
}
Run Code Online (Sandbox Code Playgroud)

这是测试/计时代码:

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());
Run Code Online (Sandbox Code Playgroud)

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

这是一台便宜的笔记本电脑(没有双核或任何花哨的东西).罗伯特,你这个男人!(但我仍然认为我应该接受检查,因为我做了工作).

更新2:好的,我彻底迟钝了.事实证明停止并重新启动秒表并没有重置经过的毫秒,所以查找只有一半的速度,因为它的时间包括Math.Sin调用的时间.此外,我重读了这个问题,并意识到你正在谈论在一个简单的数组中缓存值,而不是使用Dictionary.这是我修改过的代码(我将旧代码留作后代警告):

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

测试/定时代码:

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());
Run Code Online (Sandbox Code Playgroud)

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

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

  • 好吧,我确实运行了你的基准测试,但是没有所有的条件:没有单独的类来封装查找表,没有方法调用,没有从double到int的转换等.带表查找的基准测试需要15ms,基准计算正弦需要415ms (在3.0 Ghz Pentium III上).因此,您的基准测试的问题在于它会测量大量开销.我的基准测试问题(正如其他人已经指出的那样)是在整个基准测试期间,查找表很好地位于缓存中.这两个基准都太简单了. (3认同)
  • 这应该是选定的答案. (2认同)

Rob*_*ino 15

过去,数组查找是执行快速触发计算的一个很好的优化.

但是对于缓存命中,内置数学协处理器(使用表查找)和其他性能改进,最好自己计算特定代码以确定哪些代码会更好.

  • 我曾经一直使用memoization(tabling)*来加速图形例程(大量的正弦/余弦).当他们向CPU(使用表查找)添加数学协处理器时,计算可以全部在硬件中完成,而不是问题.现在,通过板载缓存,较小的代码块可以显着提升性能.如果用于存储表的内存导致缓存未命中,则性能损失可能很大.这不再是一个明确的问题了.您几乎*有*来测试您的特定代码以找出答案. (8认同)
  • 我猜想查找比实际计算sin值要少得多.你确定计算sin(90.00001)比从一个小数组读取sin(90.0)更快吗?先验 - 看起来像胡扯...... (2认同)
  • 梅森,请阅读以下答案要点:测量。 (2认同)

Joh*_*her 7

对于性能问题,唯一正确的答案是您在测试后达到的答案.但是,在测试之前,您需要确定测试的工作是否值得花时间 - 这意味着您已经确定了性能问题.

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