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

时间:2009-09-05 02:57:26

标签: c# performance math signal-processing

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

什么会更高效?

  1. 使用Math.Sin
  2. 使用具有预先计算值的查找数组
  3. 我认为,鉴于域名,选项2会更快。在域精度(0.0000n)的什么时刻,计算的性能超过了查找。

7 个答案:

答案 0 :(得分:20)

更新:直读到最后。看起来查找表比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的速度至少是查找表的两倍< / strong>(使用字典)。这是使用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调用的时间。此外,我重读了这个问题,并意识到你正在谈论在一个简单的数组中缓存值,而不是使用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);
    }
}

测试/计时代码:

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毫秒,字典查找表大约需要1300毫秒,基于数组的查找表大约需要600毫秒。 所以看起来(正确编写的[gulp])查找表实际上比使用Math.Sin 快一点,但不是很多。

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

答案 1 :(得分:15)

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

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

答案 2 :(得分:7)

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

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

答案 3 :(得分:2)

这个问题的答案完全取决于查找表中有多少个值。你说“域名介于0.01和360.01之间”,但是你没有说明该范围内可以使用多少个值,或者你需要答案的准确程度。原谅我不希望看到重要的用于在非科学背景下传达隐含意义的数字。

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

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

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

由于来自L2的负载通常比理智平台上的40个周期快,因此可以合理地期望查找表可以更快地被隔离考虑。但是,我怀疑你是在孤立地计算罪恶();如果sin()的参数跳到表中,那么你将从缓存中将计算的其他步骤所需的其他数据吹掉;虽然sin()计算变得更快,但计算其他部分的减速可能超过加速。只有仔细的测量才能真正回答这个问题。

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

答案 4 :(得分:2)

由于您提到傅里叶变换作为应用程序,您可能还会考虑使用方程计算您的正弦/余弦

  

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)时才有效。

比较没有实际实施的方法很困难。这很大程度上取决于您的表格在缓存中的适用程度。

答案 5 :(得分:1)

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

答案 6 :(得分:0)

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

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