首先,我在下面讨论的所有内容都假设您的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
...这正是我们所期望的。伟大的。
然而,有趣的是,不需要任何翻转处理逻辑。完全没有。每次我看到有人试图计算“当前”和“以前”计数器值之间的差异时,我总是看到他们跳过箍来处理翻车情况,而且他们往往做错了。这种情况的耻辱是用特殊情况处理翻转永远不会对于 2 次幂大小的计数器是必需的。如果计数器的满量程(翻转点)小于您的数据类型的大小,您需要将结果屏蔽回计数器中的位数,但这是您真正做过的唯一翻转处理需要,在这种情况下,您只需每次都进行屏蔽,而不必担心它是否翻转(这也避免了分支指令,因此速度更快)。由于您的翻转点是 uint32 的满量程值,因此您甚至不需要屏蔽结果。
原因如下:
如上所述,假设 preInseconds=0xffffffff 和 curInSeconds=0。同样,结果应该是 1。如果您不担心翻转,您只需将 curInSeconds-preInseconds 作为结果。但是在翻转的情况下,减法运算会产生下溢。这意味着什么?这意味着,如果您有更多要处理的位(即另一个 uint32 被用作 64 位复合计数器的高位字),那么您需要从高位字中借 1(就像小学用十进制数减法)。但在你的情况下,没有更高的词可以借用。没关系。真的。反正你也不在乎这些。您仍然可以获得您正在寻找的差异值:
elapsedInSeconds = curInSeconds - preInseconds;
... = 0x00000000 - 0xffffffff = 1
...这完全没有任何特殊的翻转处理逻辑就给出了预期的结果。
所以你可能会想,“当然,这适用于你的小例子,但是如果翻转很大怎么办?” 好吧,让我们探索一下这种可能性。然后假设 preInseconds=0xffffffff 和 curInSeconds=0xffffffffe。在这个例子中,我们几乎完全从前面的例子中恢复过来;事实上,我们离它只有一步之遥。在这种情况下,我们的结果应该是 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。如果您对数字的计算机表示和二进制补码加减法有所了解,您就会知道计算差 AB 与计算 A 和 B 的二进制补码之和相同,即 A+(-B)。如果您以前从未研究过它,请查看 Wikipedia 或任何地方,了解二进制补码如何使计算机的 ALU 能够重新使用其加法电路进行减法。
现在看看您显示的代码实际上“错误”的是什么:
要计算一个数字的二进制补码,请将该数字取反(将其所有 0 位更改为 1,将其所有 1 位更改为 0),然后加一。就是这么简单。这就是你的代码正在做的“有点”,但不完全是。
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; // do the 2's complement inversion step
preInseconds = preInseconds+1 // do the 2's complement addition step
所以现在你已经直接计算了二进制补码,你可以计算结果:
elapsedInSeconds = curInSeconds + preInseconds; // (preInseconds is the 2's compl of its original value)
但愚蠢的是,这在计算上等同于简单地这样做......
elapsedInSeconds = curInSeconds - preInseconds; // (preInseconds is its unconverted original value)
一旦你意识到这一点,你的代码示例就变成了:
if(curInSeconds>=preInseconds)
{
elapsedInSeconds = curInSeconds - preInseconds;
}
else // rollover
{
elapsedInSeconds = curInSeconds - preInseconds;
}
...这应该清楚地表明,首先不需要将翻转作为特殊情况处理。