代码之家  ›  专栏  ›  技术社区  ›  Steve314

什么是CPU寄存器,它们是如何使用的,特别是WRT多线程?

  •  4
  • Steve314  · 技术社区  · 16 年前

    下面这个问题和我的答案主要是为了回答另一个问题中的一个困惑的领域。

    在答案的最后,有一些关于“易失性”和线程同步的问题,我并不完全有信心——我欢迎评论和其他答案。然而,问题的关键在于CPU寄存器以及它们是如何使用的。

    2 回复  |  直到 10 年前
        1
  •  12
  •   Ben Zotto sberry    16 年前

    CPU寄存器是CPU硅上的小面积数据存储区。对于大多数体系结构,它们是所有操作发生的主要地方(数据从内存中加载、操作和推出)。

    无论哪个线程正在运行,都使用寄存器并拥有指令指针(指示下一个是哪个指令)。当操作系统在另一个线程中交换时,所有的CPU状态,包括寄存器和指令指针,都会保存在某个地方,有效地冻结干燥线程的状态,以备下次线程恢复使用。

    当然,还有很多关于所有这些的文档,遍布各地。 Wikipedia on registers. Wikipedia on context switching. 首先。编辑:或阅读Steve314的答案。:)

        2
  •  14
  •   Steve314    16 年前

    寄存器是CPU中的“工作存储器”。它们速度非常快,但资源非常有限。通常,一个CPU有一个小的固定的命名寄存器集,这些名称是该CPU机器代码的汇编语言约定的一部分。例如,32位Intel x86 CPU有四个主数据寄存器,分别是eax、ebx、ecx和edx,以及一些索引寄存器和其他更专业的寄存器。

    严格地说,这不是很正确的这些天-注册重命名,例如,是常见的。一些处理器有足够的寄存器来给它们编号,而不是给它们命名等等。然而,它仍然是一个很好的基本模型。例如,尽管执行顺序不对,但寄存器重命名用于保留此基本模型的假象。

    在手工编写的汇编程序中使用寄存器往往有一个简单的寄存器使用模式。在子程序或子程序的某些实质部分的持续时间内,一些变量将纯粹保存在寄存器中。其他寄存器在读-修改-写模式中使用。例如。。。

    mov eax, [var1]
    add eax, [var2]
    mov [var1], eax
    

    IIRC,这是有效的(尽管可能效率低下)x86汇编程序代码。在摩托罗拉68000上,我可能会写…

    move.l [var1], d0
    add.l  [var2], d0
    move.l d0, [var1]
    

    这一次,源通常是左边的参数,目的地在右边。68000有8个数据寄存器(D0..D7)和8个地址寄存器(A0..A7),A7 IIRC也用作堆栈指针。

    在一架6510型飞机上(回到老准将64号),我可能会写…

    lda    var1
    adc    var2
    sta    var1
    

    这里的寄存器主要是隐式的,在指令中-上面所有的都使用A(累加器)寄存器。

    请原谅这些例子中的任何愚蠢错误——我至少15年没有写过任何数量的“真实”(而不是虚拟)汇编程序。不过,原则是关键。

    寄存器的使用是特定于特定代码片段的。寄存器所保存的基本上就是最后一条指令留在其中的内容。程序员有责任在代码的每个点上跟踪每个寄存器中的内容。

    调用子例程时,调用方或被调用方必须负责确保没有冲突,这通常意味着寄存器在调用开始时保存到堆栈中,然后在结束时读取。中断也会出现类似的问题。像谁负责保存寄存器(调用者或被调用者)这样的事情通常是每个子例程文档的一部分。

    编译器通常会以比人类程序员更复杂的方式决定如何使用寄存器,但它的工作原理是相同的。从寄存器到特定变量的映射是动态的,并且根据您正在查看的代码片段有很大的变化。保存和恢复寄存器主要是根据标准约定来处理的,尽管在某些情况下编译器可能会临时采用“自定义调用约定”。

    通常,一个函数中的局部变量被想象为活在堆栈上。这是C语言中带有“auto”变量的一般规则。因为“auto”是默认值,所以它们是正常的局部变量。例如。。。

    void myfunc ()
    {
      int i;  //  normal (auto) local variable
      //...
      nested_call ();
      //...
    }
    

    在上述代码中,“i”可能主要保存在寄存器中。它甚至可以随着函数的进行从一个寄存器移到另一个寄存器,然后再移回来。但是,当调用“嵌套的_调用”时,该寄存器中的值几乎肯定会在堆栈上-要么因为变量是堆栈变量(不是寄存器),要么因为寄存器内容被保存以允许嵌套的_调用其自身的工作存储。

    在多线程应用程序中,普通局部变量是特定线程的局部变量。每个线程都有自己的堆栈,并且在运行时,CPU寄存器的独占使用。在上下文切换中,这些寄存器被保存。无论是在寄存器中还是在堆栈上,局部变量都不会在线程之间共享。

    即使两个或多个线程可能同时处于活动状态,多核应用程序也会保留这种基本情况。每个内核都有自己的堆栈和寄存器。

    存储在共享内存中的数据需要更加小心。这包括全局变量、类和函数中的静态变量以及堆分配的对象。例如。。。

    void myfunc ()
    {
      static int i;  //  static variable
      //...
      nested_call ();
      //...
    }
    

    在这种情况下,“i”的值在函数调用之间保留。主内存的一个静态区域被保留来存储这个值(因此名称为“static”)。原则上,在调用“嵌套调用”的过程中,不需要任何特殊操作来保存“i”,而且乍一看,可以从任何核心上运行的任何线程(甚至在单独的CPU上)访问该变量。

    但是,编译器仍然在努力优化代码的速度和大小。对主存储器的重复读写比寄存器访问慢得多。编译器几乎肯定会选择 要遵循上面描述的简单读-修改-写模式,但是会将寄存器中的值保留一段相对较长的时间,避免重复读和写到同一内存中。

    这意味着在一个线程中所做的修改在一段时间内可能不会被另一个线程看到。两条线最终可能会对上面的“i”的价值有非常不同的想法。

    没有神奇的硬件解决方案。例如,没有在线程之间同步寄存器的机制。对于CPU来说,变量和寄存器是完全独立的实体——它不知道它们需要同步。当然,不同线程中的寄存器或在不同核心上运行的寄存器之间没有同步——没有理由相信另一个线程在任何特定时间出于相同的目的使用相同的寄存器。

    部分解决方案是将变量标记为“volatile”…

    void myfunc ()
    {
      volatile static int i;
      //...
      nested_call ();
      //...
    }
    

    这告诉编译器不要优化对变量的读写。处理器没有波动性的概念。这个关键字告诉编译器生成不同的代码,按照分配的指定立即读写内存,而不是通过使用寄存器来避免这些访问。

    这是 然而,一个多线程同步解决方案——至少不是它本身。一个合适的多线程解决方案是使用某种锁来管理对这个“共享资源”的访问。例如。。。

    void myfunc ()
    {
      static int i;
      //...
      acquire_lock_on_i ();
      //  do stuff with i
      release_lock_on_i ();
      //...
    }
    

    这里发生的事情比显而易见的多。原则上,它可以保存在堆栈上,而不是将“i”的值写回为“release-lock-on-u-i”调用准备的变量。就编译器而言,这并不是不合理的。无论如何,它都在进行堆栈访问(例如,保存返回地址),因此将寄存器保存在堆栈上可能比将其写回“i”更有效——比访问完全独立的内存块更容易缓存。

    但不幸的是,释放锁函数不知道变量还没有被写回内存,所以无法对其进行任何修复。毕竟,这个函数只是一个库调用(真正的锁释放可能隐藏在一个更深入的嵌套调用中),而且这个库可能在应用程序之前编译了几年——它不知道 怎样 它的调用者使用寄存器或堆栈。这就是为什么我们要使用堆栈,为什么调用约定必须标准化(例如谁保存寄存器)的一个重要原因。释放锁定功能不能强制呼叫者“同步”寄存器。

    同样,你也可以将一个旧的应用程序重新链接到一个新的库中-调用方不知道“release-lock-on-u-i”是做什么的,也不知道如何做的,它只是一个函数调用。它不知道它需要先将寄存器保存回内存。

    为了解决这个问题,我们可以恢复“不稳定”状态。

    void myfunc ()
    {
      volatile static int i;
      //...
      acquire_lock_on_i ();
      //  do stuff with i
      release_lock_on_i ();
      //...
    }
    

    当锁处于活动状态时,我们可以临时使用一个普通的局部变量,以使编译器有机会在短时间内使用寄存器。不过,原则上,锁应该尽快释放,所以里面不应该有那么多代码。如果我们这样做,那么在释放锁之前,我们将临时变量写回“i”,“i”的波动性确保它被写回主内存。

    原则上,这还不够。写入主内存并不意味着您已经写入了主内存——在主内存之间有缓存层,您的数据可能会在其中任何一个层中停留一段时间。这里有一个“内存障碍”问题,我对此不太了解,但幸运的是,这个问题是线程同步调用(如上面的锁获取和释放调用)的责任。

    然而,内存屏障问题并没有消除对“volatile”关键字的需求。

    推荐文章