浮点数除法/乘法比加法/减法慢30倍以上?

4
我最近读了这篇帖子:现代硬件上的浮点数与整数计算,对于我的处理器在这个准基准测试中的性能很感兴趣,所以我编写了两个版本的代码,一个是C#,一个是C++(Visual Studio 2010 Express),并且都进行了优化编译,看看结果如何。我的C#版本的输出相当合理:
int add/sub: 350ms
int div/mul: 3469ms
float add/sub: 1007ms
float div/mul: 67493ms
double add/sub: 1914ms
double div/mul: 2766ms

当我编译并运行C++版本时,出现了完全不同的情况:
int add/sub: 210.653ms
int div/mul: 2946.58ms
float add/sub: 3022.58ms
float div/mul: 172931ms
double add/sub: 1007.63ms
double div/mul: 74171.9ms

我预料到会有一些性能差异,但没有想到会这么大!我不明白为什么C++中的除法/乘法比加法/减法要慢这么多,而托管的C#版本对我的期望更合理。以下是C++版本函数的代码:
template< typename T> void GenericTest(const char *typestring)
{
    T v = 0;
    T v0 = (T)((rand() % 256) / 16) + 1;
    T v1 = (T)((rand() % 256) / 16) + 1;
    T v2 = (T)((rand() % 256) / 16) + 1;
    T v3 = (T)((rand() % 256) / 16) + 1;
    T v4 = (T)((rand() % 256) / 16) + 1;
    T v5 = (T)((rand() % 256) / 16) + 1;
    T v6 = (T)((rand() % 256) / 16) + 1;
    T v7 = (T)((rand() % 256) / 16) + 1;
    T v8 = (T)((rand() % 256) / 16) + 1;
    T v9 = (T)((rand() % 256) / 16) + 1;

    HTimer tmr = HTimer();
    tmr.Start();
    for (int i = 0 ; i < 100000000 ; ++i)
    {
        v += v0;
        v -= v1;
        v += v2;
        v -= v3;
        v += v4;
        v -= v5;
        v += v6;
        v -= v7;
        v += v8;
        v -= v9;
    }
    tmr.Stop();

      // I removed the bracketed values from the table above, they just make the compiler
      // assume I am using the value for something do it doesn't optimize it out.
    cout << typestring << " add/sub: " << tmr.Elapsed() * 1000 << "ms [" << (int)v << "]" << endl;

    tmr.Start();
    for (int i = 0 ; i < 100000000 ; ++i)
    {
        v /= v0;
        v *= v1;
        v /= v2;
        v *= v3;
        v /= v4;
        v *= v5;
        v /= v6;
        v *= v7;
        v /= v8;
        v *= v9;
    }
    tmr.Stop();

    cout << typestring << " div/mul: " << tmr.Elapsed() * 1000 << "ms [" << (int)v << "]" << endl;
}

C#测试的代码不是通用的,实现方式如下:
static double DoubleTest()
{
    Random rnd = new Random();
    Stopwatch sw = new Stopwatch();

    double v = 0;
    double v0 = (double)rnd.Next(1, int.MaxValue);
    double v1 = (double)rnd.Next(1, int.MaxValue);
    double v2 = (double)rnd.Next(1, int.MaxValue);
    double v3 = (double)rnd.Next(1, int.MaxValue);
    double v4 = (double)rnd.Next(1, int.MaxValue);
    double v5 = (double)rnd.Next(1, int.MaxValue);
    double v6 = (double)rnd.Next(1, int.MaxValue);
    double v7 = (double)rnd.Next(1, int.MaxValue);
    double v8 = (double)rnd.Next(1, int.MaxValue);
    double v9 = (double)rnd.Next(1, int.MaxValue);

    sw.Start();
    for (int i = 0; i < 100000000; i++)
    {
        v += v0;
        v -= v1;
        v += v2;
        v -= v3;
        v += v4;
        v -= v5;
        v += v6;
        v -= v7;
        v += v8;
        v -= v9;
    }
    sw.Stop();

    Console.WriteLine("double add/sub: {0}", sw.ElapsedMilliseconds);
    sw.Reset();

    sw.Start();
    for (int i = 0; i < 100000000; i++)
    {
        v /= v0;
        v *= v1;
        v /= v2;
        v *= v3;
        v /= v4;
        v *= v5;
        v /= v6;
        v *= v7;
        v /= v8;
        v *= v9;
    }
    sw.Stop();

    Console.WriteLine("double div/mul: {0}", sw.ElapsedMilliseconds);
    sw.Reset();

    return v;
}

有什么想法吗?

