对于非常接近零的值,双重计算运行速度要慢得多

Posted

技术标签:

【中文标题】对于非常接近零的值,双重计算运行速度要慢得多【英文标题】:Double calculations running a lot slower for values very close to zero 【发布时间】:2013-10-12 10:17:45 【问题描述】:

一位朋友要求我分享我过去一段时间偶然发现的东西。原帖取自here。问题陈述可以在here 找到。基本上是一个算法竞赛的网站。

我遇到了一个算法问题,我使用以下代码解决了这个问题:

double dp[80002][50];
class FoxListeningToMusic 
public:
    vector <double> getProbabilities(vector <int> length, int T)      
        memset(dp, 0, sizeof(dp));
        int n = length.size();
        for(int i = 0; i < n; i++)
            dp[0][i] = 1.0 / (double)n;

        double mul = 1.0 / (double)n;
        int idx ;
        for(int i = 1; i <= T; i++) 
            for(int j = 0; j < n; j++)  
                idx = i - length[j];
                if(idx >= 0)  
                    for(int k = 0; k < n; k++)
                        dp[i][k] += mul * dp[idx][k];
                
                else
                    dp[i][j] += mul;
                
            
        

        vector<double> v(n);
        for(int i = 0; i < n; i++)
            v[i] = dp[T][i];
        return v;
    

;

代码是否以正确的答案解决问题并不重要,至少对于我将要讨论的内容而言。事实是我对这段代码有时间限制(这意味着它在某些测试用例中执行了超过 2 秒)。不知何故,因为这里的复杂性是 O(T * length.size() ^ 2),如果我们考虑到问题约束,它变成 2 * 108。然而,有趣的是我测试了我的解决方案,特别是针对时间限制。对于我的解决方案,我使用的情况似乎是“最坏情况”:长度为 50 1s,T = 80000。代码运行了 0.75 秒。这远远低于 2 秒的时间限制。

我说我使用的情况是最坏的情况,因为将执行的指令数量仅取决于内部 for 中的分支条件 idx >= 0。如果这是真的,将再执行一个循环(该循环的复杂度为 O(n))。在另一种情况下,只会执行一个操作 O(1)。正如您所看到的,长度越短的元素就越多。

即使这样推理,我的问题在测试以下案例后仍然失败:

length = 1, 1, 1, 1, 3, 3, 3, 3, 1, 3, 3, 2, 3, 2, 3, 3, 1, 2, 3, 1, 2, 3, 2,
          1, 3, 1, 1, 1, 2, 3, 2, 3, 2, 2, 1, 3, 1, 1, 3, 1, 3, 1, 3, 2, 3, 1,
          1, 3, 2, 76393 T= 77297.
For this case my program runs for 5.204000 seconds.

我的第一个假设是,运行时测量的这种意外比率的原因(只要我们应该预期在第一种情况下要执行的处理器指令要少得多)是处理器以某种方式缓存了类似的计算:在我的例如,计算对于长度的所有元素都是对称的,真正聪明的处理器可以使用它来避免重复相同的指令序列。所以我尝试编写另一个示例:这次在长度数组中使用不同的值:

length = 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, 77943
T=80000 runs for  0.813000 seconds. 

在这个示例之后,我不再能够说明这些时间是如何测量的 - 我的第二个示例似乎需要比我失败的测试更多的处理器指令,并且不允许我认为在第一个示例中发生的缓存。实际上我无法确定这种行为的原因,但我很确定它应该与处理器缓存或传送带有关。我非常好奇这些实验在不同芯片组上的表现如何,因此请随时在此处发表评论。

另外,如果有任何人比我更了解硬件并且他/她可以解释这种行为,我们将不胜感激。

在此之前,我应该为自己做一个注释 - 在估计算法复杂度时不要低估处理器优化。有时,它们似乎会显着降低/增加特定示例的摊销速度。

【问题讨论】:

Why does changing 0.1f to 0 slow down performance by 10x? 的可能副本 【参考方案1】:

这种奇怪行为的原因原来是denormal numbers。在这种极端情况下,将这些数字视为纯零的代码大大加快了我的代码速度。

提示:在这种情况下,非正规数是非常接近于零的数字(例如,浮点数为 10-38;由于@PascalCuoq 进行了更正)。对于这样的数字,处理器的处理速度会慢很多,因为:(取自***):

某些系统在硬件中处理非规范值的方式与 正常值。其他人将非规范值的处理留给系统 软件,仅处理正常值和硬件中的零。处理 软件中的非规范值总是导致显着下降 性能。

编辑我还发现了this suggestion,您如何检查数字是否变得异常。

【讨论】:

您使用的是什么操作系统和编译器? 那你做了什么?启用非正规为零标志?还是清零?两个都? (还有,什么微架构?) @harold 我在最里面的循环之后放置了一张dp[i][k] = (dp[i][k] &lt; VERY_SMALL_EPSYLON) ? 0 : dp[i][k]; 的支票。这是竞争的一部分。我无权更改标志。 “例如10^-20”?!单精度中最大的非正规化约为 10^-38。对于单精度非规范化,您的偏差接近 10^18 倍(对于双精度,偏差为 10^288)。 @BorisStrandjev 我没有提到它,因为 harold 已经有了,但通常的解决方案是让 FPU 将非规范值映射为零。您的解决方案有其优势(可移植性,或者例如,如果您集成了几个对 FPU 状态有自己要求以正常工作的数值库),但将 FPU 配置为刷新为零意味着不必担心限制是什么,其中其他事情。【参考方案2】:

处理这种情况的另一种选择是使用定点运算并完全避免浮点。问题陈述要求答案精确到 1e-9,并且由于 2^64 大约是 10^19,并且您最多只进行 80000 次迭代,因此这是非常精确的。这样做的方式是定义一个大常数,比如

const uint64_t ONE = pow(10,17);

您将uint64_t 数组初始化为ONE/n 而不是1.0/double(n),主循环如下所示:

  for(int i = 1; i <= T; i++) 
    for(int j = 0; j < n; j++)  
      idx = i - length[j];

      if(idx >= 0)  
        for(int k = 0; k < n; k++)
          dpi[i][k] += dpi[idx][k];
        
          
      else
        dpi[i][j] += ONE;

    
    for(int k = 0; k < n; k++)
      dpi[i][k] = dpi[i][k]/n;
    
  

理论上,这应该更快,因为您避免在主循环中进行浮点运算,而内循环仅包含整数加法。在我的机器上,性能提升只有 10% 左右,这表明真正的瓶颈可能是内存访问。但是,在其他情况下,您可能会看到更大的性能提升。

【讨论】:

嗯,我担心/n 的计算在您的方法中会不准确。您是否尝试过用这种方法实际解决问题? 是的,我尝试了一些输入,包括您提供的输入。这与double 解决方案的区别在于1e-15 的顺序。 很抱歉回复很晚,但我现在测试了您的方法并确认它有效,并且使用它也解决了问题。感谢您分享解决特定问题的另一种方法。

以上是关于对于非常接近零的值,双重计算运行速度要慢得多的主要内容,如果未能解决你的问题,请参考以下文章

OpenMP atomic 比对数组的关键速度要慢得多

远程mysql服务器速度

AVPlayer 加载缓慢

为啥在 SQL Azure 上运行查询要慢得多?

实体框架的大批量更新比我自己批量更新慢得多

xcodebuild 比 Xcode 慢得多?