有人能解释一下这段代码吗——使用位运算实现rtc翻转(c语言)?

3

有人可以解释一下这个 ((~(preRtcInseconds << 1)+1)>>1) 吗?

unsigned long preInseconds;
unsigned long curInSeconds;
unsigned long elapsedInSeconds;

if(curInSeconds>=preInseconds)
{
   elapsedInSeconds = curInSeconds - preInseconds;//does easy thing. no needs roll over
}
else{ //rollover
  preInseconds = ((~(preInseconds <<1)+1)>>1);
  elapsedInSeconds = curInSeconds + preInseconds;
}

2
左移、取反、将值加1、右移,就这些。 - Blood
你知道你的计数器是否来自提供少于32位的硬件设备吗?也就是说,计数器只有31位宽,或者可能只有28位宽吗?或者你确定计数器在滚动回0之前会计数到0xffffffff吗? - phonetagger
3个回答

4

unsigned long 的宽度为 w。则 unsigned long 可以容纳的最大值为

ULONG_MAX = 2^w - 1

假设有 preInseconds = a + b,其中

a = preInseconds & (1ul << (w-1))
b = preInseconds & ((1ul << (w-1)) - 1)

a 的值为0或2^(w-1),取决于preInseconds >= 2^(w-1)。然后进行的初始左移会使a被抵消,因此

preInseconds << 1

输出为 b << 1 或者 2*b

然后进行按位取反操作,

~(b << 1)

给出 ULONG_MAX - (b << 1)

如果 b == 0,将1添加到 ULONG_MAX - (b << 1) 的结果为0,否则

~(b << 1) + 1

提供

2^w - (b << 1)

如果b == 0,右移一位会得到0,否则会得到1。

2^(w-1) - b

否则。因此。
preInseconds = ((~(preInseconds <<1)+1)>>1);

preInseconds设置为

2^(w-1) - b

如果b != 0,则为非零值,如果b == 0,则为0。

最后,

elapsedInSeconds = curInSeconds + preInseconds;

因此,如果preInseconds2^(w-1),则将elapsedInSeconds设置为curInSeconds的值[如果执行else分支,则preInseconds > curInSeconds,因此不为0]。
curInSeconds - (preInseconds & (ULONG_MAX >> 1)) + 2^(w-1)

否则。
我不确定该操作的目的是什么。对于“preInseconds > curInSeconds”,计算
curInSeconds - preInseconds

会导致
(2^w - preInseconds) + curInSeconds

数学上来说,这与以下内容相同:

(2^w + curInSeconds) - preInseconds

如果自上次记录preInSeconds以来,curInSeconds计数器滚动了一次,则经过的时间将是多长时间呢?假设当前计数器值小于以前的值,那么这就有意义了。在else分支中进行的操作:

  • 如果preInseconds <= 2^(w-1),则elapsedInSeconds变为curInSeconds - preInseconds + 2^(w-1),即滚动后的当前计数器和上一个计数器之间的差值减去2^(w-1)
  • 如果preInseconds > 2^(w-1),由于在无符号算术中x - 2^(w-1) == x + 2^(w-1),所以我们得到与curInSeconds - preInSeconds相同的结果。

因此,假设curInSeconds滚动了一次,它会计算前一事件和当前事件之间经过的秒数,除非滚动发生在前一事件之后2^(w-1)秒或更长时间,此时将从实际经过的时间中减去2^(w-1) 秒。


1
我觉得为了争论一个邪恶程序员可能会将他们的 uintXXX 类型定义为具有其他位数的类型而分心是不必要的。显然,定义这样的类型的目的是获得该大小的对象。 - phonetagger
抱歉各位,我在代码中进行了更正。应该是 'unsigned long',之前是类似于 'typedef unsigned long xx_uint32;' 这样的定义。我还添加了 '// does easy thing.' 的注释。感谢你们的帮助,我对这段代码真的很蠢。 - user1915570
我知道基础知识,即<< 1相当于乘以2的幂,>> 1相当于除以2:因此,<< 1表示*2,>> 1表示/2。但我无法将这些基础知识与如何滚动秒数联系起来。 - user1915570
如果我说,当时间达到75秒时,它需要回滚(减去60秒),并且应该显示为1分15秒,那么正确的结果是15秒。但是,我不知道((~(preRtcInseconds <<1)+1)>>1)是如何实现这一点的。 - user1915570
@user1915570,我已经扩展了答案,但我看不出这些花招的意义所在。 - Daniel Fischer
@phonetagger 不管是不是分散注意力,了解它并明确表明 unit32 不是标准的 C 类型是很好的。顺便说一下,现在它是无符号长整型(unsigned long);) - P.P

2

看起来这是由一个不理解C无符号算术模2^n(或者不理解模2^n算术)的人编写的有缺陷的鼠标悬停代码。实际上,它与以下代码完全等价:

elapsedInSeconds = curInSeconds - preInseconds;
if (curInSeconds < UNLONG_MAX/2 && preInseconds < ULONG_MAX/2)
    elapsedInSeconds &= ULONG_MAX/2;

也就是说,它进行了模2^n减法,但在一些情况下(可能从未出现在任何测试用例中),它会得到错误的结果(清除而不是设置最高位)。
这可能有意为之,因为实际上做的是对2^n-1取模减法,如果两个数字都小于2^n-1,则进行2^n-1减法,如果任何一个数大于或等于2^n-1,则进行2^n减法(其中n是无符号长整型的大小,以比特为单位)。如果这些时间来自一些可能始终清除最高位或可能不清除的硬件设备,则这可能是有意义的。

1
首先,我以下的讨论假设你的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;       // 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;
}

这应该表明在第一次处理时没有必要将rollover作为特殊情况处理。


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