从两个(64位)整数获取可靠的整数百分比比率

4
在我的平台上,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 );

1
您是正确的,直接方法在极端情况下可能会“出错”。这是因为有两个级别的舍入以及从64位-> 53位的精度损失。 - Mysticial
1
@Mysticial。结果只需要最多7位精度。我认为64→53根本不是问题。 - kennytm
以下是两个可能对您有用的链接:1)http://kanooth.com/numbers/ 2)http://en.wikipedia.org/wiki/Arbitrary-precision_arithmetic - paulsm4
@Mysticial:OP的公式是不正确的,因为它总是向0截断,而不是四舍五入。在Python中,int((100 * 2751734980444983885.0) / 10006309019799941400.0 + 0.5)给出的结果是28。不过我同意,在极少数情况下,64位结果可能会得到0.49999...,而53位结果则会得到0.5000... - kennytm
1
@KennyTM 我删除了上一条评论,因为那是个糟糕的例子。这里有一个更好的例子:850536266682995018 / 3335436339933313800 即使加上 +0.5,这个例子也失败了。正确答案是 25%,而正确的公式给出的是 26%。但是,我们是达成了一致意见的。 - Mysticial
3个回答

5
这并非完全安全。 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 >= 100 并且 ULLONG_MAX / 100 >= partialSize % totalSize,则没有问题。

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

其他情况会更加繁琐,我不是很愿意做,但如果你需要的话,我可以被说服。


+1. 虽然我没有测试过,但它看起来可行,并解决了不同的舍入行为。 - Mysticial
但对于四舍五入和没有溢出风险的数字,你的方法更简单、更快。 - Daniel Fischer

4
请注意,您的公式不正确,因为它省略了取四舍五入所需的+0.5
因此,我会采用已更正的公式继续进行。
(unsigned long long)( ((double)partialSize)/((double)totalSize) * 100.0 + 0.5);

正如我在评论中提到的那样,直接的方法虽然简单,但不能保证结果正确舍入。因此,你的直觉是正确的,这并非绝对可靠。

在绝大多数情况下,它仍然是正确的,但会有一小部分边界情况无法正确舍入。这是否重要取决于你。但通常直接的方法对于大多数目的来说已足够。

为什么它可能失败:

有4个舍入级别。(从我在评论中提到的2个进行了更正)

  1. 将64位强制转换成53位
  2. 除法
  3. 乘以100。
  4. 最终强制转换。

每当你有多个舍入源时,你就会遭受浮点误差的常见来源。

反例:

虽然罕见,但我将列出一些例子,直接使用公式将给出错误的舍入结果:

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

解决方案:

除了使用任意精度算术,我无法想到一个完美的、100%可靠的解决方案。

但最终,您真的需要它始终完美地四舍五入吗?


编辑:

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

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

只要 x * 100 + y/2 不溢出,这个代码就可以正常运行。@Daniel Fischer 的答案提供了更全面的解决方案,可用于实现其他舍入行为。不过修改这个代码以实现银行家舍入法应该也不难。

如果我知道所有的数字都小于2^53,那么我会得到准确的结果吗?(我正在处理文件大小,在我的情况下,文件不会变得非常巨大。) - pf85
1
如果你的数字小于2^53,那么就排除了第一点。我相当有信心仍然可以找到一些情况,其中浮点舍入将结果推向0.5(或整数)边界,但对于“xx.5 ± epsilon”的百分比错误舍入结果是否真的是一个问题呢?如果是的话,我在答案中添加的整数方法将保证正确舍入的结果,因为溢出在这里不是一个问题。 - Daniel Fischer
+0.5看起来像是一个hack。那么这个怎么样?-> static unsigned int d(unsigned long long p, unsigned long long t) { return round((double)p * 100 / t); } - jørgensen
1
@jørgensen 差不多一样。可能更加干净,但仍然会受到相同的舍入误差影响,这会破坏原始方法。 - Mysticial

2

对于某些值,单一的公式总是会溢出、崩溃或产生大误差。
几乎总是可以使用以下组合:

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

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


很好的建议。但是你忽略了偏差:这将向零截断而不是四舍五入。 - Ben Voigt
实际上,这段代码并没有完全正确地截断。更糟糕的是,partialSize/(totalSize/100) 的逻辑会失去一些精度,因此可能会出现更大的误差。但问题说“四舍五入到最近的整数”,而不是最接近的整数。 - ugoren

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