关于c#:正弦值性能的计算与查找表?

Calculating vs. lookup tables for sine value performance?

假设你必须计算出正弦波(余弦或正切——无论什么),其中的域在0.01和360.01之间。(用C语言)

什么会更有效?

  • 使用数学罪
  • 使用具有预计算值的查找数组
  • 我会预料到,给定域,选项2会更快。在域精度(0.0000n)中的哪个点,计算的性能是否超过查找。


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

    我想查找方法会比math.sin更快。我也会说这会快得多,但罗伯特的回答让我觉得我仍然希望以此作为基准来确定。我做了很多音频缓冲处理,我注意到这样的方法:

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

    执行速度明显快于

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

    如果math.sin和简单乘法之间的差异很大,我想math.sin和lookup之间的差异也很大。

    不过,我不知道,我的带Visual Studio的电脑在地下室,我太累了,无法用2分钟来确定这个问题。

    更新:好的,测试这个花了2分钟多的时间(大约20分钟),但看起来像数学。sin的速度至少是查找表(使用字典)的两倍。下面是使用math.sin或查找表执行sin的类:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    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);
        }
    }

    下面是测试/计时代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    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.sin lookup只做一个直接的查找。

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

    更新2:好吧,我是个白痴…结果是停止和重新启动秒表并没有重置经过的毫秒数,所以查找的速度只有原来的一半,因为时间包括了Math.sin调用的时间。另外,我重读了这个问题,意识到您所说的是将值缓存在一个简单的数组中,而不是使用字典。这是我修改的代码(我将保留旧代码作为对后代的警告):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    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);
        }
    }

    测试/计时代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    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快一点,但不是很多。

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


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

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


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

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


    既然你提到傅立叶变换作为一个应用,你也可以考虑用这些方程来计算你的正弦/余弦。

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

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

    也就是说,你可以计算sin(n*x),cos(n*x),n=0,1,2…从sin((n-1)*x)、cos((n-1)*x)和常量sin(x)、cos(x)迭代4次乘法。当然,只有当你必须在一个算术序列上计算sin(x),cos(x)的时候,这才有效。

    比较没有实际实施的方法是困难的。这很大程度上取决于表在缓存中的容纳程度。


    答案完全取决于查找表中有多少值。您会说"域在0.01和360.01之间",但您不会说该范围内可能使用了多少值,或者您需要的答案有多精确。原谅我,因为不希望看到用于在非科学上下文中传达隐含含义的有效数字。

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

    36000个双精度值占用了256K以上的内存;查找表太大,在大多数计算机上无法容纳在一级缓存中;如果直接在表中运行,则在每个sizeof(cacheline)/sizeof(double)访问中可能会错过一次l1,并且可能会碰到l2。另一方面,如果您的表访问或多或少是随机的,那么几乎每次进行查找时都会丢失l1。

    它还很大程度上取决于您所处平台的数学库。例如,sin函数的常见i386实现范围从约40个周期到400个周期甚至更多,这取决于您的精确微体系结构和库供应商。我还没有对Microsoft库进行计时,因此我不知道C Math.sin实现将落在何处。

    由于在一个健全的平台上,来自l2的负载通常比40个周期要快,因此我们可以合理地期望在孤立的情况下更快地考虑查找表。但是,我怀疑您是在孤立地计算sin();如果您对sin()的参数跳到表中,您将把其他计算步骤所需的其他数据从缓存中吹走;尽管sin()计算速度更快,但对其他计算部分的减速可能会超过加速。只有仔细的测量才能真正回答这个问题。

    从你的其他评论中,我能理解你是在做FFT计算的一部分吗?是否有理由需要滚动自己的fft,而不是使用已经存在的众多非常高质量的实现之一?


    数学。罪恶更快。写东西的人很聪明,在准确和快速的时候使用表格查找,在快速的时候使用数学。而且这个域没有什么特别快的地方,大多数trig函数实现所做的第一件事就是映射到一个有利的域。


    因为查阅表格中可能有数千个值,所以您可能需要使用字典,当您计算值时,请将其放入字典中,这样您只需计算一次每个值,并使用C函数进行计算。

    但是,没有理由反复重新计算相同的值。