go cmd/compile: redundant conditional jump

ubof19bj  于 6个月前  发布在  Go
关注(0)|答案(7)|浏览(48)

你做了什么?
你好。我正在编写一些代码,并决定检查它生成的汇编指令。
https://godbolt.org/z/zFjGRF
你期望看到什么?
我期望看到没有冗余指令。
你看到了什么?
在第31行有一个冗余的条件跳转。
它是冗余的,因为从第24行到第31行的跳转发生在AX不等于0(即err不等于nil)的情况下。但是在第31行,条件跳转测试的是AX是否等于0,这是永远不会为真的。

i5desfxk

i5desfxk1#

是的,看起来很糟糕。
在SSA中查找,看起来我们没有为itabs使用一致的类型。有些是uintptr,有些是*uintptr。这使得itabs不会被CSE(Common Subexpression Elimination,公共子表达式消除),从而保持分支的统一性。
应该不难修复。留给1.13版本。

jtoj6r0c

jtoj6r0c2#

这将保持itabs不被CSE,从而保持分支不被统一。
嗯。我看到了一些不同的东西。uintptr / *uint8 不匹配是因为一个 itab 与从该 itab 加载的类型描述符不匹配。在SSA的末尾,我们有一个完全为空的块,其种类和控制完全与其前驱匹配。我认为我们根本没有任何机制来处理这种情况中的块。我在 trim 传递中添加了一个增强来处理这些情况中最明显的情况,它在 make.bash 期间触发了800多次。
如果有人想在我做之前再看一眼(不知道什么时候会这样),这是我为可修剪块替换的替代方案:

func trimmableBlock(b *Block) bool {
	if b == b.Func.Entry {
		return false
	}
	switch b.Kind {
	case BlockDefer, BlockRet, BlockRetJmp, BlockExit:
		return false
	case BlockPlain:
		s := b.Succs[0].b
		return s != b && (len(s.Preds) == 1 || emptyBlock(b))
	}
	if len(b.Preds) != 1 {
		return false
	}
	p := b.Preds[0].b
	if p.Kind != b.Kind || p.Control != b.Control {
		// TODO: detect inverted kind?
		return false
	}
	s0, s1 := b.Succs[0].b, b.Succs[1].b
	if s0 == b || s1 == b {
		return false
	}
	if p.Succs[0].b != b { // TODO: allow b in either position
		return false
	}
	return true
}

在这里需要做的事情包括:使代码更美观,修复生成调试信息时引发的恐慌,处理倒置的块种类(即 NE b1 b2 === EQ b2 b1),并处理重复块不是其前驱的第一个后继者的情况(这需要在 trim 中修复调用代码)。
cc @ysmolsky @mvdan

r9f1avp5

r9f1avp53#

代码:

package main

var e error

func main() {
	err := e
	if err != nil {
		panic(err)
	}
}

经过CSE后,我们得到以下SSA:

b1:-
v1 (?) = InitMem <mem>
v2 (?) = SP <uintptr>
v3 (?) = SB <uintptr>
v4 (?) = Addr <*error> {"".e} v3
v5 (+6) = Load <error> v4 v1 (err[error])
v17 (?) = OffPtr <*interface {}> [0] v2
v22 (?) = ConstNil <uintptr>
v19 (?) = ConstNil <*uint8>
v13 (+7) = ITab <uintptr> v5
v6 (?) = IMake <error> v22 v19
v12 (+7) = ITab <uintptr> v6
v7 (+7) = NeqPtr <bool> v13 v12
If v7 → b2 b3 (7)
b2: ← b1-
v9 (+8) = ITab <*uintptr> v5
v11 (8) = IsNonNil <bool> v9
If v11 → b4 b5 (8)
b3: ← b1
Ret v1
b4: ← b2-
v14 (8) = NilCheck <void> v9 v1
v15 (8) = OffPtr <**uint8> [8] v9
v16 (8) = Load <*uint8> v15 v1
Plain → b5 (8)
b5: ← b2 b4-
v18 (8) = Phi <*uint8> v9 v16
v20 (8) = IData <*uint8> v5
v21 (8) = IMake <interface {}> v18 v20
v23 (8) = Store <mem> {interface {}} v17 v21 v1
v24 (8) = StaticCall <mem> {runtime.gopanic} [16] v23
Exit v24 (8)

