对于非常接近零的值,双重计算运行速度要慢得多
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] < 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
的顺序。
很抱歉回复很晚,但我现在测试了您的方法并确认它有效,并且使用它也解决了问题。感谢您分享解决特定问题的另一种方法。以上是关于对于非常接近零的值,双重计算运行速度要慢得多的主要内容,如果未能解决你的问题,请参考以下文章