文章目录
- Cache Memory
- 完整代码
Cache Memory
//Addresses and data
sig Addr {}
sig Data {}
//A cache system consists of main memory and cached memory, but mapping addresses to data
one sig CacheSystem {
var main, cache: Addr -> lone Data
}
//Initially there is no memory allocated
pred init [c: CacheSystem] {
no c.main + c.cache
}
//Write data to a specified address in the cache
pred write [c : CacheSystem, a: Addr, d: Data] {
c.main' = c.main
c.cache' = c.cache ++ a -> d
}
//Read data from a specified address in the cache
pred read [c: CacheSystem, a: Addr, d: Data] {
some d
d = c.cache [a]
c.main' = c.main
c.cache' = c.cache
}
//Load some data from the main memory in the cache
//Note that this is non-deterministic: it does not specific what to load. This is left to the implementation
pred load [c : CacheSystem] {
some addrs: set c.main.Data - c.cache.Data |
c.cache' = c.cache ++ addrs <: c.main
c.main' = c.main
}
//Flush memory from the cache back into the main memory.
//Note this is also non-deterministic
pred flush [c : CacheSystem] {
some addrs: some c.cache.Data {
c.main' = c.main ++ addrs <: c.cache
c.cache' = c.cache - addrs -> Data
}
}
//If a load is performed between a write and a read, this should not be observable.
//That is, if we perform the sequence: read, write, load, and read,
//then reading the same address in both reads should return the same value,
//unless the write in between overwrote that value (i.e. a1=a2)
assert LoadNotObservable {
all c : CacheSystem, a1, a2: Addr, d1, d2, d3: Data |
{
read [c, a2, d2];
write [c, a1, d1];
load [c];
read [c, a2, d3]
} implies d3 = (a1=a2 => d1 else d2)
}
assert LoadPreserves{
all c: CacheSystem | load[c] => c.main'=c.main
}
assert CannotReadInitially{
all c: CacheSystem, a: Addr, d: Data | init[c] => not read[c, a, d]
}
pred destroy[c: CacheSystem, a: Addr]{
c.main' = c.main - (a -> Data)
c.cache' = c.cache - (a -> Data)
}
assert OnlyOneMemoryDestroyed{
all c: CacheSystem, a: Addr | (destroy[c, a] => c.main'=c.main-(a->Data) and c.cache'=c.cache)
or (destroy[c, a] => c.cache'=c.cache-(a->Data) and c.main' = c.main)
}
//check LoadNotObservable for 5
//check LoadPreserves
//check CannotReadInitially
check OnlyOneMemoryDestroyed
In this workshop we will investigate checking whether the cache system satisfies the following
invariant
: No memory address is present in both the cache and the main memory.
Write a predicateinv
that encodes this property.
-
这里有两种写法:
pred Inv[c: CacheSystem]{ all a: Addr | (a in c.main.Data => a not in c.cache.Data) and (a in c.cache.Data => a not in c.main.Data) }
pred Inv2[c: CacheSystem] { // for a relation r : A -> B, r.B is a relational join that gives us the domain of r no (c.cache.Data & c.main.Data) }
-
本质思想是一样的,
c.cache.Data
会拿到所有cache
中的Address
,同样的c.main.Data
也会拿到所有main
中的address
,核心的思想是,一个address
a
如果出现在c.main
中,那么就还一定不能出现在c.cache
中,反之亦然。 -
为了验证这两种写法是否相同,我还写了一个
assertion
assert InvEqual{ all c: CacheSystem | Inv[c] <=> Inv2[c] } check InvEqual
-
没有
counterexample
代表结果完全相同。
题目:We can use Alloy to help us reason about whether this invariant will always hold, by getting it to check two things:
- (a) The initial state satisfies the invariant.
- (b) Each operation preserves the invariant.
The cache system has the operations:
write
,read
,load
, andflush
.
Writeassertions
to specify the two conditions above. Note that for the second condition we usually encode it as a collection of assertions: one for each of the operations.
Use Alloy to check these assertions. Which operations preserve the invariant and which don’t? Examine the counter-examples that Alloy finds to understand why certain operations don’t preserve the invariant. What are the smallest bounds for which the counter-examples show up?
assert writeInvariant{
all c: CacheSystem, a:Addr, d:Data | Inv[c] and write[c, a, d] => after(Inv[c])
}
check writeInvariant
writeInvariant
是有counter example
的
assert readInvariant{
all c: CacheSystem, a:Addr, d:Data | Inv[c] and read[c, a, d] => after(Inv[c])
}
check readInvariant
readInvariant
没有counter example
assert loadInvariant{
all c: CacheSystem | Inv[c] and load[c] => after(Inv[c])
}
check loadInvariant
loadInvariant
是有counter example
的
assert flushInvariant{
all c: CacheSystem | Inv[c] and flush[c] => after(Inv[c])
}
check flushInvariant
flushInvariant
没有counter example
反例最小范围:
- Alloy工具通过寻找反例来验证模型的断言。给定一个断言,Alloy尝试寻找一个实例,即一个特定的模型状态,使得断言不成立。这个实例被称为一个反例。
- 然而,对于一个给定的断言,可能存在多个反例。Alloy默认的策略是寻找尽可能小的反例。“最小范围"这个概念是指,为了找到反例,Alloy搜索的模型状态空间的大小。例如,如果你有一个涉及整数的断言,Alloy可能会在一个范围内搜索可能的整数值,如-10到10。这个范围就是搜索的"范围”。"最小范围"是指能找到反例的最小的搜索范围。
- 寻找最小范围的反例可以有助于更好地理解断言为什么会失败,因为这可以消除不必要的复杂性,并帮助你专注于导致断言失败的核心原因。
题目:Given that some of the operations don’t preserve the invariant, we can expect that from the initial state the system could reach a state in which the invariant doesn’t hold. However, the checks above don’t tell us what this sequence of operations might be. We will use Alloy to generate such a sequence of operations by modelling traces of transitions.
Write afact
first init that states that the first state in the trace is an initial state, i.e. satisfies the init predicate. Then write afact
trans
that states that it is always the case that either one of the cache system operations occurs or the state remains unchanged. Hint: You should write a predicate that specifies what it means for the cache system state to remain unchanged during a transition and use that predicate in your fact.
题目解析:这两段话是在讨论如何用Alloy模型来生成操作序列,这些操作序列可以从初始状态开始,并可能导致一个违反不变性条件的状态。
- 因为上面的题目中有些
assertion
可以找到counter example
这就代表这些过程违反了不变性
- 这就意味着从初始状态开始,系统可能从某一个 state 开始会到达一个不满足不变性的状态。
- 通俗来说,我们的程序是一系列状态的转变,从初始状态 s 0 s_0 s0 开始,这整个状态转变的序列我们称为
trace
,其中我们不知道经过哪个state
的时候就不满足不变性
了,然而,前面的检查并不能告诉我们导致这种结果的操作序列是什么。所以作者建议使用Alloy来生成这样的操作序列,方法是对状态转移的轨迹进行建模。- 第二段给出了两个任务。首先,要编写一个
fact
,命名为first init
,该fact
声明轨迹中的第一个状态是一个初始状态 s 0 s_0 s0,即满足init
谓词。接着,要编写一个名为trans
的fact
,该fact
声明在任何时候,都应满足以下条件:要么执行一个缓存系统操作(write, read, flush, load),要么状态保持不变。 这里需要编写一个谓词,来规定在转移过程中缓存系统状态保持不变的含义,然后在fact
中使用这个谓词。
总结一下,这两段话的主要内容是:在Alloy模型中,我们可以通过建立轨迹的模型,来生成可能导致违反不变性的操作序列。具体来说,我们需要定义两个fact,一个声明轨迹的初始状态,另一个声明在任何时候,要么进行一个操作,要么状态不变。
pred do_nothing[c: CacheSystem] {
c.cache' = c.cache and c.main' = c.main
}
fact trans {
always {
all c: CacheSystem |
do_nothing[c] or
(some a: Addr, d: Data | write[c,a,d]) or
(some a: Addr, d: Data | read[c,a,d]) or
load[c] or
flush[c]
}
}
问题:Write an
assertion
that says that it is always the case that theinvariant
holds. Use Alloy to check this assertion and to generate a trace in which the system starts in the initial state and then performs a sequence of operations to reach a state in which the invariant is violated.
- 在文档中,已经定义了一个名为
Inv
的不变式(invariant),该不变式表明了一个地址不可能同时在 cache 和 main 中存在。为了断言这个不变式总是被保持,我们可以在 Alloy 中添加如下的断言:
assert alwaysInvHolds {
always all c: CacheSystem | Inv[c]
}
check alwaysInvHolds
- 然而,需要注意的是,如果这个断言被 Alloy 模型检查器验证为假,那么表示存在一种可能的情况(或者称之为一个反例),即系统从初始状态开始,经过一系列的操作后,到达了一个违反了不变式 Inv 的状态。
问题:After you add the
first_init
fact
you might find that some of your earlier checks that produced counterexamples no longer do so. Specifically the checks that each operation preserves the invariant.
There are two ways to write these checks, in general, for a state type State (which in this workshop is the CacheMemory type) and an operation predicate op:
You should take one of the checks that produced a counter-example earlier and write it in each of the forms above. Work out which form produces the counter-example and which does not.
Think about why the first init fact prevents one of them from generating a counter-example but not
the other. What does this tell you about the meaning of these two ways of writing invariant preservation assertions, in trace-based Alloy models? (i.e. in models that model traces of operations, not just individual transitions).
问题解析:
这个问题关注的是如何编写能够保持不变量的检查(check)。给出了两种编写的方式,并询问你在给定初始化 fact 后,哪种方式会生成反例(即违反不变量的情况),哪种方式不会。
两种编写检查的方式分别是:
all s:State | inv[s] and op[s] => after inv[s]
always all s:State | inv[s] and op[s] => after inv[s]
这两种方式的差异在于
always
关键字。在 Alloy 中,always
表示的是 “总是” 的意思,意味着不仅是当前的状态满足条件,而且在所有的后续状态中,这个条件也会被满足。
问题希望你选择之前生成反例的一个检查,然后用这两种方式重新写一遍,看看哪种方式会生成反例,哪种方式不会。然后思考一下为什么给定初始化fact
后,其中一种方式不能生成反例,而另一种方式可以。这个结果对你理解在基于轨迹的 Alloy 模型(即那些建模操作序列的模型,而不仅仅是单个的转换)中,这两种方式的含义有什么启示?
- 初始化的
fact
强制了模型的初始状态必须满足某种条件。对于那种不使用always
的检查,如果初始状态满足了这个fact
,那么这个检查就有可能不会生成反例,因为所有满足 fact 的状态都满足了不变量。而对于使用always
的检查,即使初始状态满足了这个fact
,只要存在一个可能的后续状态违反了不变量,那么这个检查就会生成反例。 - 这表明,在基于轨迹的 Alloy 模型中,使用
always
的检查可以帮助我们发现那些只有在一系列操作之后才能发现的问题,而不使用always
的检查只能帮助我们发现在初始状态就能发现的问题。
完整代码
//Addresses and data
sig Addr {}
sig Data {}
//A cache system consists of main memory and cached memory, but mapping addresses to data
one sig CacheSystem {
var main, cache: Addr -> lone Data
}
//Initially there is no memory allocated
pred init [c: CacheSystem] {
no c.main + c.cache
}
//Write data to a specified address in the cache
pred write [c : CacheSystem, a: Addr, d: Data] {
c.main' = c.main
c.cache' = c.cache ++ a -> d
}
//Read data from a specified address in the cache
pred read [c: CacheSystem, a: Addr, d: Data] {
some d
d = c.cache [a]
c.main' = c.main
c.cache' = c.cache
}
//Load some data from the main memory in the cache
//Note that this is non-deterministic: it does not specific what to load. This is left to the implementation
//pred load [c : CacheSystem] {
// some addrs: set c.main.Data - c.cache.Data |
// c.cache' = c.cache ++ addrs <: c.main
// c.main' = c.main
//}
pred load [c : CacheSystem] {
some addrs: set c.main.Data - c.cache.Data |
c.cache' = c.cache ++ addrs <: c.main
c.main' = c.main
}
//Flush memory from the cache back into the main memory.
//Note this is also non-deterministic
pred flush [c : CacheSystem] {
some addrs: some c.cache.Data {
c.main' = c.main ++ addrs <: c.cache
c.cache' = c.cache - addrs -> Data
}
}
//If a load is performed between a write and a read, this should not be observable.
//That is, if we perform the sequence: read, write, load, and read,
//then reading the same address in both reads should return the same value,
//unless the write in between overwrote that value (i.e. a1=a2)
assert LoadNotObservable {
all c : CacheSystem, a1, a2: Addr, d1, d2, d3: Data |
{
read [c, a2, d2];
write [c, a1, d1];
load [c];
read [c, a2, d3]
} implies d3 = (a1=a2 => d1 else d2)
}
assert LoadPreserves{
all c: CacheSystem | load[c] => c.main'=c.main
}
assert CannotReadInitially{
all c: CacheSystem, a: Addr, d: Data | init[c] => not read[c, a, d]
}
pred destroy[c: CacheSystem, a: Addr]{
c.main' = c.main - (a -> Data)
c.cache' = c.cache - (a -> Data)
}
assert OnlyOneMemoryDestroyed{
all c: CacheSystem, a: Addr | (destroy[c, a] => c.main'=c.main-(a->Data) and c.cache'=c.cache)
or (destroy[c, a] => c.cache'=c.cache-(a->Data) and c.main' = c.main)
}
pred Inv[c: CacheSystem]{
all a: Addr | (a in c.main.Data => a not in c.cache.Data) and (a in c.cache.Data => a not in c.main.Data)
}
pred Inv2[c: CacheSystem] {
// for a relation r : A -> B, r.B is a relational join that gives us the domain of r
no (c.cache.Data & c.main.Data)
// alternatively we could write
//all a: Addr |
// (some c.cache[a] => no c.main[a]) and (some c.main[a] => no c.cache[a])
}
//check LoadNotObservable for 5
//check LoadPreserves
//check CannotReadInitially
//check OnlyOneMemoryDestroyed
assert InvEqual{
all c: CacheSystem |
Inv[c] <=> Inv2[c]
}
assert initInvariant {
all c: CacheSystem | init[c] => Inv[c]
}
assert writeInvariant{
always all c: CacheSystem, a:Addr, d:Data | Inv[c] and write[c, a, d] => after(Inv[c])
}
assert readInvariant{
all c: CacheSystem, a:Addr, d:Data | Inv[c] and read[c, a, d] => after(Inv[c])
}
assert loadInvariant{
all c: CacheSystem | Inv[c] and load[c] => after(Inv[c])
}
assert flushInvariant{
all c: CacheSystem | Inv[c] and flush[c] => after(Inv[c])
}
fact first_init{
all c: CacheSystem | init[c]
}
//
//fact trans{
// always all c: CacheSystem |
// // 保持不变
// c.main' = c.main and c.cache' = c.cache
// // 进行一些操作
// or (some a: Addr, d: Data | read[c, a, d])
// or (some a: Addr, d: Data | write[c, a, d])
// or load[c]
// or flush[c]
//}
pred do_nothing[c: CacheSystem] {
c.cache' = c.cache and c.main' = c.main
}
fact trans {
always {
all c: CacheSystem |
do_nothing[c] or
(some a: Addr, d: Data | write[c,a,d]) or
(some a: Addr, d: Data | read[c,a,d]) or
load[c] or
flush[c]
}
}
check initInvariant
//check flushInvariant
//check loadInvariant
//check readInvariant
//check writeInvariant
//check InvEqual