移动负符号值是未定义的

shifting a negative signed value is undefined

我主要关注原始 JPEG 标准 (ITU 81),特别是图 图 F.12:扩展 V 中解码值的符号位:

作为参考,SLL 术语表示:shift left logical operation(参见 PDF 第 15 页)

现在著名的libjpeg实现决定实现它this way(相当直接的转录):

/*
 * Figure F.12: extend sign bit.
 * On some machines, a shift and add will be faster than a table lookup.
 */

#ifdef AVOID_TABLES

#define HUFF_EXTEND(x,s)  ((x) < (1<<((s)-1)) ? (x) + (((-1)<<(s)) + 1) : (x))

#else

#define HUFF_EXTEND(x,s)  ((x) < extend_test[s] ? (x) + extend_offset[s] : (x))

static const int extend_test[16] =   /* entry n is 2**(n-1) */
  { 0, 0x0001, 0x0002, 0x0004, 0x0008, 0x0010, 0x0020, 0x0040, 0x0080,
    0x0100, 0x0200, 0x0400, 0x0800, 0x1000, 0x2000, 0x4000 };

static const int extend_offset[16] = /* entry n is (-1 << n) + 1 */
  { 0, ((-1)<<1) + 1, ((-1)<<2) + 1, ((-1)<<3) + 1, ((-1)<<4) + 1,
    ((-1)<<5) + 1, ((-1)<<6) + 1, ((-1)<<7) + 1, ((-1)<<8) + 1,
    ((-1)<<9) + 1, ((-1)<<10) + 1, ((-1)<<11) + 1, ((-1)<<12) + 1,
    ((-1)<<13) + 1, ((-1)<<14) + 1, ((-1)<<15) + 1 };

#endif /* AVOID_TABLES */

很明显移动负符号值是 UB,所以我想知道 JPEG 标准的原作者在这里的实际含义。 JPEG 标准是否仅限于二进制补码表示?

Is the JPEG standard limited to Two's complement number representation ?

图表使用SLL而不是*2,流程图依赖于2的补码实现。

C,OTOH,不限于 2 的补码,也不限于以某种“2 的补码”方式使用移位。最好在没有该假设的情况下进行编码。使用无符号类型是良好的开端。

// ((-1)<<15) + 1
((-1u)<<15) + 1

需要查看 HUFF_EXTEND() 的应用以获得更深入的答案。

移动负值只是 C 语言中的未定义行为。在汇编程序级别,这样的转变非常好。 logical/arithmetic 左移指令会将 MSB 移入进位位,仅此而已。 CPU 不会以未定义的方式停止和着火。

也就是说,您可以通过将有符号数转换为无符号数来避免 C 中的 UB。然后您将仅具有实现定义的行为。例如,(int)(-1u<<1) 实际上并没有调用 UB,即使这样的代码看起来很可疑。

安全的方法是对无符号数 (uint32_t) 执行所有计算,并仅在实际需要时才转换为有符号数。切勿对有符号类型执行任何形式的按位运算。

我会完全忽略与不使用二进制补码的 exotic/fictional 系统的兼容性。注重与现实世界主流电脑的兼容性。

很明显,将负数 移动与 multiply/divide 相乘的 n 次幂具有相同的效果(对于 a << n/a >> n 其中 asigned) 对于左负操作数,这绝不是未定义的行为,并且对于 C/C++.

好吧,尽管事实上已经选择了接受的答案并且这个问题已经产生了关于 <<>> 运算符的混乱程度,但我会尝试澄清什么是未定义和未定义的内容。

  • << 运算符在 signedunsigned 左操作数数量上表现相同,因为对整数的净影响是在数值上加倍它,并且过程对于 signedunsigned 数量是相同的,只需在数字的右侧插入一个 0,将所有数字位左移作为右运算符指示的数字。

  • >> 运算符在 signedunsigned 左操作数 上的行为 不同,因为净效应再次,就像整数除以 2 提高到右边的操作数,这意味着,对于二进制补码表示 扩展 最重要的位(0 左边变为 00,左边 1 变为 11) 以实现除以 2 的右操作数次方的净效果 (-24 >> 3 变为 -3,除以 2^3,并且 24 >> 3 变为 3)使用 unsigned 整数时,此行为发生变化(0 变为 001 变为 01) 再次像除以 2 一样移动到右运算符的幂。

如标准所述,行为未定义如果您尝试left/right 移动负数位(正确的运算符必须是> 0)或大于或等于左侧运算符的位数。但这只影响右操作数,而不影响左操作数。

在试图获得有关 JPEG 解码的解释时,请尝试认为 JPEG 解码对编码为有符号量的离散实数近似值使用余弦变换(为实数向量定义),或者它使用有符号整数来评估低精度样本,因此它们从不处理无符号数量(库设计者假设对整数进行运算比对浮点数进行运算更快)。

注意

  • -24 是二进制 1111111...1111101000,右移 3 使其成为 11111...11111101,或 -3。如果我们再次移动它,它将变为 111...1110-2-1.5 四舍五入为负无穷大),然后再次变为 1111...1111-1.
  • 2400000000....00011000 右移它变成 000000...00000113。如果我们再次移动它,它变成 000...000111.5 四舍五入为负无穷大),我们再次得到 000...000000.5四舍五入到负无穷大)。

(请注意,使用此方法进行的负偏移会截断为负无穷大,使 -1 >> 1 变为 -1,因为 -0.5 截断为 -1 而不是0.)