3

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

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

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 * 10 8 。然而,有趣的是我测试了我的解决方案,特别是针对时间限制。对于我的解决方案,我使用的情况似乎是“最坏情况”:长度为 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. 

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

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

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

4

2 回答 2

7

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

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

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

编辑我还发现了这个关于如何检查数字是否变得异常的建议。

于 2013-10-12T10:17:45.977 回答
1

处理这种情况的另一种选择是使用定点运算并完全避免浮点。问题陈述要求答案精确到 1e-9,因为 2^64 大约是 10^19,而且您最多只进行 80000 次迭代,这已经足够精确了。这将工作的方式是定义一个大的常数,说

const uint64_t ONE = pow(10,17);

您将数组初始化为uint64_ttoONE/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% 左右,这表明真正的瓶颈可能是内存访问。但是,在其他情况下,您可能会看到更大的性能提升。

于 2013-10-12T16:54:15.420 回答