存储引擎在存储整数时,一般会使用最高位作为标志位,标记存储的整数是正数还是负数(参考),最高位也被称为“most significant bit (MSb)”。通常,最高位为1
则表示正数,最高位为0
,则表示负数。更进一步的,负数则会通过补码(参考:two’s complement)的方式表示。但是,InnoDB没有使用这种方法。
InnoDB 的整数存储
在死锁诊断时,偶然注意到,InnoDB 在存储整数时,与一般的系统是不同的。例如,int 类型存储 1 的时候,使用的表示是:0x80000001
。更多的示例可以参考右图:
整数值 | InnoDB 表示 |
---|---|
1 | 0x80000001 |
-1 | 0x7fffffff |
7 | 0x80000007 |
-7 | 0x7ffffff9 |
可以看到,这与一般的有符号型的整数存储是相反的。即:
- 正数表示时,最高位(MSb)为
1
- 负数表示时,最高位(MSb)为
0
关于这个问题,在 Stackoverflow
上也有看到有部分用户有类似的疑问:
- mysql highest bit Whether is 1 or 0 , when column is designed signed int
- how mysql represent two’s complement
本文将讨论为什么会这样。
考虑 8-bit 场景下的
这里来回顾一下“体系结构”中的最为基础的一些知识吧。
整数值 | 绝对值 | 绝对值的二进制 | 原码 | 2-补码 | Offset binary(“移码”) |
---|---|---|---|---|---|
1 | 1 | 0000-0001 | 0000-0001 | 0000-0001 | 1000-0001 |
-1 | 1 | 0000-0001 | 1000-0001 | 1111-1111 | 0111-1111 |
7 | 7 | 0000-0111 | 0000-0111 | 0000-0111 | 1000-0111 |
-7 | 7 | 0000-0111 | 1000-0111 | 1111-1001 | 01111001 |
说明:
移码有两种计算方式,结果是等价的,即:
- 直接将原始数据加上
2(n-1)
,然后转化为二进制即可以 - 将其补码,最高位进行一次翻转,即
“补码 XOR
2(n-1)
“
验证存储方式
为了确认 InnoDB 的整数处理,再MySQL 8.4.4的源码中找到如下 InnoDB 处理整型数据的代码:
if (type == DATA_INT) {
/* Store integer data in Innobase in a big-endian format,
sign bit negated if the data is a signed integer. In MySQL,
integers are stored in a little-endian format. */
byte *p = buf + col_len;
for (;;) {
p--;
*p = *mysql_data;
if (p == buf) {
break;
}
mysql_data++;
}
if (!(dtype->prtype & DATA_UNSIGNED)) {
*buf ^= 128;
}
ptr = buf;
buf += col_len;
这段代码中,先将字节序做了颠倒(从最高字节位开始,逐个字节进行拷贝存储),即将 MySQL 层面的小端(little-endian)转化为了InnoDB层面的(big-endian)存储。而后,再对最高位进行了一次翻转,即这里的:*buf ^= 128
操作。
即:先将数据在MySQL层面的表示做了大小端的转化并拷贝过来,然后,将最高位进行翻转。即,先将2补码的表示模式拷贝过来,再将最高位进行翻转。
什么要这么存储
在 MySQL/InnoDB 官方文档或者代码中,并没有关于该实现的说明。不过这么做,有一个非常明显的好处,即所有的整数表示的大小关系,与实际存储的数据(当中无符号型对待)的大小关系是一致的。
即,在上述的例子中:7 > 1 > -1 > -7
,而对应的编码表示,也有对应的大小关系:
0x80000007 > 0x80000001 >
0x7fffffff
> 0x7ffffff9
这里对这个问题做一个简单探讨。先说结论吧,这是一种较为典型的整数编码方式:Offset binary(“移码”)。即,将需要表示的整数,加上一个数值,让所有的整数映射到自然数空间。例如,在MySQL中使用32位的int
类型,需要表示的整数范围为[-231,
。那么,实际表示时,则加上231
]
。更为一般的,对于231
[-2(n-1), 2(n-1)]
之间的所有整数在表示时,都加上了2(n-1)
。即,建立的映射关系是:
f(i) = i + 2(n-1)
即对于任何要存储的整数i
,实际存储时都存储上述的f(i)
。而在实际运算时,则是,将补码的最高位进行一次翻转即可。
关于补码
例如,在 8 位二进制中,00000001
表示 +1
,而 11111111
代表 -1
。具体的,在表示-3
时,先取 3
的二进制 00000011
,再逐位取反 11111100
,最后加 1
得到 11111101
,即 -3
的补码表示。这种方式让计算机能够高效地进行整数运算,是典型的正负数的方法,该方法的更多优势可以参考:two’s complement。
补充说明
MySQL 层面的整数表示和 InnoDB 的整数存储是不同的。在“验证存储方式”小结中的代码中可以看到:
- MySQL使用了小端(little-endian),InnoDB层面使用了大端(big-endian)存储
- 在 MySQL 层面使用2-补码做有符号整数类型存储;而InnoDB层面使用了“移码”存储
Leave a Reply