最近在看一些代码,里面要实现 “长度截断 + 符号扩展”,自带实现是下面这样的
#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);
}
}