首先,我以下的讨论假设你的
unsigned long
类型宽度为32位。如果不是,没关系;它的宽度并不重要,但我的所有示例都假定它的宽度为32位。我只是使用
uint32
来表示它,知道
uint32
实际上不像
uint32_t
那样是一个标准类型,请不要告诉我这个。
Daniel Fischer已经很好地详细解释了每个低级操作,我不会在这里重复。但我认为你感兴趣的不是每个低级操作的意义,而是作为一组应用的所有这些操作的必要性。正如我将在下面解释的那样,它们根本不必要。事实上,它们略微有误,但只有在“当前”计数器读数超过半圆到达“前一个”读数的情况下才会出现这种情况。
在我深入解释你的实现中的数学含义之前,让我们先看一个微不足道的例子,说明它如何在最小的翻转情况下计算elapsedInSeconds:
如果preInseconds = 0xffffffff且curInSeconds = 0x00000000,则elapsedInSeconds应为1。
preInseconds = preInseconds<<1; // 0xfffffffe
preInseconds = ~preInseconds; // 0x00000001
preInseconds = preInseconds+1 // 0x00000002
preInseconds = preInseconds>>1; // 0x00000001
elapsedInSeconds = curInSeconds + preInseconds;
... = 0x00000000 + 0x00000001 = 1
...这正是我们所期望的。太好了。
然而,有趣的是,没有任何rollover处理逻辑是必要的。每次我看到有人尝试计算“当前”和“先前”计数器值之间的差异时,我总是看到他们在处理rollover情况时跳跃。更常见的是,他们做错了。情况的耻辱是,对于2的幂大小的计数器,使用特殊情况处理rollover是从来不必要的。如果计数器的满刻度(它翻转的点)小于您的数据类型的大小,则需要将结果掩码回计数器中的位数,但那就是您真正需要的唯一rollover处理,而在这种情况下,您只需每次进行掩码处理,而不用担心它是否翻转(这也避免了分支指令,因此速度更快)。由于您的rollover点是uint32的全刻度值,因此甚至不需要掩码处理结果。
原因如下:
假设,如上所述,preInseconds=0xffffffff,curInSeconds=0。同样,结果应该是1。如果您不关心溢出,那么您只需将curInSeconds-preInseconds作为结果即可。但在溢出的情况下,减法操作将产生下溢。这是什么意思?这意味着如果您有更多的位数要处理(即另一个uint32被用作64位复合计数器的高位),那么您需要从高位借1(就像小学用十进制数字进行减法运算一样)。但在您的情况下,没有更高的字可以借用。没关系。真的。您也不关心那些位。您仍然可以获得您要查找的差异值:
elapsedInSeconds = curInSeconds - preInseconds;
... = 0x00000000 - 0xffffffff = 1
...这样就可以得到预期的结果,而不需要任何特殊的翻转处理逻辑。
因此,您可能会想,“当然,这对于您的简单示例有效,但如果翻转很大怎么办?”好吧,让我们探索一下这种可能性。假设preInseconds=0xffffffff,curInSeconds=0xfffffffe。在这个例子中,我们几乎完全从上一个样本回到了原点;实际上,我们只差一个计数。在这种情况下,我们的结果应该是0xffffffff(即比uint32可以表示的值少一个计数):
elapsedInSeconds = curInSeconds - preInseconds;
... = 0xfffffffe - 0xffffffff = 0xffffffff
不相信我?试试这个:
#include <stdio.h>
typedef unsigned long uint32;
int main()
{
uint32 prev = 0xffffffff;
uint32 cur = 0xfffffffe;
uint32 result = cur - prev;
printf("0x%08x - 0x%08x = 0x%08x\n", cur, prev, result);
}
现在,让我们回到你的实现背后的数学问题:
这个计算“有点像”计算preInseconds的二进制补码,并将结果赋值回preInseconds。如果你了解计算机数字表示和二进制补码加减法,你就知道计算差A-B与计算A和B的二进制补码之和是相同的,即A+(-B)。如果你以前从未研究过它,请在维基百科或其他地方查找有关如何使用二进制补码使计算机的ALU能够重复使用其加法电路进行减法的信息。
现在让我们来看看你展示的代码实际上“有什么问题”:
要计算一个数字的二进制补码,你需要反转该数字(将所有的0位变为1,所有的1位变为0),然后再加1。这很简单。这就是你的代码“有点像”在做什么,但不完全是这样。
preInseconds = preInseconds<<1; // oops, here we lose the top bit
preInseconds = ~preInseconds; // do the 2's complement inversion step*
preInseconds = preInseconds+1 // do the 2's complement addition step*
preInseconds = preInseconds>>1; // shift back to where it ought to be,
// but without that top bit we wish we kept
*NOTE: The +1 above only works here because the low bit is
guaranteed to be 1 after the ~ operation, which carries a 1
up into the 2nd bit, where it matters.
因此,我们可以看到,数学运算实际上是通过执行“几乎”二进制补码转换来手动否定preInseconds的值。不幸的是,在这个过程中也会丢失最高位,这使得翻转逻辑只能在经过的时间elapsedInSeconds = 0x7fffffff内工作,而不是真正应该达到的完整范围限制0xffffffff。
您可以将其转换为以下内容,并消除顶部位的损失:
preInseconds = ~preInseconds
preInseconds = preInseconds+1 // do the 2's complement addition step
现在你已经直接计算出了二进制补码,可以计算结果:
elapsedInSeconds = curInSeconds + preInseconds;
但愚蠢的是,这在计算上等同于简单地执行以下操作...
elapsedInSeconds = curInSeconds - preInseconds
一旦你意识到这一点,你的代码示例就变成了:
if(curInSeconds>=preInseconds)
{
elapsedInSeconds = curInSeconds - preInseconds;
}
else
{
elapsedInSeconds = curInSeconds - preInseconds;
}
这应该表明在第一次处理时没有必要将rollover作为特殊情况处理。