Ste*_*314 4 compiler-construction assembly code-generation cpu-registers
这个问题和我下面的答案主要是为了回应另一个问题中的一个混乱领域.
在答案的最后,有一些问题WRT"易变"和线程同步,我不是完全有信心 - 我欢迎评论和替代答案.然而,问题的关键主要涉及CPU寄存器及其使用方式.
Ste*_*314 15
寄存器是CPU中的"工作存储".它们非常快,但资源非常有限.通常,CPU具有一小组固定的命名寄存器,这些名称是该CPU机器代码的汇编语言约定的一部分.例如,32位Intel x86 CPU有四个主要数据寄存器,分别是eax,ebx,ecx和edx,以及许多索引和其他更专用的寄存器.
严格来说,现在情况并非如此 - 例如,注册重命名很常见.有些处理器有足够的寄存器,它们对它们进行编号而不是命名它们.但是,它仍然是一个很好的基本模型.例如,寄存器重命名用于保留此基本模型的错觉,尽管无序执行.
在手动编写的汇编程序中使用寄存器往往具有简单的寄存器使用模式.在子程序或其中一些重要部分的持续时间内,一些变量将纯粹保存在寄存器中.其他寄存器用于读 - 修改 - 写模式.例如...
mov eax, [var1]
add eax, [var2]
mov [var1], eax
Run Code Online (Sandbox Code Playgroud)
IIRC,这是有效的(虽然效率很低)x86汇编程序代码.在摩托罗拉68000上,我可能会写......
move.l [var1], d0
add.l [var2], d0
move.l d0, [var1]
Run Code Online (Sandbox Code Playgroud)
这次,源通常是左参数,目的地在右侧.68000有8个数据寄存器(d0..d7)和8个地址寄存器(a0..a7),其中7个IIRC也用作堆栈指针.
在6510(回到古老的Commodore 64上)我可能会写...
lda var1
adc var2
sta var1
Run Code Online (Sandbox Code Playgroud)
这里的寄存器主要隐含在指令中 - 上面都使用A(累加器)寄存器.
请原谅这些例子中的任何愚蠢错误 - 我至少15年没有写过任何大量的"真实"(而不是虚拟)汇编程序.不过,原则就是重点.
寄存器的使用特定于特定代码片段.寄存器的含义基本上与其中的最后一条指令无关.程序员有责任跟踪代码中每个点的每个寄存器中的内容.
在调用子例程时,调用者或被调用者必须负责确保没有冲突,这通常意味着寄存器在调用开始时被保存到堆栈中,然后在最后读回.中断也会出现类似问题.像负责保存寄存器(调用者或被调用者)的人通常是每个子例程的文档的一部分.
编译器通常会以比人类程序员更复杂的方式决定如何使用寄存器,但它的工作原理相同.从寄存器到特定变量的映射是动态的,并且根据您正在查看的代码片段而显着变化.保存和恢复寄存器主要是根据标准约定来处理的,尽管编译器可能在某些情况下即兴创建"自定义调用约定".
通常,函数中的局部变量被设想为存在于堆栈中.这是C中"auto"变量的一般规则.由于"auto"是默认值,因此这些是正常的局部变量.例如...
void myfunc ()
{
int i; // normal (auto) local variable
//...
nested_call ();
//...
}
Run Code Online (Sandbox Code Playgroud)
在上面的代码中,"i"可能主要在寄存器中.它甚至可以从一个寄存器移动到另一个寄存器并随着函数的进行而返回.但是,当调用"nested_call"时,该寄存器中的值几乎肯定会在堆栈上 - 或者因为变量是堆栈变量(不是寄存器),或者因为保存了寄存器内容以允许nested_call自己的工作存储.
在多线程应用程序中,普通局部变量是特定线程的本地变量.每个线程都有自己的堆栈,当它运行时,独占使用CPU寄存器.在上下文切换中,保存这些寄存器.无论是在寄存器中还是在堆栈上,都不会在线程之间共享局部变量.
这种基本情况在多核应用程序中保留,即使两个或多个线程可能同时处于活动状态.每个内核都有自己的堆栈和自己的寄存器.
存储在共享内存中的数据需要更加小心.这包括全局变量,类和函数中的静态变量以及堆分配的对象.例如...
void myfunc ()
{
static int i; // static variable
//...
nested_call ();
//...
}
Run Code Online (Sandbox Code Playgroud)
在这种情况下,函数调用之间保留"i"的值.保留主存储器的静态区域以存储该值(因此名称为"静态").原则上,在调用"nested_call"期间不需要任何特殊操作来保留"i",并且乍一看,可以从任何内核(甚至是单独的CPU)上运行的任何线程访问该变量.
但是,编译器仍在努力优化代码的速度和大小.对主存储器的重复读取和写入比寄存器访问慢得多.编译器几乎肯定会选择不遵循上述简单的读-修改-写模式,而是将保持在寄存器中的值相对较长时间,避免重复读取和写入相同的内存.
这意味着在一个线程中进行的修改可能在一段时间内不会被另一个线程看到.两个线程可能最终对上面的"i"的值有不同的看法.
没有神奇的硬件解决方案.例如,没有用于在线程之间同步寄存器的机制.对于CPU,变量和寄存器是完全独立的实体 - 它不知道它们需要同步.在不同线程中的寄存器之间或在不同的内核上运行时肯定没有同步 - 没有理由相信另一个线程在任何特定时间使用相同的寄存器用于相同的目的.
部分解决方案是将变量标记为"易失性"......
void myfunc ()
{
volatile static int i;
//...
nested_call ();
//...
}
Run Code Online (Sandbox Code Playgroud)
这告诉编译器不要优化对变量的读写操作.处理器没有波动性的概念.此关键字告诉编译器生成不同的代码,按分配指定立即读取和写入内存,而不是通过使用寄存器来避免这些访问.
然而,这不是多线程同步解决方案 - 至少本身并不是这样.一种适当的多线程解决方案是使用某种锁来管理对这种"共享资源"的访问.例如...
void myfunc ()
{
static int i;
//...
acquire_lock_on_i ();
// do stuff with i
release_lock_on_i ();
//...
}
Run Code Online (Sandbox Code Playgroud)
这里发生的事情比立即显而易见的要多.原则上,不是将"i"的值写回其为"release_lock_on_i"调用准备好的变量,而是可以将其保存在堆栈中.就编译器而言,这并非不合理.无论如何它都在进行堆栈访问(例如保存返回地址),因此将堆栈中的寄存器保存起来可能比将其写回"i"更有效 - 比访问完全独立的内存块更加缓存友好.
不幸的是,释放锁定功能还不知道变量还没有被写回内存,所以无法解决它.毕竟,该函数只是一个库调用(真正的锁定版本可能隐藏在一个更深层嵌套的调用中),并且该库可能已经在您的应用程序之前编译了几年 - 它不知道其调用者如何使用寄存器或堆.这是我们使用堆栈的原因的一个重要部分,以及为什么调用约定必须标准化(例如谁保存寄存器).释放锁定功能不能强制调用者"同步"寄存器.
同样,您可以使用新库重新链接旧应用程序 - 调用者不知道"release_lock_on_i"的作用或方式,它只是一个函数调用.它不知道它需要先将寄存器保存回存储器.
要解决这个问题,我们可以带回"不稳定".
void myfunc ()
{
volatile static int i;
//...
acquire_lock_on_i ();
// do stuff with i
release_lock_on_i ();
//...
}
Run Code Online (Sandbox Code Playgroud)
我们可以在锁定处于活动状态时临时使用普通局部变量,以便编译器有机会在该短暂时间段内使用寄存器.但原则上,应尽快释放锁,因此不应该有那么多代码.但是,如果我们这样做,我们会在释放锁之前将临时变量写回"i",而"i"的波动性确保将其写回主存储器.
原则上,这还不够.写入主存储器并不意味着您已经写入主存储器 - 存在多层缓存以在其间进行遍历,并且您的数据可以暂时位于这些层中的任何一个层中.有一个"记忆障碍"的问题在这里,我不知道这个很大 - 但幸运的是这个问题的线程同步调用的责任,如锁获取和释放上述呼吁.
但是,此内存屏障问题并未消除对"volatile"关键字的需求.
Ben*_*tto 13
CPU寄存器是CPU芯片上的小数据存储区域.对于大多数体系结构而言,它们是所有操作发生的主要位置(数据从内存加载,操作并推回).
无论运行什么线程都使用寄存器并拥有指令指针(指示下一条指令).当另一个线程操作系统互换,所有的CPU状态,包括寄存器和指令指针,得到的地方保存关闭,有效的冷冻干燥时,下一个起死回生的线程的状态.
当然,还有更多关于所有这些的文档.关于登记的维基百科. Wikipedia关于上下文切换.对于初学者.编辑:或阅读Steve314的答案.:)