【问题标题】:Obtaining a reliable integer percentage ratio from two (64-bit) integers从两个(64 位)整数中获得可靠的整数百分比
【发布时间】:2012-01-31 04:12:58
【问题描述】:

在我的平台上,unsigned long long 是 64 位(8 字节)。假设我有两个这样的变量:

unsigned long long partialSize;
unsigned long long totalSize;
//somehow determine partialSize and totalSize

如何可靠地确定partialSizetotalSize 的百分比(四舍五入到附近的整数)? (如果可能的话,如果我不必假设前者小于后者,那就太好了,但如果我真的必须做出这个假设,那很好。但我们当然可以假设两者都不-负数。)

例如,以下代码是否完全防弹?我担心它包含某种舍入、强制转换或转换错误,可能会导致比率在某些情况下不正常。

unsigned long long ratioPercentage
    = (unsigned long long)( ((double)partialSize)/((double)totalSize) * 100.0 );

【问题讨论】:

  • 您说得对,在非常极端的情况下,直接的方法可能会“失控”。这是因为有两个级别的舍入以及 64 位 -> 53 位的精度损失。
  • @Mysticial。结果最多只需要 7 位精度。我会说 64 → 53 根本不是问题。
  • @Mysticial:OP 的公式不正确,因为它总是向 0 截断,而不是四舍五入。 int((100 * 2751734980444983885.0) / 10006309019799941400.0 + 0.5) 在 Python 中给了我 28。我同意,尽管在极少数情况下 64 位结果给出 0.49999... 而 53 位结果给出 0.5000...
  • @KennyTM 我删除了我的最后一条评论,因为这是一个不好的例子。这是一个更好的:850536266682995018 / 3335436339933313800 即使使用+0.5,这个也失败了。正确答案是25%,正确的公式是26%。但是,是的,我们是一致的。

标签: c 64-bit int computer-science


【解决方案1】:

请注意,您的公式不正确,因为它省略了进行四舍五入所需的 +0.5

所以我将继续假设这个更正的公式:

(unsigned long long)( ((double)partialSize)/((double)totalSize) * 100.0 + 0.5);

正如我在 cmets 中提到的,直截了当的方法虽然简单,但不能保证正确舍入结果。所以你的直觉是正确的,它不是防弹的。

在绝大多数情况下,它仍然是正确的,但会有一小部分边界情况无法正确四舍五入。这些事情是否取决于您。但是对于大多数目的而言,直接的方法通常就足够了。

为什么会失败:

有 4 个四舍五入级别。 (从我在 cmets 中提到的 2 更正)

  1. 强制转换为 64 位 -> 53 位
  2. 乘以 100。
  3. 最后的演员阵容。

当您有多个舍入来源时,您就会遇到通常的浮点错误来源。

反例:

虽然很少见,但我将列出几个示例,其中直截了当的公式会给出不正确的四舍五入结果:

 850536266682995018 /  3335436339933313800  //  Correct: 25%  Formula: 26%
3552239702028979196 / 10006309019799941400  //  Correct: 35%  Formula: 36%
1680850982666015624 /  2384185791015625000  //  Correct: 70%  Formula: 71%

解决方案:

除了使用arbitrary precision arithmetic之外,我想不出一个干净的 100% 防弹解决方案。

但最后,你真的需要它始终完美圆润吗?


编辑:

对于较小的数字,这是一个非常简单的解决方案,可以在 0.5 上进行四舍五入:

return (x * 100 + y/2) / y;

只要x * 100 + y/2 不溢出,这将起作用。

@Daniel Fischer 的回答对其他舍入行为有更全面的解决方案。虽然修改这个应该不会太难以达到四舍五入。

【讨论】:

  • 如果我知道我的所有数字都小于 2^53 怎么办?那我会得到准确的结果吗? (我正在处理文件大小,在我的情况下,文件不会那么庞大。)
  • 如果您的数字小于 2^53,则消除第 1 点。我相当有信心,仍然可以找到一些浮点舍入将结果推向 0.5(或整数)的情况) 边界,但是对于xx.5 ± epsilon 的百分比的错误舍入结果是一个真正的问题吗?如果是这样,我添加到答案中的整数方法将保证正确舍入的结果,因为溢出在这里不是问题。
  • +0.5 看起来像个 hack。那么这个呢?-> static unsigned int d(unsigned long long p, unsigned long long t) { return round((double)p * 100 / t); }
  • @jørgensen 几乎一样。可能更简洁一些,但它仍然会遇到会破坏原始方法的相同舍入错误。
【解决方案2】:

它并非完全防弹。 double 尾数只有 53 位(52 + 1 隐式),因此如果您的数字大于 2^53,则转换为 double 通常会引入舍入错误。但是,与数字本身相比,舍入误差非常小,因此导致整数值的百分比计算会比转换引入更多的不准确性。

一个可能更严重的问题是它总是向下舍入,例如对于totalSize = 1000partialSize = 99,它将返回9,而不是更接近的值10。您可以通过在转换为 unsigned long long 之前添加 0.5 来获得更好的舍入。

你可以只使用整数算术得到精确的结果(如果最终结果没有溢出),如果partialSize不太大,这相当容易:

if (partialSize <= ULLONG_MAX / 100) {
    unsigned long long a = partialSize * 100ULL;
    unsigned long long q = a / totalSize, r = a % totalSize;
    if (r == 0) return q;
    unsigned long long b = totalSize / r;
    switch(b) {
        case 1: return q+1;
        case 2: return totalSize % r ? q : q+1; // round half up
        default: return q;
    }
}

如果您想要地板、天花板或圆半对偶,则可以轻松修改。

totalSize &gt;= 100ULLONG_MAX / 100 &gt;= partialSize % totalSize 没关系,

unsigned long long q0 = partialSize / totalSize;
unsigned long long r = partialSize % totalSize;
return 100*q0 + theAbove(r);

在其他情况下它会变得更加繁琐,我不热衷于这样做,但如果你需要它,我可以被说服。

【讨论】:

  • +1。虽然我没有对此进行测试,但它看起来可行并且解决了不同的舍入行为。
  • 但是对于四舍五入和没有溢出风险的数字,您的更简单、更快。
【解决方案3】:

对于某些值,单个公式总是会溢出、崩溃或出现大错误。
这种组合几乎总是很有效:

if (totalSize > 1000000) {
    pct = partialSize / (totalSize / 100);
} else {
    pct = (partialSize*100) / totalSize;
}

只有当 partialSize 大于 MAX_U_LONG_LONG/100,totalSize 小于 1000000 时才会失败。在这种情况下,正确的百分比远大于 100%,所以不是很有趣。

【讨论】:

  • 很好的建议。但是你错过了偏见:这将截断为零而不是四舍五入。
  • 确实,这段代码没有完全正确地截断。更糟糕的是,partialSize/(totalSize/100) 逻辑会丢失一些精度,因此它可能会丢失更多。但问题是“四舍五入到附近的整数”,而不是最近的。
猜你喜欢
  • 2011-08-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-04-09
  • 2011-09-12
  • 2019-08-17
  • 2012-02-11
  • 2019-03-15
相关资源
最近更新 更多