本节通过一个write-through缓存的例子,详细讲解了该规约的设计考虑及遇到的问题与其解决方式,引入了之前没有用到的LET/IN关键字,用于定义函数的局部变量,方便分层拆解大而复杂的规约。
现在我们来定义一个简单的 w r i t e − t h r o u g h write-through write−through缓存,它实现内存规约,其系统架构图参见图 5.4 5.4 5.4:每个处理器 p p p与一个本地控制器通信,控制器维护三个状态组件: b u f [ p ] buf[p] buf[p]、 c t l [ p ] ctl[p] ctl[p]和 c a c h e [ p ] cache[p] cache[p]。 c a c h e [ p ] cache[p] cache[p]的值表示处理器的缓存; b u f [ p ] buf[p] buf[p]和 c t l [ p ] ctl[p] ctl[p]与其在内部内存规约(模块 I n t e r n a l M e m o r y InternalMemory InternalMemory)中起相同的作用。(不过,正如我们将在后面看到的, c t l [ p ] ctl[p] ctl[p]会引入一个额外的值“ w a i t i n g waiting waiting”。)这些本地控制器通过总线与主存储器 w m e m wmem wmem彼此通信。从处理器到主存的请求都进入长度为 Q L e n QLen QLen的队列 m e m Q memQ memQ。
处理器 p p p的写请求是通过动作 D o W r ( p ) DoWr(p) DoWr(p)来执行的。这是一个 w r i t e − t h r o u g h write-through write−through缓存,这意味着每个写请求都会更新主存。因此, D o W r ( p ) DoWr(p) DoWr(p)操作将值写入 c a c h e [ p ] cache[p] cache[p]并将写请求添加到 m e m Q memQ memQ的尾部。当写请求到达 m e m Q memQ memQ的头部时,动作 M e m Q W r MemQWr MemQWr将值存储到 w m e m wmem wmem中。 D o W r ( p ) DoWr(p) DoWr(p)操作还会为其他在其缓存中有相同地址副本的处理器 q q q更新 c a c h e [ q ] cache[q] cache[q]。
处理器 p p p的读请求由动作 D o R d ( p ) DoRd(p) DoRd(p)执行,它先从缓存中获取值。如果该值不在缓存中,动作 R d M i s s ( p ) RdMiss(p) RdMiss(p)将该读请求添加到 m e m Q memQ memQ的尾部,并将 c t l [ p ] ctl[p] ctl[p]的值设置为“ w a i t i n g waiting waiting”,当排队中的请求到达 m e m Q memQ memQ的头部时,动作 M e m Q R d MemQRd MemQRd读取该值并将其放入 c a c h e [ p ] cache[p] cache[p],同时使能 D o R d ( p ) DoRd(p) DoRd(p)操作。
我们可能期望 M e m n Q R d MemnQRd MemnQRd操作直接从 w m e m wmem wmem中读取值。但是,如果读请求后面的 m e m Q memQ memQ队列中有对该地址的写操作,则可能会导致错误。在这种情况下。该操作可能导致两个处理器在其缓存中对同一地址有不同的值:一个是读请求的处理器获取到的内存值,另一个是其后发出写请求的处理器写入的值。因此,如果 m e m Q memQ memQ队列中存在对该地址的写入请求,则 M e m Q R d MemQRd MemQRd从中读取最后一个写入的值,否则从 w m e m wmem wmem中读取值。
从处理器 p p p的缓存中清除某个地址的操作是由一个单独的动作 E v i c t ( p ) Evict(p) Evict(p)表示的。由于所有缓存的值已写入内存,清除操作只是从缓存中删除地址就够了。在空间受限之前,没有理由删除一个地址,因此在实现中,只有当从处理器 p p p处接收到对未缓存地址的请求且 p p p的缓存已满时,才会执行此操作。不过这只是一个性能优化,它不影响算法的正确性,所以它没有出现在规约中。在规约中,我们允许 p p p在任何时候清除一个缓存的地址——除非这个地址是为一个读请求 M e m Q R d MemQRd MemQRd操作放入缓存的,而读请求的 D o R d ( p ) DoRd(p) DoRd(p)操作还没有被执行。这是当 c t l [ p ] ctl[p] ctl[p]等于“ w a i t i n g waiting waiting”,而 b u f [ p ] . a d r buf[p].adr buf[p].adr等于缓存地址时的情况。
动作 R e q ( p ) Req(p) Req(p)和 R s p ( p ) Rsp(p) Rsp(p),分别代表处理器 p p p发出一个请求和内存回给处理器 p p p一个应答的操作,与内存规约的相应动作相同,除了它们还保留了新的变量 c a c h e cache cache和 m e m Q memQ memQ不变,还有保持不变的是变量 w m e m wmem wmem而不是 m e m mem mem。
要定义所有这些动作,我们必须确定处理器缓存和内存请求队列是如何由变量 m e m Q memQ memQ和 c a c h e cache cache表示的。我们令 m e m Q memQ memQ为 ⟨ p , r e q ⟩ \langle p,req \rangle ⟨p,req⟩这种形式,其中, r e g reg reg是一个请求, p p p是发出请求的处理器。对于任何内存地址 a a a,我们让 c a c h e [ p ] [ a ] cache[p][a] cache[p][a]为地址 a a a在 p p p的 c a c h e cache cache中的值( a a a在 p p p的 c a c h e cache cache中的副本)。如果 p p p的缓存中没有 a a a的副本,我们让缓存 c a c h e [ p ] [ a ] = N o V a l cache[p][a] = NoVal cache[p][a]=NoVal。
实际的规约我们放在模块 W r i t e T h r o u g h C a c h e WriteThroughCache WriteThroughCache中,我现在带大家过一遍这个规约,解释一些我们以前没有遇到过的细节和符号。
E X T E N D S EXTENDS EXTENDS、声明语句和 A S S U M E ASSUME ASSUME大家都很熟悉了,我们这里重用了来自 I n t e r n a l M e r n o r y InternalMernory InternalMernory模块的一些定义,通过一个 I N S T A N C E INSTANCE INSTANCE语句实例化了该模块的一个副本,除了用 w m e m wmem wmem代替 m e m mem mem,模块 I n t e r n a l M e m n o r y InternalMemnory InternalMemnory的其他参数都是由模块中同名参数实例化的。
初始谓词 I n i t Init Init包含合取词 M ! I I n i t M!IInit M!IInit,它断言 c t l ctl ctl和 b u f buf buf具有与内部内存规约相同的初始值,而 w m e m wmem wmem具有与 m e m mem mem在该规约中相同的初始值。 w r i t e − t h r o u g h write-through write−through缓存允许 c t l [ p ] ctl[p] ctl[p]拥有在内部内存规约中没有的值“ w a i t i n g waiting waiting”,因此我们不能重用内部内存的类型不变式 M ! T y p e I n v a r i a n t M!TypeInvariant M!TypeInvariant。因此,公式 T y p e I n v a r i a n t TypeInvariant TypeInvariant显式地定义了 w m e m wmem wmem、 c t l ctl ctl和 b u f buf buf的类型。 m e m Q memQ memQ的类型是 ⟨ p , r e q ⟩ \langle p,req \rangle ⟨p,req⟩键值对序列的集合。
接下来模块定义了谓词 C o h e r e n c e Coherence Coherence,声明了 w r i t e − t h r o u g h write-through write−through缓存基本的一致性属性:对于任意处理器 p p p和 p p p和任意地址 a a a,如果 p p p和 q q q的缓存中都有地址 a a a的副本,那么这些副本是相等的。注意这个技巧,我们用 x ∉ { y , z } x \notin \{y,z\} x∈/{y,z}而不是等价但更长的公式 ( x ≠ y ) ∧ ( x ≠ z ) (x \neq y) \land (x \neq z) (x=y)∧(x=z)。
动作 R e q ( p ) Req(p) Req(p)和 R s p ( p ) Rsp(p) Rsp(p)表示处理器发送了一个请求和接收到一个应答,它们本质上与模块 I n t e r n a l M e m o r y InternalMemory InternalMemory中定义的相应的动作相同。但是,它们还必须指明不存在于模块 I n t e r n a l M e m o r y InternalMemory InternalMemory中的变量 c a c h e cache cache和 m e m Q memQ memQ保持不变。
在 R d M i s s RdMiss RdMiss的定义中,表达式 A p p e n d ( m e m Q , ⟨ p , b u f [ p ] ⟩ ) Append(memQ,\langle p, buf[p] \rangle) Append(memQ,⟨p,buf[p]⟩)是通过将元素 ⟨ p , b u f [ p ] ⟩ \langle p, buf[p] \rangle ⟨p,buf[p]⟩附加到 m e m Q memQ memQ队尾而获得的序列。
动作 D o R d ( p ) DoRd(p) DoRd(p)表示从 p p p的缓存中读的操作,如果 c t l [ p ] = " b u s y " ctl[p] ="busy" ctl[p]="busy",则该地址最初位于缓存中。如果 c t l [ p ] = " w a i t i n g " ctl[p] = "waiting" ctl[p]="waiting",则该地址刚从内存中读入缓存。
动作 D o W r ( p ) DoWr(p) DoWr(p)将值写入到 p p p的缓存中,并在其他具有相同副本的处理器缓存中更新该值。它还将写请求放入 m e m Q memQ memQ队列。在一个实现中,请求被放到总线上,总线将它传送到其他缓存和 m e m Q memQ memQ队列。在我们对系统的高一级视图中,我们将所有这些表示为一个单一步骤。
D o W r DoWr DoWr的定义引入了 T L A + TLA+ TLA+的 L E T / I N LET/IN LET/IN结构。 L E T LET LET子句由一系列定义组成,其定义域一直延伸到 I N IN IN子句的末尾。在 D o W r DoWr DoWr的定义中, L E T LET LET子句定义:在 I N IN IN子句的范围内, r = b u f [ p ] r=buf[p] r=buf[p]。观察 r r r的定义包含了在 D o W r DoWr DoWr定义的参数 p p p。因此,我们不能把 r r r的定义移到 D o W r DoWr DoWr的定义之外。
L E T LET LET中的定义与模块中的普通定义一样,特别是,它可以有参数。这些局部定义可以通过使用运算符替代常见的子表达式来缩短表达式。在 D o W r DoWr DoWr的定义中,我用单独的符号 r r r取代了缓冲区的五个实例,这是一个愚蠢的做法,因为它几乎没有缩短定义的长度还要求读者记住新的符号 r r r的定义。但使用 L E T LET LET子句消除常见的子表达式通常可以大大缩短和简化表达式。
一个 L E T LET LET子句还可以使一个表达式更容易阅读,即使它定义的操作符只在 I N IN IN表达式中出现一次。我们使用一系列定义来编写规约,而不是仅仅定义一个单一的整体公式,因为以小块形式呈现公式更容易让人理解。 L E T LET LET构造使得将一个公式分解成更小的部分的过程更有层次性。 L E T LET LET可以作为 I N IN IN表达式的子表达式出现,嵌套的 L E T LET LET在大型复杂的规约中很常见。
接下来是状态函数 v m e m vmem vmem的定义,在其后的动作 M e m Q R d MemQRd MemQRd的定义中会用到它。它等于执行了当前 m e m Q memQ memQ中所有写操作之后,主存 w m e m wmem wmem将拥有的值。回顾一下,动作 M e m Q R d MemQRd MemQRd读取的值必须为最接近一次的写入该地址的值——一个可能仍在 m e m Q memQ memQ中的值,该值是 v m e m vmem vmem中的一个。函数 v m e m vmem vmem是根据递归定义的函数 f f f定义的,其中 f [ i ] f[i] f[i]是执行完 m e m Q memQ memQ中的第一个 i i i操作后 w m e m wmem wmem的值。注意, m e m Q [ i ] [ 2 ] memQ[i][2] memQ[i][2]是序列 m e m Q memQ memQ中的第 i i i个元素 m e m Q [ i ] memQ[i] memQ[i]的第二个组件(请求)。
接下来的两个动作 M e n Q W r MenQWr MenQWr和 M e m Q R d MemQRd MemQRd表示在 m e m Q memQ memQ队列头部处理请求的操作: M e m Q W r MemQWr MemQWr用于写请求, M e m Q R d MemQRd MemQRd用于读请求。这些动作还使用 L E T LET LET来进行局部定义。在这里, p p p和 r r r的定义可以在 M e m Q W r MemQWr MemQWr的定义之前挪动。事实上,我们可以通过用一个全局定义(在模块内)替换 r r r的两个本地定义来节省空间。但是,以这种方式将 r r r定义为全局的话会有点分散注意力,因为 r r r只用在 M e m Q W r MemQWr MemQWr和 M e m Q R d MemQRd MemQRd的定义中。相反,将这两个动作合并为一个可能更好。不管您是将定义放入 L E T LET LET中,还是作为全局变量,都应该取决于怎样才能使规约更易于阅读。
动作 E v i c t ( p , a ) Evict(p, a) Evict(p,a)表示从处理器 p p p的缓存中移除地址 a a a的操作。如上所述,我们允许在任何时候删除一个地址,除非这个地址只是为了满足一个挂起的读请求而写的,这是当且仅当 c t l [ p ] = " w a i t i n g " ctl[p]="waiting" ctl[p]="waiting"和 b u f [ p ] . a d r = a buf[p].adr=a buf[p].adr=a的情况。注意在动作的第二个合取词的 E X C E P T EXCEPT EXCEPT表达式中使用了“双下标”,这个合取词是“给 c a c h e [ p ] [ a ] cache[p][a] cache[p][a]赋值 N o V a l NoVal NoVal”。如果地址 a a a不在 p p p的缓存中,那么 c a c h e [ p ] [ a ] cache[p][a] cache[p][a]已经等于 N o V a l NoVal NoVal,一个 E v i c t ( p , a ) Evict(p, a) Evict(p,a)步骤是一个重叠本周。
下一状态动作 N e x t Next Next和完整规约 S p e c Spec Spec的定义很简单。该模块以下面讨论的两个定理结束。