移动负符号值是未定义的
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
其中 a
是 signed
) 对于左负操作数,这绝不是未定义的行为,并且对于 C/C++.
好吧,尽管事实上已经选择了接受的答案并且这个问题已经产生了关于 <<
和 >>
运算符的混乱程度,但我会尝试澄清什么是未定义和未定义的内容。
<<
运算符在 signed
和 unsigned
左操作数数量上表现相同,因为对整数的净影响是在数值上加倍它,并且过程对于 signed
和 unsigned
数量是相同的,只需在数字的右侧插入一个 0
,将所有数字位左移作为右运算符指示的数字。
>>
运算符在 signed
和 unsigned
左操作数 上的行为 不同,因为净效应再次,就像整数除以 2 提高到右边的操作数,这意味着,对于二进制补码表示 到 扩展 最重要的位(0
左边变为 00
,左边 1
变为 11
) 以实现除以 2 的右操作数次方的净效果 (-24 >> 3
变为 -3
,除以 2^3,并且 24 >> 3
变为 3
)使用 unsigned
整数时,此行为发生变化(0
变为 00
和 1
变为 01
) 再次像除以 2 一样移动到右运算符的幂。
如标准所述,行为未定义如果您尝试left/right 移动负数位(正确的运算符必须是> 0
)或大于或等于左侧运算符的位数。但这只影响右操作数,而不影响左操作数。
在试图获得有关 JPEG 解码的解释时,请尝试认为 JPEG 解码对编码为有符号量的离散实数近似值使用余弦变换(为实数向量定义),或者它使用有符号整数来评估低精度样本,因此它们从不处理无符号数量(库设计者假设对整数进行运算比对浮点数进行运算更快)。
注意
-24
是二进制 1111111...1111101000
,右移 3
使其成为 11111...11111101
,或 -3
。如果我们再次移动它,它将变为 111...1110
或 -2
(-1.5
四舍五入为负无穷大),然后再次变为 1111...1111
或 -1
.
24
是 00000000....00011000
右移它变成 000000...0000011
即 3
。如果我们再次移动它,它变成 000...0001
或 1
(1.5
四舍五入为负无穷大),我们再次得到 000...0000
或 0
(0.5
四舍五入到负无穷大)。
(请注意,使用此方法进行的负偏移会截断为负无穷大,使 -1 >> 1
变为 -1
,因为 -0.5
截断为 -1
而不是0
.)
我主要关注原始 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
其中 a
是 signed
) 对于左负操作数,这绝不是未定义的行为,并且对于 C/C++.
好吧,尽管事实上已经选择了接受的答案并且这个问题已经产生了关于 <<
和 >>
运算符的混乱程度,但我会尝试澄清什么是未定义和未定义的内容。
<<
运算符在signed
和unsigned
左操作数数量上表现相同,因为对整数的净影响是在数值上加倍它,并且过程对于signed
和unsigned
数量是相同的,只需在数字的右侧插入一个0
,将所有数字位左移作为右运算符指示的数字。>>
运算符在signed
和unsigned
左操作数 上的行为 不同,因为净效应再次,就像整数除以 2 提高到右边的操作数,这意味着,对于二进制补码表示 到 扩展 最重要的位(0
左边变为00
,左边1
变为11
) 以实现除以 2 的右操作数次方的净效果 (-24 >> 3
变为-3
,除以 2^3,并且24 >> 3
变为3
)使用unsigned
整数时,此行为发生变化(0
变为00
和1
变为01
) 再次像除以 2 一样移动到右运算符的幂。
如标准所述,行为未定义如果您尝试left/right 移动负数位(正确的运算符必须是> 0
)或大于或等于左侧运算符的位数。但这只影响右操作数,而不影响左操作数。
在试图获得有关 JPEG 解码的解释时,请尝试认为 JPEG 解码对编码为有符号量的离散实数近似值使用余弦变换(为实数向量定义),或者它使用有符号整数来评估低精度样本,因此它们从不处理无符号数量(库设计者假设对整数进行运算比对浮点数进行运算更快)。
注意
-24
是二进制1111111...1111101000
,右移3
使其成为11111...11111101
,或-3
。如果我们再次移动它,它将变为111...1110
或-2
(-1.5
四舍五入为负无穷大),然后再次变为1111...1111
或-1
.24
是00000000....00011000
右移它变成000000...0000011
即3
。如果我们再次移动它,它变成000...0001
或1
(1.5
四舍五入为负无穷大),我们再次得到000...0000
或0
(0.5
四舍五入到负无穷大)。
(请注意,使用此方法进行的负偏移会截断为负无穷大,使 -1 >> 1
变为 -1
,因为 -0.5
截断为 -1
而不是0
.)