MY GOAL: Save generated executions from Herd7 against a specific memory model for further learning.
背景介绍
这个是我当前在做的事情之一,里头需要得到一个小 program 在某个特定的 relaxed memory models 下的所有 executions / traces.
经过调研,Herd7 是能够做到我想要的事情的。之前我还帮老板准备了一个使用 herd7 来体验不同的 RMM 的 lab, 写在另一篇博客里了。Herd7 能够输出以下边这样的图片的格式输出 executions: 显然在生成的过程中肯定会存有每幅图对应的中间状态,我的目标就是把这些中间状态打印到一个文件里存起来。
在这一篇博客里,我大致记录了整个实验、扩展的流程,对于想要了解 herd7 工具的实现的同学应该是会有一点帮助的吧,仅此而已了。除此之外的一点点干货可能就是 OCaml 的一点配置经验了吧。不知道会不会有人认真看完全文呢 😂
herd7 的代码可以在这里可以找到,我自己 fork 后修改的版本在这里。
这里我基于的是 v7.43 的版本。其实我原本还尝试过 diy v5.99 (diy 是旧版本的名字,v7 改名了)里的代码的,因为觉得旧版本的代码可能会更短小、更方便理解。但是我在旧版本下好像 make
一下就自动 build + install 了.. 不方便调试,也没有找到生成的中间文件路径用于 merlin-mode 的代码解析(这个真的很关键!!),所以就算了。而且我随便对比了两个版本下的几个文件,比如 top/top_herd, mem/mem, 好像并没有特别大的变化。反正最后也基本满足目标了呗 😁。
OCaml 开发环境配置
Herd7 是用 OCaml 写的,所以我得先学习一下 OCaml 这门语言。在12年听 coursera 上的 PL 课的时候学过一点点的 Standard ML, 除此之外对 OCaml 就没什么了解了。不过上手起来还是挺快的,我一直以来给自己的要求都是「3天之内能够上手任何一门新语言」。为了熟悉 OCaml,
- 看了 TryOCaml 的所有
- 看了 OCaml 官网上列出来的所有 tutorials
- 然后做了做 tutorials 里头的 99 problems 的一些题目。我发现这其实就是「算法题 in OCaml」嘛。。
这些果然就基本足够了。除了后来为了看懂 Herd7 里的一些 module / functor 的高级写法,又看了看 Real World OCaml 这本书。然后语言上就很熟悉了。
关于开发环境,我并没有找到 OCaml 的什么大型 IDE, 部分原因是 Emacs + Merlin-mode 就已经很强力了!!虽然这次我还没有怎么尝试 debugger, 光 printf 就做完了.. 不得不说,merlin mode 真的是超棒的!帮了大忙了。我最常用的两个功能是 jump to definition 和 show type information.
关于 merlin mode 以及 emacs 的设置,在这里有更加详细的介绍,我的 .emacs 里也有设置好的部分可供参考。
下边是一些 emacs 里使用 OCaml 相关的一些常用快捷键:(启用)
- ctrl+c, ctrl+l:jump to definition. 在看代码的时候,这个几乎是最常用的了!
- 要用这个的话,需要先配置好 ocaml-merlin
- 我还自己添加了一个 .merlin 的配置文件(放在项目根目录下),描述哪些文件夹下有 source code, 哪些有 compiled binary file, 然后真的就脱胎换骨,效果拔群了!!感觉没有设置这个的话其实 merlin mode 也提供不了多大的好处..
- ctrl+c, &: 回到刚才 jump to definition 之前的位置..
- ctrl+t: 这个可以看选中部分(或者当前光标所在位置)的 type info, 有时候需要再按一次 ctrl+t 来显示完整的 type info.
代码理解
从 herd.ml 文件开始,在
let () = ...
那里开始是整个程序的 entry point,- 先是读取命令行参数设置各种环境变量,放在
Opts
和Model
这两个 module 内 - 然后从
from_file
函数开始解析,这个函数实际上是去调用ParseTest.Top
这个 module, 其会根据配置好的env
和filename
来从这个 litmus test 文件中开始执行。 - 它会把所有在命令行参数中传入的
files
都这么去做一遍
- 先是读取命令行参数设置各种环境变量,放在
所以下一步的重点是 parseTest.ml 文件中的
ParseTest.Top.from_file
函数,它会进一步调用内部的do_from_file
函数:- 先调用
Splitter
去解析 input litmus test program, 分出 initial state / program / final condition 等等不同的 section. - 这个 Splitter module 似乎是个在其它地方也有用到的公用代码,反正具体实现我并不关心 😓
- 然后就可以根据 splitter 得到的 architecture 信息,调用不同的具体的 parser 来解析这个 litmus test 了。
- 这些 arch 有:
PPC
/ARM
/AArch64
/X86
/MIPS
/C
/CPP
/LISA
- 看起来,这些 litmus test 是一定要有一个 architecture 的,或许是因为不同的 architecture 有着各自的 keyword, 不同平台的就别混起来了!所以只有他们自己可以解析。
- 这些 arch 有:
- 先调用
在 parseTest.ml parse 了 litmus test 的 syntax, 得到其 semantics 之后,会调用同一个文件里的
Top.Make
这个 module 调用其run
函数,在这个run
函数里,先生成了Test_herd
module (test_herd.ml), 应该只是定义了其内部的 state 包含哪些内容;然后通过Top_herd
module (top_herd.ml) 的run
函数去真正执行——execute test according to model!在 top_herd.ml 文件里可以看到,这里的
run
函数就是剩下的所有 generate + output 部分了。在一开始就调用了mem.ml
中的glommed_event_structures
函数:glommed_event_structures
的输出是一个 record, 里头的too_far
(bool) 说是可以丢弃一些 events, 我暂时不关心。重点是其中的第一个 field:event_structures : (int * S.M.VC.cnstrnts * S.event_structure) list ;
- 这里是 xxx list 所以已经是把所有可能的 execution graph 都给输出来了吧!!
- 第一个 int 我还不知道是啥,难道是在这个 list 中的编号??
- 第二个
S.M.VC.cnstrnts
, 唉,你们为啥不写全名呢 = = - 第三个
S.event_structure
展开来是Sem.Semantics.event_structure
- 而
Sem.Semantics
会☞到SemExtra.S
里头 - 而那里头的
event_structure
又会☞到Event.S.event_structure
那里 - 所以这么一路下来最终要到
event.ml
(~line160) 里头看每个 event structure graph 里头到底放了什么东西 = =procs
events
(set)intra_causality_data
(relation) 类似于 data dependency 吧intra_causality_control
(relation) 类似于 control dependency 吧control
(relation) 似乎指的是 if 这种的data_ports
(event set) 这个没太懂:Wvents that lead to the data port of a W. 所以是所有可能的 Write??output
(event set) 应该是最终外界 visible 的那些 events 吧
- 而每个 event 是有一个
eeid
最为唯一标识的
- 不过,在这个
glommed_event_structures
里得到的 event structures 里似乎仅仅是 events 为主呀,像是给 event 编个号啊、一些最最基本的信息等等。在 SemExtra 中有一个更大的数据结构 —— concrete, 这个里头应该才是所有 output 的总集合,里头包括了:- event structure, 这里是 Event 里头的 event_structure, 所以就是每个具体的 graph 有一个 concrete 咯
- rf map
- rfmap 的类型有点奇怪,是一个
write_to → read_from
的 map?! - 其实
write_to
表示的是 “the event” to write to, 一个write_to
是 to final (no further read) or another Load. 对应的,read_from
表示的是 “the event” to read from. 所以呀,这其实就是类似于俩 event 而已。 - 然后
write_to
作为 key, 其实就是 load event 作为 key, 因此是一个 rf-map 并非 wt-map 嘛
- rfmap 的类型有点奇怪,是一个
- final states
- po + iico
- po-same location
- pco: 似乎是 co 的 precursor
- store-load relation: 这里下边这4个是 deduced from rfmaps
- init-load relation
- last store relation
- atomic load / store relation
- 所以 很多函数里的
conc
参数原来说的就是 concrete 咯! - 这个 concrete 我去搜了下(搜
S.concrete
或者conc
关键字),它至少用在下边这么几个地方的:- pretty.ml 这个就是输出到 .dot file 了
- XXXMem.ml 这个应该是用各个 architecture 的信息来 examine entire state, 看里头东西到底对不对,可以继续转到
Minimal
/CAV12
/MachModelChecker
里去。 - 还有一个
kont
function 马上就会说到了
然后,top_herd.ml 文件里在生成了
event_structure
之后,会通过iter_rfms
函数去 generate and process rfmap. 这里才是真正真正得到 relation 的部分吧?!那个kont
就是这里用的!- 这个
iter_rfms
函数内部调用了 mem.ml 里的calculate_rf_with_cnstrnts
函数,那个是有带着详细注释的,难得啊!甚至还有对于kont
函数的描述,说是对于每个生成的 concrete event structures 都会调用一下kont
。就是在这里头 generate / process rfmap 的吧,里头会有 solve 等等的函数似乎可以决定具体的 rf relations.- 所谓的 event structures 其实还是比较 abstract 的,因为:some values and even memory locations in them are variables
- 在
calculate_rf_with_cnstrnts
函数中会传入kont
函数作为参数(传进来的是临时定义的call_model
),在这个函数里头:- 当成功 solve constraints 的时候,会调用
fold_mem_finals
函数,在里头构建一个个 concrete instance!! 用fold_cross
这个函数应该是用来一下生成多种不同的结果的吧。这里就是我想要关心的东西了呀!等下会说到,把这个生成的具体结果再根据 user-specified model 去验证一下,没问题的话就会输出打印了。 - 如没能成功 solve constraints 的话(cannot make such event structures concrete),会调用
when_unsolved
函数。如果判定是 unloop depth 不够或者 cyclic rfmaps 的话就会调用另一个专用函数kont_loop
函数来做事情。不过我看在现在的代码中传进来的kont_loop
函数仅仅是fun c -> c
这样子,这个真的会有区别么 = = 所以我猜测应该还没有实现完全,不过跟我也没有关系了,我关心的 Litmu test 都是没有 loop 的
- 当成功 solve constraints 的时候,会调用
- 当能够成功 solve constraints, 得到一个
concrete
的时候,calculate_rf_with_cnstrnts
函数会调用在 top_herd.ml 里的iter_rfms
函数里传入的call_model
函数参数:- 在
call_model
调用了check_test
函数,它会去调用 XXXMem.ml 所定义的check_event_structure
函数。比如说在 machModelCheck.ml (mach ≡ machine) 中的check_event_structure
函数是下边这样的。除了这个之外,还有 CAV12.ml 和 minimal.ml 里的实现,所以显然是下边这个 mach 的最通用了吧。 check_event_structure
里头是会引入 bell info. 这个 bell 我在 herd7 tool demo website 里见到过的,但是不知道它什么意思,猜测应该是一些具体的 configuration 吧。- 如果声明了要有 co 的话,还会自动去生成 co / fr 呢
- 如果是 machModelChecker 的话,因为是针对 user specified model 嘛,所以最后会去调用
run_interpret
这个函数,传kont
进去。这个会调用 interpreter.ml 里的代码,去 interpret 一个 user-specified model! interpreter.ml 好大呢,2k+ 行,基本就是他们的类 OCaml cat DSL 的 interpreter 了吧,我就不看了 😓 - 然后如果 interpretation 处理后的结果全部正确的话,会调用
kont
去输出:kont st res
. 代码中的注释说的是:For all success call kont, accumulating results.
- 在
- 这个
终于可以说具体的
kont
函数里头做了什么事情了,其实就是在 top_herd.ml 里的model_kont ochan test cstr
函数,它返回的是一个函数:- 它是这么用的:
kont conc conc.S.fs st.I.out_show st.I.out_flags res
(in machModelChecker.ml)kont conc conc.S.fs vb_pp Flag.Set.empty res
(in CAV12.ml)kont conc conc.S.fs pp_relns Flag.Set.empty res
(in minimal.ml)
- 所以可以确定它的参数分别表示:
- concrete state / final states / vbpp: print relations 的函数 / flags / c: results
- 接着我们再来看
model_kont
返回的函数里究竟做了什么事情。 - 首先,它有去看
-through
这个 flag —— 额外打印哪些 executions.-through all
表示无论对错都给打出来,像-through none
就表示只有正确的才打印 - 然后去调用
check_prop test fsc
, 去看这个 final states 到底符不符合,然后继续根据其它的参数一起决定show_exec
(bool) 与否。 - 然后,
dump_legend
(ine 244 - line 283),然后收集一下 n-pos, n-neg 这种信息而已。然后就没有了。 - 等等,这个
dump_legend
函数其实不仅仅 dump 了 legend ——图片标题,还有其它东西的 = = (这个渣命名..!!)在Pretty.dump_legend
里会去调用pp_dot_event_structure
, 所有的具体代码都在它的 worker function –do_pp_dot_event_structure
(line 743) 里头了,注释还说:This complex function is not meant to be used directly, 我还能说什么.. ╮(╯_╰)╭ 反正我就是围观一下,确认一下那些个具体的 data 是从哪里取出来的,每一个 data 分别表示什么而已。
- 它是这么用的:
在调用
kont
的时候,肯定那些个 concrete / rfmaps / 啥等等等等的东西都已经确定了,所以我直接在这里 inject 保存我想要的东西就可以了!在上边这些都执行完了之后,终于回到了 top_herd.ml 里的
run
函数 😂,里头剩下的部分都是输出到 terminal 的那些个 output 了,我就不太关心了。
上边是大致的流程,不过还有一个问题就是,在 pretty.ml 中具体是怎么抽取出关键的信息的呢,这里还是有点门道的:
- events
- 通过
dump_es_rfm_legend
函数可以学习 herd7 是怎么去读取 events 的。其实所有的操作都是通过调用pp_dot_event_structure
这个函数来做的。 - 可以看到它会首先去 filter out events / rfs. 因为在命令行参数中可能会说哪些个 events 是不需要的。哪些个是需要的。然后对于 relations 的 select 其实就是只留下俩 event 都是 valid 的那些个。而对我们来说,就不用挑了吧,都打出来!
- 其实默认设定
PrettyConf.showevents
的值是 NON-REG-EVENTS. - 通过 disable/enable
select_es
的效果可以看出,NON-REG-EVENTS 会禁用掉 write to r1 这类的 events. - 比如说
r1 := y
这样的东西,会变成 read y 和 write to r1 这样两个 events. 然后更新 r1 的 event 其实毫无意义了。毕竟真正 rf relation 去连的是 read y event 呀。所以这种 events 忽略掉完全没有问题! - 所以我也和 Pretty 一样地 filter events.
- 其实默认设定
- 对于 rf 的 filter 其实也是基于 events 的 filtering, 就是去挑出真正将要显示的那些 events 的 rf relation. 因此我也留着吧。
- 在这里一直有一个
vbss
的参数,我看了很久它到底是个什么东西..- 在 top_herd 里有一个
vbpp
的东西,可以看出这就是将来传到vbss
里的东西。可以通过ctrl+c, ctrl+t
看到这个的类型是个(string, relation) pair list
. 然后随便针对测试例子打印出来,就可以发现,这个是额外要打印的 relations!! 就像 ghb / fr / co 这些(虽然 co 在 concrete 里也有) - 至于 vb 到底表示啥.. 我不知道..
- 可能是 verbose, 毕竟只有 verbose level > 0 的时候才在 pretty.ml 中去打印的
- 也可能是 view-before, 在 pretty.ml 打印函数的最后一个部分 “A bunch of arrows” 那里有注释说到 viewed-before edges. 诶,viewed-before 是不是就是 fr 的别称??
- 在 top_herd 里有一个
do_pp_dot_event_structure
还有一个 mark 参数,我看了下,感觉像是 a set of events marked for debugging 啥的..
- 通过
- po relations
conc.po
就是!!直接打印就好了!!为什么 Pretty.ml 里头还要大费周章地重新算一遍..
- rf relations
- 其实直接遍历 rfmap 就可以了!
- co relations
- 也在 conc 里就有!另外 vbs 也有..
- fr relations
- vbs 里就有!
- other relations
就照着这么去改就改好了~ 😄
测试流程
因为每次 make all
之后构造出来的是 herd.native
这么个东西呀。所以我就 ln -s xxx/herd.native yyy/herd7
了一下,经过确认,重新编译之后,yyy/herd7 也的确是最新的,然后就好了。
不过这样是不够的,因为 stdlib.cat 这种东西找不到.. 在 src code 是有这些的,在 herd/libdir 文件夹下,然后在命令里加上 -I path/to/herd/libdir
就可以了
一点点体会
这是第一次看 functional language 的 real code. 感觉是:确实有些东西可以写的很简洁!但是让我非常困惑的是好些个变量的命名,「用一个字母命名」这个行为真的是太不友好了 >_<