为什么在这个例子中使用浮动使我比双打慢2倍?

时间:2009-05-14 11:35:54

标签: c# .net c++ optimization floating-point

我最近一直在做一些分析,我遇到了一个令我疯狂的案例。以下是一段不安全的C#代码,它基本上将源样本缓冲区复制到具有不同采样率的目标缓冲区。就像现在一样,它占每帧总处理时间的约0.17%。我没有得到的是,如果我使用浮动而不是双打,处理时间将提高到0.38%。有人可以解释一下这里发生了什么吗?

快速版(~17%)

double rateIncr = ...
double readOffset = ...
double offsetIncr = ...

float v = ... // volume

// Source and target buffers.
float* src = ...
float* tgt = ...

for( var c = 0; c < chunkCount; ++c)
{
    for( var s = 0; s < chunkSampleSize; ++s )
    {
        // Source sample            
        var iReadOffset = (int)readOffset;

        // Interpolate factor
        var k = (float)readOffset - iReadOffset;

        // Linearly interpolate 2 contiguous samples and write result to target.
        *tgt++ += (src[ iReadOffset ] * (1f - k) + src[ iReadOffset + 1 ] * k) * v;

        // Increment source offset.
        readOffset += offsetIncr;
    }
    // Increment sample rate
    offsetIncr += rateIncr;
}

慢速版(~38%)

float rateIncr = ...
float readOffset = ...
float offsetIncr = ...

float v = ... // volume

// Source and target buffers.
float* src = ...
float* tgt = ...

for( var c = 0; c < chunkCount; ++c)
{
    for( var s = 0; s < chunkSampleSize; ++s )
    {
        var iReadOffset = (int)readOffset;

        // The cast to float is removed
        var k = readOffset - iReadOffset;

        *tgt++ += (src[ iReadOffset ] * (1f - k) + src[ iReadOffset + 1 ] * k) * v;
        readOffset += offsetIncr;
    }
    offsetIncr += rateIncr;
}

奇数版(~22%)

float rateIncr = ...
float readOffset = ...
float offsetIncr = ...

float v = ... // volume

// Source and target buffers.
float* src = ...
float* tgt = ...

for( var c = 0; c < chunkCount; ++c)
{
    for( var s = 0; s < chunkSampleSize; ++s )
    {
        var iReadOffset = (int)readOffset;
        var k = readOffset - iReadOffset;

        // By just placing this test it goes down from 38% to 22%,
        // and the condition is NEVER met.
        if( (k != 0) && Math.Abs( k ) < 1e-38 )
        {
           Console.WriteLine( "Denormalized float?" );
        }

        *tgt++ += (src[ iReadOffset ] * (1f - k) + src[ iReadOffset + 1 ] * k) * v;
        readOffset += offsetIncr;
    }
    offsetIncr += rateIncr;
}

我现在所知道的是我什么都不知道

7 个答案:

答案 0 :(得分:4)

您是在64位还是32位处理器上运行它?我的经验是,在某些边缘情况下,如果对象的大小与寄存器的大小匹配,CPU可以使用这样的低级功能进行优化(即使您可以假设两个浮点数在64位中整齐地适合注册你可能仍然失去优化的好处)。如果在32位系统上运行它,您可能会发现情况发生逆转...

快速搜索和我能做的最好的事情是C ++游戏开发论坛上的一些帖子(我在游戏开发的一年中,我自己注意到了这一点,但那是唯一一次我正在分析这个级别)。 This post有一些有趣的反汇编结果来自C ++方法,可能适用于非常低的水平。


另一个想法:

来自MSDN的

This article涉及在.NET中使用浮点数的许多内部细节,主要是为了解决浮点数比较的问题。它有一个有趣的段落,总结了处理浮点值的CLR规范:

  

这个规范明确考虑到了x87   FPU。该规范基本上是这样说的   允许使用CLR实现   内部代表(在我们的   case,x87 80位表示)   只要没有明确的   存储到强制位置(一个类   或者值类型字段),那种力量   缩小。此外,在任何时候,IL   流可能有conv.r4和conv.r8   说明,这将迫使   缩小范围。