v9v13 应该被CSE,我认为。但它们没有被合并,一个是 uintptr ,另一个是 *uintptr
然后 IsNonNilNeqPtr 也需要被CSE。不确定这是否是一个单独的问题,还是在 v9v13 统一后自然解决。

3hvapo4f

3hvapo4f4#

Right. But after decompose builtin , both v9 and v13 have been eliminated in favor of v14 :

b1:-
v1 (?) = InitMem <mem>
v2 (?) = SP <uintptr>
v3 (?) = SB <uintptr>
v4 (?) = Addr <*error> {"".e} v3
v17 (?) = OffPtr <*interface {}> [0] v2
v22 (?) = ConstNil <uintptr>
v19 (?) = ConstNil <*uint8>
v14 (+11) = Load <uintptr> v4 v1 (err.itab[uintptr])
v25 (+11) = OffPtr <**uint8> [8] v4
v6 (?) = IMake <error> v22 v19
v7 (+13) = NeqPtr <bool> v14 v22
v10 (+11) = Load <*uint8> v25 v1 (err.data[*uint8])
v5 (+11) = IMake <error> v14 v10
If v7 → b2 b3 (13)
b2: ← b1-
v11 (+15) = IsNonNil <bool> v14
If v11 → b4 b5 (15)
b3: ← b1
Ret v1
b4: ← b2-
v15 (15) = OffPtr <**uint8> [8] v14
v16 (15) = Load <*uint8> v15 v1
Plain → b5 (15)
b5: ← b2 b4-
v18 (15) = Phi <*uint8> v14 v16
v21 (15) = IMake <interface {}> v18 v10
v8 (15) = OffPtr <**uint8> [8] v17
v26 (15) = Store <mem> {uintptr} v17 v18 v1
v23 (15) = Store <mem> {*uint8} v8 v10 v26
v24 (15) = StaticCall <mem> {runtime.gopanic} [16] v23
Exit v24 (15)
name err.itab[uintptr]: v14
name err.data[*uint8]: v10

Then lowered CSE manages to combine the IsNonNil and NeqPtr into v19:
(This dump is from two passes later, to eliminate the dead values)

b1:-
v1 (?) = InitMem <mem>
v2 (?) = SP <uintptr>
v3 (?) = SB <uintptr>
v14 (+11) = MOVQload <uintptr> {"".e} v3 v1 (err.itab[uintptr])
v10 (+11) = MOVQload <*uint8> {"".e} [8] v3 v1 (err.data[*uint8])
v19 (+13) = TESTQ <flags> v14 v14
NE v19 → b2 b3 (13)
b2: ← b1
NE v19 → b4 b5 (+15)
b3: ← b1
Ret v1
b4: ← b2-
v16 (15) = MOVQload <*uint8> [8] v14 v1
Plain → b5 (15)
b5: ← b2 b4-
v18 (15) = Phi <*uint8> v14 v16
v26 (15) = MOVQstore <mem> v2 v18 v1
v23 (15) = MOVQstore <mem> [8] v2 v10 v26
v24 (15) = CALLstatic <mem> {runtime.gopanic} [16] v23
Exit v24 (15)
name err.itab[uintptr]: v14
name err.data[*uint8]: v10

So all is well, except that we don't eliminate the duplicate conditional block.

qcbq4gxm

qcbq4gxm5#

我猜想如果比较是冗余的,那么证明过程会消除重复的代码块。这需要在降低阶段足够早地发生CSE(Common Subexpression Elimination)。你在降低之后修复它,而当前的CSE发生在这个时候,但到那时证明过程已经太晚了。所以你提议在修剪过程中添加一个简单的证明过程。当然,这会起作用,但我认为如果我们可以的话,最好能更早地捕获这个问题。

mdfafbf1

mdfafbf16#

有道理。似乎需要进行一些实验。顺便说一下,我可以告诉你,将特定的 *uint8 切换为 uintptr 并不困难——我做了这个操作并发现它本身并没有解决这个问题,这就是为什么我进一步深入研究的原因。:)

xwmevbvl

xwmevbvl7#

https://golang.org/cl/229984提到了这个问题:cmd/compile: fix type of ssa OpITab Values

相关问题