InnoDB 整数类型存储的sign bit的设置

存储引擎在存储整数时,一般会使用最高位作为标志位,标记存储的整数是正数还是负数(参考),最高位也被称为“most significant bit (MSb)”。通常,最高位为1则表示正数,最高位为0,则表示负数。更进一步的,负数则会通过补码(参考:two’s complement)的方式表示。但是,InnoDB没有使用这种方法。

InnoDB 的整数存储

在死锁诊断时,偶然注意到,InnoDB 在存储整数时,与一般的系统是不同的。例如,int 类型存储 1 的时候,使用的表示是:0x80000001。更多的示例可以参考右图:

整数值InnoDB 表示
10x80000001
-10x7fffffff
70x80000007
-70x7ffffff9

可以看到,这与一般的有符号型的整数存储是相反的。即:

  • 正数表示时,最高位(MSb)为1
  • 负数表示时,最高位(MSb)为0

关于这个问题,在 Stackoverflow上也有看到有部分用户有类似的疑问:

本文将讨论为什么会这样。

考虑 8-bit 场景下的

这里来回顾一下“体系结构”中的最为基础的一些知识吧。

整数值绝对值绝对值的二进制原码2-补码Offset binary(“移码”)
110000-00010000-00010000-00011000-0001
-110000-00011000-00011111-11110111-1111
770000-01110000-01110000-01111000-0111
-770000-01111000-01111111-100101111001

说明:

移码有两种计算方式,结果是等价的,即:

  • 直接将原始数据加上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

Your email address will not be published. Required fields are marked *