因此,当对它们执行操作时,您的浮动实际上可能不是浮点数,而是它们可能是x87 FPU上的80位数字或编译器可能认为是优化或计算准确性所需的任何其他内容。如果不查看IL,你肯定不会知道,但是当你使用浮动时,可能会有许多代价高昂的演员阵容。遗憾的是,您无法通过C ++中的fp开关定义C#中浮点运算所需的精度,因为这会阻止编译器在操作之前将所有内容放入更大的容器中。

答案 1 :(得分:3)

也许在一些占用CPU时间的地方发生了一系列双重浮动转换。你能用IL反汇编程序查看输出,看看它实际上在做什么吗?

答案 2 :(得分:2)

您的计算可能导致浮点值进入“非正常”状态,这在大多数x86处理器上效率非常低。非正规值非常小,以至于它们位于可能的最小浮点值的边缘。相比之下,这样的值可以很好地适应双倍范围,因此在这种情况下计算是有效的。

我不能确定这是否适用于您,但它肯定会解释您所看到的行为。

http://en.wikipedia.org/wiki/Denormal

答案 3 :(得分:1)

了解正在发生的事情的一种方法是在代码中闯入调试器并查看正在执行的实际x86指令。在不知道您的C#转换为机器代码的情况下,可能被建议的大部分原因只是猜测。即使看IL也可能不会告诉你很多。

如果执行此操作,您可能需要先启动程序,然后再附加调试程序,以便不禁用JIT优化。毕竟,你想确保你正在查看你实际要运行的代码。

答案 4 :(得分:1)

考虑到你的大部分代码是而不是处理你在双打和浮点数之间切换的3个变量,并且你在谈论性能的相当大的变化,我会说小的类型和测试的变化足以改变您的缓存占用空间和/或注册用法。

我在32位机器上做了一些快速测试:

// NOTE: runnable - copy in paste into your own project
class Program
    {
        static int endVal = 32768;
        static int runCount = 100;
        static void Main(string[] args)
        {
            Stopwatch doublesw = Stopwatch.StartNew();
            for (int i = 0; i < runCount; ++i)
                doubleTest();
            doublesw.Stop();
            Console.WriteLine("Double: " + doublesw.ElapsedMilliseconds);
            Stopwatch floatsw = Stopwatch.StartNew();
            for (int i = 0; i < runCount; ++i)
                floatTest();
            floatsw.Stop();
            Console.WriteLine("Float: " + floatsw.ElapsedMilliseconds);
            Console.ReadLine();
        }

        static void doubleTest()
        {
            double value = 0;
            double incr = 0.001D;

            while (value < endVal)
            {
                value += incr;
            }
        }

        static void floatTest()
        {
            float value = 0;
            float incr = 0.001f;

            while (value < endVal)
            {
                value += incr;
            }
        }
    }
}

结果是:

Double: 12897
Float: 10059

重复测试显示浮动具有超过双倍的明显优势。现在,这是一个小程序,所有这些变量都适合寄存器。

不幸的是,你提供的代码中有足够的缺少部分,我无法获得良好的编译和读取程序集以确切了解到底发生了什么,但从我的(快速)测试判断,这是我的答案。

(对我来说,赠品就是你的情况#3 - 添加代码会改变足迹和缓存模式 - 我已经在各种语言中看到过几次这种奇怪的感觉)

答案 5 :(得分:0)

double to float转换可能会慢下来:

(float)readOffset

尝试使readOffset浮动。

答案 6 :(得分:0)

关于您的分析的简短问题。所有你写的都是百分比值。那么总时间怎么样,功能需要??

如果你在你的函数浮点数和外部地方使用了一些双打,你需要一些时间进行转换,这意味着由于函数本身的处理时间是恒定的,因此内部函数本身的百分比时间会下降。整个过程需要更多时间。

希望我的写作有意义并且可以理解。但简而言之,如果整个过程需要更长的总时间,那么给定函数的百分比值(整个时间保持不变,由于它不会被更改)将会下降。