你在C++中使用了哪些优化设置?你是在Visual Studio的测试主机中运行吗(时间似乎有点慢)? - Reed Copsey
它们在我看来也很慢。我是从命令行运行的,采用了完整程序优化、速度优化。操作系统是32位的Windows XP。 - Chris D.
当我尝试运行你的基准测试时,我得到了非常奇怪的结果,似乎完全是随机的。有一次我在doublefloat除法中得到了约200毫秒的时间,下一次可能多达7000毫秒。我将迭代次数减少了10倍,否则当它失控时,它会花费太长时间。这是在C#方面,在C++方面,我发现float加/减比C#慢3倍,而除法始终很慢,需要7秒以上。 - JulianR
@JulianR:这就是为什么我在这里发布了这个问题,我不太理解为什么会有奇怪的性能差异。 - Chris D.
5个回答

5

针对浮点数的div/mul测试,你可能会得到非规格化值,这些值比正常的浮点数处理速度慢得多。这对于整数测试来说不是问题,并且在双精度测试中会更晚出现。

你可以在C++开头添加以下代码将非规格化值清零:

_controlfp(_DN_FLUSH, _MCW_DN);

我不确定如何在C#中实现它(或者它是否可能)。

这里有更多信息: 浮点数运算执行时间


这解决了它。将那行代码加入 GenericTest 函数后,使其执行更加合理:浮点数加法 1 秒,浮点数乘除法 1.3 秒,双精度浮点数加法 1 秒,双精度浮点数乘除法 1.6 秒。 - Chris D.
我仍然不确定它是否是有缺陷的基准测试,但至少现在它更少有缺陷 :) - celion

3

很可能C#将除以vx的优化转换为乘以1 / vx,因为它知道这些值在循环期间不会被修改,并且可以提前计算出倒数。

您可以在C++中自己进行此优化并计时。


2
如果你对浮点速度和可能的优化感兴趣,可以阅读这本书:http://www.agner.org/optimize/optimizing_cpp.pdf 另外你也可以查看这个:Microsoft Visual C++ 浮点数优化 你的结果可能会受到诸如JIT、编译标志(调试/发布,执行哪种FP优化或允许的指令集)等因素的影响。
尝试将这些标志设置为最大优化,并更改你的程序,以确保不会产生溢出或NAN,因为它们会影响计算速度。 (即使像 v += v1; v += v2; v -= v1; v -= v2; 这样的操作也是可以的,因为在strictprecise浮点模式下不会被简化)。同时尽量不要使用超过你拥有的FP寄存器数量的变量。

1

乘法并不算差。我认为它比加法慢几个周期,但是是的,与其他操作相比,除法非常慢。它需要显著更长的时间,而且与其他三个操作不同,它不能进行流水线处理。


0

我还决定你的C++非常慢。所以我自己跑了一下。结果实际上,你完全错了。 fail

我用Windows高性能定时器替换了你的计时器(我不知道你使用的是哪个计时器,但我手头没有一个),那个东西可以做到纳秒级别或更好。猜猜看?Visual Studio不行。我甚至没有为最高性能进行任何调整。VS可以直接穿透这种垃圾代码并省略所有循环。这就是为什么你永远都不应该使用这种"剖析"。获取专业的剖析工具并回来。除非2010 Express与2010 Professional不同,但我怀疑。它们主要有IDE功能上的区别,而不是原始代码性能/优化。

我甚至不想去跑你的C#。

编辑:这是DEBUG x64(之前的屏幕是x86,但我想我应该做x64,因为我在x64上),我还修复了一个小错误,导致时间为负而不是正。所以,除非你告诉我你的32位发布FP慢了一百倍,否则我认为你已经搞砸了。 alt text

我发现有一件奇怪的事情是,x86调试程序从未在第二个浮点测试上终止,即如果您先执行float,然后执行double,则double div/mul会失败。如果您先执行double,然后执行float,则float div/mul会失败。可能是编译器故障。


@PoweRoy:阅读帖子。重点是编译器将所有内容都优化掉了。 - Puppy
@SoapBox:楼主已经发布了我的代码。坦白地说,我只是懒得打出结果。 - Puppy
@DeadMG 为什么第一次和第二次的时间不同?第一个只显示了0(你没有解释原因)。 - RvdK
@PoweRoy:...也许你可以看一下这篇文章。第二次运行是在DEBUG模式下进行的,也就是所有编译器优化都被禁用,而且还有额外的调试开销,即使如此,它的速度仍然比OP快100倍。 - Puppy
我在两种情况下都使用高性能计时器QueryPerformanceCounter。我从未让编译器优化循环,你设置了哪些编译器选项? - Chris D.
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接