【问题标题】:Google Protocol Buffers: ZigZag EncodingGoogle 协议缓冲区:ZigZag 编码
【发布时间】:2010-12-26 07:27:39
【问题描述】:

来自Encoding - Protocol Buffers - Google Code上的“签名类型”:

ZigZag 编码将有符号整数映射到无符号整数,因此具有较小绝对值(例如 -1)的数字也具有较小的 varint 编码值。它以一种通过正整数和负整数来回“曲折”的方式来执行此操作,因此 -1 被编码为 1,1 被编码为 2,-2 被编码为 3,依此类推,就像你可以看下表:

Signed Original  Encoded As
0                0
-1               1
1                2
-2               3
2147483647       4294967294
-2147483648      4294967295

换句话说,每个值 n 都是使用编码的

(n << 1) ^ (n >> 31)

对于 sint32s,或

(n << 1) ^ (n >> 63)

适用于 64 位版本。

(n << 1) ^ (n >> 31) 如何等于表中的内容?我知道这对积极因素有用,但是对于-1来说,这如何工作? -1 不是1111 1111(n << 1) 不是1111 1110? (在任何语言中,对负数的移位是否格式正确?)

尽管如此,使用公式并执行 (-1 << 1) ^ (-1 >> 31),假设是 32 位 int,我得到 1111 1111,即 40 亿,而表格认为我应该有 1。

【问题讨论】:

    标签: protocol-buffers bit-shift zigzag-encoding


    【解决方案1】:

    将一个负符号整数右移复制符号位,这样

    (-1 >> 31) == -1
    

    那么,

    (-1 << 1) ^ (-1 >> 31) = -2 ^ -1
                           = 1
    

    这可能更容易以二进制形式可视化(此处为 8 位):

    (-1 << 1) ^ (-1 >> 7) = 11111110 ^ 11111111
                          = 00000001
    

    【讨论】:

    • 啊,这实际上是我误读的下一段所说的。非常感谢!
    • 我给了 +1。但是应该指出&gt;&gt;&gt;&gt;&gt; 的含义因语言/实现而异(请参阅Shift Operator)。在协议缓冲区文档的情况下,它明确表示 Arithmetic Shift (aka "Signed Shift") 在语义上如所述。
    • 只是想指出,在 C/C++ 中移动负符号整数是不可移植的。根据 C 标准,负符号整数左移具有未定义的行为,而负符号整数右移具有实现定义的行为。首先转换为无符号类型以确保安全并具有明确定义的可移植结果。这意味着您不能像上面那样依赖于负数的算术右移。
    • 可移植 C 中等价的编码 + 解码表达式为: ( x > 31 );和 ( x >> 1 ) ^ -( x & 0x1 );其中 x 是原始有符号值的无符号 32b 表示。
    • 将带符号的 32 位整数右移 31 位是“未定义行为”
    【解决方案2】:

    考虑 zig zag 映射的另一种方式是,它是对符号和幅度表示的轻微扭曲。

    在zig zag映射中,映射的最低有效位(lsb)表示值的符号:如果为0,则原始值为非负数,如果为1,则原始值为负数。

    将非负值简单地左移一位,以便为 lsb 中的符号位腾出空间。

    对于负值,您可以对数字的绝对值(大小)执行相同的左移一位,并简单地让 lsb 指示符号。例如,-1 可以映射到 0x03 或 0b00000011,其中 lsb 表示为负,1 的大小左移 1 位。

    这种符号和幅度表示的丑陋之处在于“负零”,映射为 0x01 或 0b00000001。这种零变体“用尽”了我们的一个值,并将我们可以表示的整数范围移动了一个。我们可能希望将负零映射到 -2^63 的特殊情况,以便我们可以表示 [-2^63, 2^63) 的完整 64b 2 的补码范围。这意味着我们使用了一种有价值的单字节编码来表示一个值,该值将非常、非常、非常少地用于为小幅度数字优化的编码中,并且我们引入了一种特殊情况,这很糟糕。

    这就是 zig zag 对这个符号和幅度表示的扭曲发生的地方。符号位仍在 lsb 中,但对于负数,我们从幅度中减去 1,而不是特殊情况下的负零。现在,-1 映射到 0x01 并且 -2^63 也具有非特殊情况表示(即 - 幅度 2^63 - 1,左移一位,设置 lsb / 符号位,所有位都设置为 1) .

    因此,考虑 zig zag 编码的另一种方式是,它是一种更智能的符号和幅度表示:符号位存储在 lsb 中,从负数的幅度中减去 1,幅度左移一个位。

    使用您发布的无条件按位运算符来实现这些转换比显式测试符号、特殊情况下处理负值(例如 - 取反和减 1,或按位不)、移动幅度和然后显式设置 lsb 符号位。但是,它们在效果上是等效的,并且这些更明确的符号和幅度系列步骤可能更容易理解我们在做什么以及为什么要做这些事情。

    我会警告你,尽管 C/C++ 中的位移负值是不可移植的,应该避免。左移负值具有未定义的行为,而右移负值具有实现定义的行为。即使左移一个正整数也可能有未定义的行为(例如 - 如果您移入符号位,可能会导致陷阱或更糟)。所以,一般来说,不要在 C/C++ 中对有符号类型进行位移。 “拒绝吧。”

    首先转换为类型的无符号版本,以根据标准获得安全、明确定义的结果。这确实意味着您将不会有负值的算术移位 - 只有逻辑移位,因此您需要调整逻辑来解决这个问题。

    这里是 C 中 64b 整数的 zig zag 映射的安全且可移植的版本(注意算术否定):

    #include <stdint.h>
    
    uint64_t zz_map( int64_t x )
    {
      return ( ( uint64_t ) x << 1 ) ^ -( ( uint64_t ) x >> 63 );
    }
    
    int64_t zz_unmap( uint64_t y )
    {
      return ( int64_t ) ( ( y >> 1 ) ^ -( y & 0x1 ) );
    }
    

    【讨论】:

      【解决方案3】:

      让我在讨论中添加我的两分钱。正如其他答案所指出的,锯齿形编码可以被认为是符号幅度的扭曲。这一事实可用于实现适用于任意大小整数的转换函数。 例如,我在我的 Python 项目中使用以下代码:

      def zigzag(x: int) -> int:
          return x << 1 if x >= 0 else (-x - 1) << 1 | 1
      
      def zagzig(x: int) -> int:
          assert x >= 0
          sign = x & 1
          return -(x >> 1) - 1 if sign else x >> 1
      

      尽管 Python 的 int 没有固定位宽,但这些函数仍然有效;相反,它动态扩展。但是,这种方法在编译语言中可能效率低下,因为它需要条件分支。

      【讨论】:

      • 此代码还修复了依赖于平台的行为:将带符号的 32 位整数移动 31 位在 c++ 中是未定义的
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2016-02-29
      • 2010-11-28
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-10-18
      • 2011-04-23
      相关资源
      最近更新 更多