因为存储速度和成本之间的问题,电脑的整个存储系统按照离CPU从近到远可以分为4级:寄存器、cache缓存、主内存、硬盘。
离CPU最近的寄存器,读写速度最快。
离CPU最远的硬盘,读写速度最慢。
包括C语言在内的大多数语言,是不需要关注寄存器怎么分配的。这部分的工作被编译器处理了。
当然,汇编语言是需要手动分配寄存器的。
程序员在写汇编时,会按照自己的直觉给出一个寄存器分配方案。
例如 5 / 3 = 1;汇编是这么写的:
mov 5, eax
xor edx, edx
mov 3, ecx
div ecx
这时,商在eax里,余数在edx里。英特尔的CPU就是这么设计除法指令的。
除法使用固定的寄存器eax和edx,是CISC架构的缺点,让寄存器的分配变得麻烦。
如果是int a = 5, b = 3, c = a / b;
那么就要尽量给c分配寄存器eax,因为除法的商默认就在eax里,这样可以提高生成的汇编码的效率。
如果是c = a % b,那么就要尽量给c分配寄存器edx,因为余数默认就在edx里。
因为寄存器只有16个,在程序规模较大的时候,是没法这么理想的分配寄存器的。
在编译器里,寄存器的分配是根据变量之间的活跃度来的:同时活跃的变量,不能使用同一个寄存器,否则数据就互相覆盖了。
c = a / b这行代码的被除数a和除数b肯定是同时活跃的,在除法指令运行的那一刻它们必须同时有效(而且互相覆盖)。
c与a、b并不是同时活跃的,它是在除法运行之后开始活跃。
如果变量a在这行代码之后不再使用,那么c和a是可以共用eax的。
如果后续还要使用a,那么就不能共用eax。例如:
int a = 5, b = 3;
int c = a / b;
c += a; // a在这里还是活跃的,不能在第二行c = a / b时被覆盖。
这时就只能给c分配eax,同时edx被除法指令占用,所以a和b只能使用ecx和ebx。
以上代码翻译成汇编:
mov 5, ecx // a使用ecx
mov 3, ebx // b使用ebx
mov ebx, eax // 加载被除数的最低32位到eax
xor edx, edx // 被除数的高32位清零
div ebx // 真正的除法运算只是这一条
add ecx, eax // c += a

上面2行代码c = a / b, c += a的寄存器冲突图,如上。
给这个图的3个顶点a、b、c着色,被冲突线连接的两个顶点不能是同一个颜色,即不能分配同一个寄存器。
因为CISC的除法使用edx,我们把它也作为一个约束条件添加到这个图里:变量a是不能使用它的,因为它会被除法运算的余数覆盖,而a在c += a这行代码还要使用。
c可以使用它,但c作为除法运算的商应该优先分配eax,否则就还需要多1条mov eax, edx的汇编码。

64位寄存器的字节分配
实际CPU的寄存器是个64位的寄存器组,它的最低8位AL、次低8位AH、最低16位AX、最低32位EAX、整个寄存器RAX的关系如图。
al和rax是冲突的:
char buf[8] = {0};
char c = 'A';
char* p = buf;
*p += c;
如果指针p使用了rax,那么char类型的变量c就不能再使用al,反之也一样。
代码 *p += c里,指针变量p和字符变量c是同时活跃的。
这种情况可以通过掩码来判断,可以用1个二进制位表示寄存器的1个字节:
1,RAX是8字节的寄存器,掩码就是0xff。
2,al是1字节的寄存器,掩码就是0x1。
3,ah的掩码是0x2,因为它使用的是第2个字节。
4,ax的掩码是0x3,2个字节。
5,eax的掩码是0xf,4个字节。
如果掩码的与运算不为0,就是互相冲突的寄存器,不能用于同一个变量。
当然不是同一个寄存器组的寄存器,肯定是不冲突的。
ah和al是不冲突的,因为0x2 & 0x1 == 0。
ah和ax是冲突的,因为0x2 & 0x3 != 0。
