打算做成一个系列,希望不要变成征讨檄文
做个标题党
不知从那里听来的……他认识一种值,名曰“X”,混沌所化,用if一算,就消释了……
X的作用似乎很多,可以表示未知、don't care、注入错误,但许多仿真器不带X玩也可以足够流行(说的就是你,verilator)。作为一个比较独特的语言特性,X必然要硅农花费时间熟悉。这篇文章希望带你10分钟知道和X有关的一切。
X 是什么
仿真
天地浑沌如鸡子,盘古生其中。万八千岁,天地开辟,阳清为天,阴浊为地。
知识告诉我们单纯的数字电路中只有0和1,实践告诉我们电路在刚上电(开机)的时候具有一些不确定性,这是电路物理特性决定的。
在编程语言中表达这种初始值的不确定性一般有这么几种路子
UB派
比如大家都喜欢的C
未经初始化的变量的值是未定义的
这句话中译中后是这样的
如果变量
v
没有被初始化,那么v
的值可能是0
也可能是1
这么做的好处首先是简单,我们甚至不需要定义什么,只需要说它是未定义的
unknown派
比如大家最后都在写的SQL
未经初始化的变量的值是特殊值UNKNOWN
这句话定义了除0, 1外的新值,UNKNOWN,并规定未初始化的变量将持有UNKNOWN
敏锐的童鞋可能会问:引入了新的值,那么运算要怎么办呢?
答案是传播UNKNOWN。简单来说,只要有一个操作数带了 UNKNOWN,那么结果就要体现出 UNKOWN
显然,verilog 选择了 UNKNOWN。
为什么
我斗胆猜测了一下原因
- verilog 最早是用来验证和仿真的语言,引入X可以方便验证(写断言/写性质)
- 选择UB会引入额外的不确定性
- X可以覆盖更多的情况,更加general
综合
“不知道!”他似乎很不高兴,脸上还有怒色了。
表达 do not care 的需求在手画电路图的工匠时代就已经产生了。某些综合工具会利用X的仿真语义,认为这代表了硅农在表示 do not care,进而做一些优化
begin
always_comb if (sel) out = A;
else out = 1'bx;
end
上面这个例子就可以被优化成
assign out = A;
验证
由于X在运算中会传播,因此X可以作为“污点”。当电路进入非法状态时,可以通过注入X来起警示作用。如果观察到了X,说明电路很可能出了问题
begin
always_comb if (sanity_check_fail(current_state)) begin
= 32'bx;
next_state end else begin
// ...
end
end
结论
- X表示不确定的值——“0或1”
- X不是仿真必须的——只是verilog选择了UNKOWN语义
- X需要有配套的运算语义
X怎么样
X的语义
因为X是一个抽象值,所以我们需要给X运算的语义
定义 \(\gamma(\texttt{X})=\{0,1\},\gamma(\texttt{0})=\{0\},\gamma(\texttt{1})=\{1\},\gamma(s)=\prod_{i}\gamma(s_i)\),我们称 \(\gamma\) 是一个具体函数,它将抽象的比特串映射回可能的01比特串。例如 \(\gamma(\texttt{01X0})=\{\texttt{0110\),\(0100}\}\)
我们希望对于任意含X的比特\(b_1,b_2\),应该有 \(\gamma(b_1\oplus b_2)=\{c_1\oplus c_2\mid c_1\in\gamma(b_1),c_2\in\gamma(b_2)\}\)
这给出了理论上运算符精确性的极限,我们当然希望越接近这个极限越好。
这里从语言标准里摘给几个运算让大家感受一下,可以验证哪些满足最优,哪些不满足
and
initial begin
= 3'bxxx;
A = 3'b01x;
B $displayb(A & B); // 0xx
end
可以发现标准还是做了一些bit操作的优化的,例如0&X=0,值的表扬
add
initial begin
= 3'b00x;
A = 3'b110;
B $displayb(A + B); // xxx
end
注意到无论A是000还是001,它和B作加法都不会影响高位,因此最好的结果应该是11X
手册中规定只要有一位是X,那么加法的结果都将是X
array
// store
initial begin
for (i = 0; i < 256; i = i + 1)
[i] = 0;
arr[32'bx] = 114514;
arrfor (i = 0; i < 256; i = i + 1)
$display(arr[i]); // 0
end
// load
initial begin
for (i = 0; i < 256; i = i + 1)
[i] = 114514;
arr$display(arr[32'x]); // x
end
手册规定:
- store的index带X,则忽略这次store
- load的index带X,则产生一个X
严格来说,arr[X]=val
应当导致后续所有对 arr
的读取都产生X
condition
always @* begin
if (sel) out = A;
else out = B; // (sel=X, A=0, B=1) -> 1
end
assign out = sel ? A : B; // (sel=X, A=0, B=1) -> X
这就是比较著名的 X-optimism,难以想象一个 fundamentally flawed implementation 竟然也能给一个这么积极的名字,大伙真是起名的大师
pessimism
assign out = sel ? A : B; // (sel=X,A=1,B=1) -> 1
assign out =(sel & A) | (~sel & B); // (sel=X,A=1,B=1) -> X
虽然二者功能等价,但在仿真时结果精度不一致,即使它们都完整地覆盖了所有可能的结果
结论
- X作为仿真抽象值,其上定义的运算并非最精确(某些情况下是具体语义的over approximation)
- X作为仿真抽象值,其上定义的运算可能遗漏了具体执行时可能的结果(并非全都是safe approximation)
想法
开始夹带私货:假如要设计一个新的HDL,或者H别的什么L(说的就是你,chisel),我们要如何对待X?
要不要 X
不要
我个人觉得 X 更像是 meta level 的东西。verilog之所以复杂,是因为它兼具仿真、综合、验证三种功能。
把 X作为值放进标准里,定死的运算语义使得仿真器不能做精确优化;验证和仿真都用同一套语言让具体值和抽象值只能搅在一起;引入的X-optimism还会隐藏某些bug,得不偿失
如果X作为了语言本身的一部分,那么任何针对X的分析都将变成 partial evaluation
怎么描述不确定性
UB人
把描述不确定性交给 meta level 的工具,例如linter/formal checker/analyzer。仿真只负责处理01值,又快又好
这也是目前verilator的思路。不过verilog仍然允许显式赋值X,因此verilator还需要打一个x-assign
的补丁
怎么解决X-optimism
这其实是个抽象解释理论能解决的问题