1

我正在我的简单 VM 中实现一个相对分支功能。

基本上,我得到了一个 8 位的相对值。然后我将其左移 1 位以使其成为 9 位值。因此,例如,如果您说“分支 +127”,这实际上意味着 127 条指令,因此 IP 将增加 256。

我当前的代码如下所示:

uint8_t argument = 0xFF; //-1 or whatever
int16_t difference = argument << 1;
*ip += difference; //ip is a uint16_t

但是,我不相信差异会被检测为小于 0。我对签名到未签名的工作方式感到生疏。除此之外,我不确定在 case 参数是 -1 或 -2 或其他东西的情况下是否会正确地从 IP 中减去差异。

基本上,我想要满足这些“测试”的东西

//case 1
argument = -5
difference -> -10
ip = 20 -> 10 //ip starts at 20, but becomes 10 after applying difference

//case 2
argument = 127 (must fit in a byte)
difference -> 254
ip = 20 -> 274

希望这使它更清楚一点。

无论如何,我将如何便宜地做到这一点?我看到了一个类似问题的“解决方案”,但它涉及分裂。我正在使用慢速嵌入式处理器(假设没有有效的乘法和除法方法),所以这是我想避免的一件大事。

4

3 回答 3

0

我所有的引用都来自 C 标准,第 6.3.1.3 节。当值在有符号类型的范围内时,无符号到有符号的定义很好:

1 当整数类型的值转换为_Bool以外的其他整数类型时,如果该值可以用新的类型表示,则不变。

有符号到无符号定义明确:

2 否则,如果新类型是无符号的,则在新类型可以表示的最大值的基础上反复加减一,直到该值在新类型的范围内。

无符号到有符号,当值超出范围时定义不太明确:

3 否则,新类型是有符号的,值不能在其中表示;结果是实现定义的,或者引发了实现定义的信号。

不幸的是,您的问题在于第 3 点的领域。C 不保证任何隐式机制来转换超出范围的值,因此您需要明确提供一个。第一步是决定您打算使用哪种表示形式:1 的补码、2 的补码或符号和幅度

您使用的表示将影响您使用的翻译算法。在下面的示例中,我将使用二进制补码:如果符号位为 1 并且值位均为 0,则这对应于您的最低值。您的最低值是您必须做出的另一个选择:在二进制补码的情况下,使用INT16_MIN(-32768) 或INT8_MIN(-128) 中的任何一个都是有意义的。INT16_MIN - 1在其他两个的情况下,使用或INT8_MIN - 1由于负零的存在是有意义的,这可能应该被翻译为与常规零无法区分。在此示例中,我将使用INT8_MIN,因为(uint8_t) -1应该将 -1 转换为int16_t.

将符号位与值位分开。value应该是绝对值,除非在二进制补码最小值的情况下,何时为sign1,而value为 0。当然,符号位可以是您喜欢的任何位置,尽管它是常规的休息在最左边。因此,右移 7 位得到传统的“符号”位:

uint8_t sign =  input >> 7;
uint8_t value = input & (UINT8_MAX >> 1);
int16_t result;

如果符号位为 1,我们将把它称为负数并添加到 INT8_MIN 以构造符号,这样我们就不会陷入与开始时相同的难题,或者更糟:未定义的行为(这是一个人的命运其他答案)。

if (sign == 1) {
    result = INT8_MIN + value;
}
else {
    result = value;
}

这可以缩短为:

int16_t result = (input >> 7) ? INT8_MIN + (input & (UINT8_MAX >> 1)) : input;

...或者,更好的是:

int16_t result = input <= INT8_MAX ? input
                                   : INT8_MIN + (int8_t)(input % (uint8_t) INT8_MIN);

符号测试现在涉及检查它是否在正范围内。如果是,则该值保持不变。否则,我们使用加法和取模来产生正确的负值。这与上述 C 标准的语言相当一致。它适用于二进制补码,因为int16_t并且int8_t保证在内部使用二进制补码表示。然而,类似int的类型不需要在内部使用二进制补码表示。例如,在转换为时,unsigned int需要int进行另一次检查,以便我们将小于或等于 INT_MAX 的值视为正数,将大于或等于 (unsigned int) INT_MIN 的值视为负数。任何其他值都需要作为错误处理;在这种情况下,我将它们视为零。

/* Generate some random input */
srand(time(NULL));
unsigned int input = rand();
for (unsigned int x = UINT_MAX / ((unsigned int) RAND_MAX + 1); x > 1; x--) {
    input *= (unsigned int) RAND_MAX + 1;
    input += rand();
}


int result = /* Handle positives: */ input <= INT_MAX ? input
           : /* Handle negatives: */ input >= (unsigned int) INT_MIN ? INT_MIN + (int)(input % (unsigned int) INT_MIN)
           : /* Handle errors: */ 0;
于 2013-04-11T03:51:29.003 回答
0

如果偏移量在 2 的补码表示中,则

转换这个

uint8_t argument = 0xFF; //-1
int16_t difference = argument << 1;
*ip += difference;

进入这个:

uint8_t argument = 0xFF; //-1
int8_t signed_argument;

signed_argument = argument; // this relies on implementation-defined
                            // conversion of unsigned to signed, usually it's
                            // just a bit-wise copy on 2's complement systems
// OR
// memcpy(&signed_argument, &argument, sizeof argument);

*ip += signed_argument + signed_argument;
于 2013-04-11T08:28:51.053 回答
0

澄清一下:您担心左移一个负 8 位数字会使它看起来像一个正 9 位数字?只需在左移前用初始数字的符号位填充前 9 位:

diff = 0xFF;
int16 diff16=(diff + (diff & 0x80)*0x01FE) << 1;

现在你diff16的签名2*diff

正如 Richard J Ross III 所指出的,您可以使用条件分支避免乘法(如果这在您的平台上很昂贵):

int16 diff16 = (diff + ((diff & 0x80)?0xFF00:0))<<1;

如果您担心事情会停留在范围内等(“未定义的行为”),您可以这样做

int16 diff16 = diff;
diff16 = (diff16 | ((diff16 & 0x80)?0x7F00:0))<<1;

这绝不会产生超出范围的数字。

不过,最干净的解决方案似乎是“cast and shift”:

diff16 = (signed char)diff; // recognizes and preserves the sign of diff
diff16 = (short int)((unsigned short)diff16)<<1; // left shift, preserving sign

这会产生预期的结果,因为编译器会自动处理第一行中的符号位(因此不需要掩码);在第二行中,它对 unsigned int 进行左移(根据标准对溢出进行了很好的定义);最后的转换回short int确保数字被正确解释为负数。我相信这种形式的构造永远不会“未定义”。

于 2013-04-11T02:55:52.467 回答