符号扩展的实现方式
最近在看一些代码,里面要实现 “长度截断 + 符号扩展”,自带实现是下面这样的
#define SEXT(x, len) ({ struct { int64_t n : len; } __x = { .n = x }; (int64_t)__x.n; })
但是这个东西只对于编译期长度固定的情况有效,对于运行期则大约需要使用bit操作比如下面这样
#define SIGNEX(v, sb) ((v) | (((v) & ((uint64_t)1 << (sb))) ? ~(((uint64_t)1 << (sb)) - 1) : 0))
上面这种实现方式非常巧妙:
- 前面一个部分负责 [0,sb-1] 这个范围的bits
- 后面一个部分负责 [sb,63] 这个范围的bits
如果bits超过64也是可以的,因为 `((uint64_t)1 << sb)` 会回绕过来(或者是>>),这个非常有趣。我这里做了一个验证。
int main() { for (int i = 0; i < 32; i++) { uint64_t a = (uint64_t)1 << (i); uint64_t b = (uint64_t)1 << (i + 64); printf("a = 0x%llx, b = 0x%llx\n", a, b); assert(a == b); } for (int i = 0; i < 32; i++) { uint64_t a = (uint64_t)0x8000000000000000 >> (i); uint64_t b = (uint64_t)0x8000000000000000 >> (i + 64); printf("a = 0x%llx, b = 0x%llx\n", a, b); assert(a == b); } }
我这里想了另外一个实现方式,思路就是完全使用指令本身的符号扩展功能:
- 现将这个数左移到64位最高位
- 然后算术右移回来,那么就自动实现了符号扩展
- 在这个思路上也可以实现零扩展(zero-extended)
uint64_t signext(uint64_t value, int width) { int shift = (sizeof(uint64_t) - width) * 8; int64_t ans = ((int64_t)value << shift) >> shift; return ans; } uint64_t zeroext(uint64_t value, int width) { int shift = (sizeof(uint64_t) - width) * 8; return (value << shift) >> shift; } uint64_t zeroext2(uint64_t value, int width) { uint64_t mask = (1ULL << mask) - 1; return value & mask; }
可以简单地验证下
int main() { struct Case { uint64_t value; int width; uint64_t exp; } cases[] = { {0x0ff, 1, (uint64_t)-1}, {0x07f, 1, 0x07f}, {0, 0, 0}, }; for (int i = 0; cases[i].value; i++) { uint64_t ans = signext(cases[i].value, cases[i].width); printf("case %d, ans = 0x%llx, exp = 0x%llx\n", i, ans, cases[i].exp); ans = signext(cases[i].value, cases[i].width + 64); printf("case %d, width + 64, ans = 0x%llx, exp = 0x%llx\n", i, ans, cases[i].exp); assert(ans == cases[i].exp); uint64_t zext = zeroext(cases[i].value, cases[i].width); assert(zext == cases[i].value); uint64_t zext2 = zeroext2(cases[i].value, cases[i].width); assert(zext2 == cases[i].value); } }