2016-02-19 12:14:42 +01:00
|
|
|
// Copyright 2016 The Go Authors. All rights reserved.
|
|
|
|
|
// Use of this source code is governed by a BSD-style
|
|
|
|
|
// license that can be found in the LICENSE file.
|
|
|
|
|
|
|
|
|
|
package ssa
|
|
|
|
|
|
2016-10-19 11:47:52 -04:00
|
|
|
import (
|
2019-10-01 11:10:22 -04:00
|
|
|
"cmd/internal/src"
|
2016-10-19 11:47:52 -04:00
|
|
|
"fmt"
|
|
|
|
|
"math"
|
2024-06-27 15:53:24 -07:00
|
|
|
"math/bits"
|
2016-10-19 11:47:52 -04:00
|
|
|
)
|
2016-03-23 10:20:44 -07:00
|
|
|
|
2016-03-07 18:36:16 +01:00
|
|
|
type branch int
|
2016-02-19 12:14:42 +01:00
|
|
|
|
|
|
|
|
const (
|
cmd/compile: make prove pass use unsatisfiability
Currently the prove pass uses implication queries. For each block, it
collects the set of branch conditions leading to that block, and
queries this fact table for whether any of these facts imply the
block's own branch condition (or its inverse). This works remarkably
well considering it doesn't do any deduction on these facts, but it
has various downsides:
1. It requires an implementation both of adding facts to the table and
determining implications. These are very nearly duals of each
other, but require separate implementations. Likewise, the process
of asserting facts of dominating branch conditions is very nearly
the dual of the process of querying implied branch conditions.
2. It leads to less effective use of derived facts. For example, the
prove pass currently derives facts about the relations between len
and cap, but can't make use of these unless a branch condition is
in the exact form of a derived fact. If one of these derived facts
contradicts another fact, it won't notice or make use of this.
This CL changes the approach of the prove pass to instead use
*contradiction* instead of implication. Rather than ever querying a
branch condition, it simply adds branch conditions to the fact table.
If this leads to a contradiction (specifically, it makes the fact set
unsatisfiable), that branch is impossible and can be cut. As a result,
1. We can eliminate the code for determining implications
(factsTable.get disappears entirely). Also, there is now a single
implementation of visiting and asserting branch conditions, since
we don't have to flip them around to treat them as facts in one
place and queries in another.
2. Derived facts can be used effectively. It doesn't matter *why* the
fact table is unsatisfiable; a contradiction in any of the facts is
enough.
3. As an added benefit, it's now quite easy to avoid traversing beyond
provably-unreachable blocks. In contrast, the current
implementation always visits all blocks.
The prove pass already has nearly all of the mechanism necessary to
compute unsatisfiability, which means this both simplifies the code
and makes it more powerful.
The only complication is that the current implication procedure has a
hack for dealing with the 0 <= Args[0] condition of OpIsInBounds and
OpIsSliceInBounds. We replace this with asserting the appropriate fact
when we process one of these conditions. This seems much cleaner
anyway, and works because we can now take advantage of derived facts.
This has no measurable effect on compiler performance.
Effectiveness:
There is exactly one condition in all of std and cmd that this fails
to prove that the old implementation could: (int64(^uint(0)>>1) < x)
in encoding/gob. This can never be true because x is an int, and it's
basically coincidence that the old code gets this. (For example, it
fails to prove the similar (x < ^int64(^uint(0)>>1)) condition that
immediately precedes it, and even though the conditions are logically
unrelated, it wouldn't get the second one if it hadn't first processed
the first!)
It does, however, prove a few dozen additional branches. These come
from facts that are added to the fact table about the relations
between len and cap. These were almost never queried directly before,
but could lead to contradictions, which the unsat-based approach is
able to use.
There are exactly two branches in std and cmd that this implementation
proves in the *other* direction. This sounds scary, but is okay
because both occur in already-unreachable blocks, so it doesn't matter
what we chose. Because the fact table logic is sound but incomplete,
it fails to prove that the block isn't reachable, even though it is
able to prove that both outgoing branches are impossible. We could
turn these blocks into BlockExit blocks, but it doesn't seem worth the
trouble of the extra proof effort for something that happens twice in
all of std and cmd.
Tests:
This CL updates test/prove.go to change the expected messages because
it can no longer give a "reason" why it proved or disproved a
condition. It also adds a new test of a branch it couldn't prove
before.
It mostly guts test/sliceopt.go, removing everything related to slice
bounds optimizations and moving a few relevant tests to test/prove.go.
Much of this test is actually unreachable. The new prove pass figures
this out and doesn't try to prove anything about the unreachable
parts. The output on the unreachable parts is already suspect because
anything can be proved at that point, so it's really just a regression
test for an algorithm the compiler no longer uses.
This is a step toward fixing #23354. That issue is quite easy to fix
once we can use derived facts effectively.
Change-Id: Ia48a1b9ee081310579fe474e4a61857424ff8ce8
Reviewed-on: https://go-review.googlesource.com/87478
Reviewed-by: Keith Randall <khr@golang.org>
2018-01-10 16:28:58 -05:00
|
|
|
unknown branch = iota
|
2016-03-07 18:36:16 +01:00
|
|
|
positive
|
|
|
|
|
negative
|
2022-03-06 12:07:54 -08:00
|
|
|
// The outedges from a jump table are jumpTable0,
|
|
|
|
|
// jumpTable0+1, jumpTable0+2, etc. There could be an
|
|
|
|
|
// arbitrary number so we can't list them all here.
|
|
|
|
|
jumpTable0
|
2016-03-07 18:36:16 +01:00
|
|
|
)
|
|
|
|
|
|
2024-06-14 22:29:09 -07:00
|
|
|
func (b branch) String() string {
|
|
|
|
|
switch b {
|
|
|
|
|
case unknown:
|
|
|
|
|
return "unk"
|
|
|
|
|
case positive:
|
|
|
|
|
return "pos"
|
|
|
|
|
case negative:
|
|
|
|
|
return "neg"
|
|
|
|
|
default:
|
|
|
|
|
return fmt.Sprintf("jmp%d", b-jumpTable0)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-03-07 18:36:16 +01:00
|
|
|
// relation represents the set of possible relations between
|
|
|
|
|
// pairs of variables (v, w). Without a priori knowledge the
|
|
|
|
|
// mask is lt | eq | gt meaning v can be less than, equal to or
|
|
|
|
|
// greater than w. When the execution path branches on the condition
|
|
|
|
|
// `v op w` the set of relations is updated to exclude any
|
|
|
|
|
// relation not possible due to `v op w` being true (or false).
|
|
|
|
|
//
|
|
|
|
|
// E.g.
|
|
|
|
|
//
|
2022-02-03 14:12:08 -05:00
|
|
|
// r := relation(...)
|
2016-03-07 18:36:16 +01:00
|
|
|
//
|
2022-02-03 14:12:08 -05:00
|
|
|
// if v < w {
|
|
|
|
|
// newR := r & lt
|
|
|
|
|
// }
|
|
|
|
|
// if v >= w {
|
|
|
|
|
// newR := r & (eq|gt)
|
|
|
|
|
// }
|
|
|
|
|
// if v != w {
|
|
|
|
|
// newR := r & (lt|gt)
|
|
|
|
|
// }
|
2016-03-07 18:36:16 +01:00
|
|
|
type relation uint
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
lt relation = 1 << iota
|
2016-02-19 12:14:42 +01:00
|
|
|
eq
|
|
|
|
|
gt
|
|
|
|
|
)
|
|
|
|
|
|
2018-01-09 14:45:30 -05:00
|
|
|
var relationStrings = [...]string{
|
|
|
|
|
0: "none", lt: "<", eq: "==", lt | eq: "<=",
|
|
|
|
|
gt: ">", gt | lt: "!=", gt | eq: ">=", gt | eq | lt: "any",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (r relation) String() string {
|
|
|
|
|
if r < relation(len(relationStrings)) {
|
|
|
|
|
return relationStrings[r]
|
|
|
|
|
}
|
|
|
|
|
return fmt.Sprintf("relation(%d)", uint(r))
|
|
|
|
|
}
|
|
|
|
|
|
2016-03-07 18:36:16 +01:00
|
|
|
// domain represents the domain of a variable pair in which a set
|
2018-07-24 23:59:48 +00:00
|
|
|
// of relations is known. For example, relations learned for unsigned
|
2016-04-03 12:43:27 +01:00
|
|
|
// pairs cannot be transferred to signed pairs because the same bit
|
2016-03-07 18:36:16 +01:00
|
|
|
// representation can mean something else.
|
|
|
|
|
type domain uint
|
2016-02-19 12:14:42 +01:00
|
|
|
|
|
|
|
|
const (
|
2016-03-07 18:36:16 +01:00
|
|
|
signed domain = 1 << iota
|
2016-02-19 12:14:42 +01:00
|
|
|
unsigned
|
|
|
|
|
pointer
|
2016-03-07 18:36:16 +01:00
|
|
|
boolean
|
2016-02-19 12:14:42 +01:00
|
|
|
)
|
|
|
|
|
|
2018-01-09 14:45:30 -05:00
|
|
|
var domainStrings = [...]string{
|
|
|
|
|
"signed", "unsigned", "pointer", "boolean",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (d domain) String() string {
|
|
|
|
|
s := ""
|
|
|
|
|
for i, ds := range domainStrings {
|
|
|
|
|
if d&(1<<uint(i)) != 0 {
|
|
|
|
|
if len(s) != 0 {
|
|
|
|
|
s += "|"
|
|
|
|
|
}
|
|
|
|
|
s += ds
|
|
|
|
|
d &^= 1 << uint(i)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if d != 0 {
|
|
|
|
|
if len(s) != 0 {
|
|
|
|
|
s += "|"
|
|
|
|
|
}
|
|
|
|
|
s += fmt.Sprintf("0x%x", uint(d))
|
|
|
|
|
}
|
|
|
|
|
return s
|
|
|
|
|
}
|
|
|
|
|
|
2016-03-23 10:20:44 -07:00
|
|
|
// a limit records known upper and lower bounds for a value.
|
2024-07-07 14:58:47 -07:00
|
|
|
//
|
|
|
|
|
// If we have min>max or umin>umax, then this limit is
|
|
|
|
|
// called "unsatisfiable". When we encounter such a limit, we
|
|
|
|
|
// know that any code for which that limit applies is unreachable.
|
|
|
|
|
// We don't particularly care how unsatisfiable limits propagate,
|
|
|
|
|
// including becoming satisfiable, because any optimization
|
|
|
|
|
// decisions based on those limits only apply to unreachable code.
|
2016-03-23 10:20:44 -07:00
|
|
|
type limit struct {
|
|
|
|
|
min, max int64 // min <= value <= max, signed
|
|
|
|
|
umin, umax uint64 // umin <= value <= umax, unsigned
|
2024-07-07 14:58:47 -07:00
|
|
|
// For booleans, we use 0==false, 1==true for both ranges
|
|
|
|
|
// For pointers, we use 0,0,0,0 for nil and minInt64,maxInt64,1,maxUint64 for nonnil
|
2016-03-23 10:20:44 -07:00
|
|
|
}
|
|
|
|
|
|
2016-10-19 11:47:52 -04:00
|
|
|
func (l limit) String() string {
|
|
|
|
|
return fmt.Sprintf("sm,SM,um,UM=%d,%d,%d,%d", l.min, l.max, l.umin, l.umax)
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-10 17:39:43 -05:00
|
|
|
func (l limit) intersect(l2 limit) limit {
|
|
|
|
|
if l.min < l2.min {
|
|
|
|
|
l.min = l2.min
|
|
|
|
|
}
|
|
|
|
|
if l.umin < l2.umin {
|
|
|
|
|
l.umin = l2.umin
|
|
|
|
|
}
|
|
|
|
|
if l.max > l2.max {
|
|
|
|
|
l.max = l2.max
|
|
|
|
|
}
|
|
|
|
|
if l.umax > l2.umax {
|
|
|
|
|
l.umax = l2.umax
|
|
|
|
|
}
|
|
|
|
|
return l
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-14 22:29:09 -07:00
|
|
|
func (l limit) signedMin(m int64) limit {
|
|
|
|
|
if l.min < m {
|
|
|
|
|
l.min = m
|
|
|
|
|
}
|
|
|
|
|
return l
|
|
|
|
|
}
|
|
|
|
|
func (l limit) signedMax(m int64) limit {
|
|
|
|
|
if l.max > m {
|
|
|
|
|
l.max = m
|
|
|
|
|
}
|
|
|
|
|
return l
|
|
|
|
|
}
|
|
|
|
|
func (l limit) signedMinMax(min, max int64) limit {
|
|
|
|
|
if l.min < min {
|
|
|
|
|
l.min = min
|
|
|
|
|
}
|
|
|
|
|
if l.max > max {
|
|
|
|
|
l.max = max
|
|
|
|
|
}
|
|
|
|
|
return l
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (l limit) unsignedMin(m uint64) limit {
|
|
|
|
|
if l.umin < m {
|
|
|
|
|
l.umin = m
|
|
|
|
|
}
|
|
|
|
|
return l
|
|
|
|
|
}
|
|
|
|
|
func (l limit) unsignedMax(m uint64) limit {
|
|
|
|
|
if l.umax > m {
|
|
|
|
|
l.umax = m
|
|
|
|
|
}
|
|
|
|
|
return l
|
|
|
|
|
}
|
|
|
|
|
func (l limit) unsignedMinMax(min, max uint64) limit {
|
|
|
|
|
if l.umin < min {
|
|
|
|
|
l.umin = min
|
|
|
|
|
}
|
|
|
|
|
if l.umax > max {
|
|
|
|
|
l.umax = max
|
|
|
|
|
}
|
|
|
|
|
return l
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (l limit) nonzero() bool {
|
|
|
|
|
return l.min > 0 || l.umin > 0 || l.max < 0
|
|
|
|
|
}
|
|
|
|
|
func (l limit) nonnegative() bool {
|
|
|
|
|
return l.min >= 0
|
|
|
|
|
}
|
|
|
|
|
func (l limit) unsat() bool {
|
|
|
|
|
return l.min > l.max || l.umin > l.umax
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If x and y can add without overflow or underflow
|
|
|
|
|
// (using b bits), safeAdd returns x+y, true.
|
|
|
|
|
// Otherwise, returns 0, false.
|
|
|
|
|
func safeAdd(x, y int64, b uint) (int64, bool) {
|
|
|
|
|
s := x + y
|
|
|
|
|
if x >= 0 && y >= 0 && s < 0 {
|
|
|
|
|
return 0, false // 64-bit overflow
|
|
|
|
|
}
|
|
|
|
|
if x < 0 && y < 0 && s >= 0 {
|
|
|
|
|
return 0, false // 64-bit underflow
|
|
|
|
|
}
|
|
|
|
|
if !fitsInBits(s, b) {
|
|
|
|
|
return 0, false
|
|
|
|
|
}
|
|
|
|
|
return s, true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// same as safeAdd for unsigned arithmetic.
|
|
|
|
|
func safeAddU(x, y uint64, b uint) (uint64, bool) {
|
|
|
|
|
s := x + y
|
|
|
|
|
if s < x || s < y {
|
|
|
|
|
return 0, false // 64-bit overflow
|
|
|
|
|
}
|
|
|
|
|
if !fitsInBitsU(s, b) {
|
|
|
|
|
return 0, false
|
|
|
|
|
}
|
|
|
|
|
return s, true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// same as safeAdd but for subtraction.
|
|
|
|
|
func safeSub(x, y int64, b uint) (int64, bool) {
|
|
|
|
|
if y == math.MinInt64 {
|
|
|
|
|
if x == math.MaxInt64 {
|
|
|
|
|
return 0, false // 64-bit overflow
|
|
|
|
|
}
|
|
|
|
|
x++
|
|
|
|
|
y++
|
|
|
|
|
}
|
|
|
|
|
return safeAdd(x, -y, b)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// same as safeAddU but for subtraction.
|
|
|
|
|
func safeSubU(x, y uint64, b uint) (uint64, bool) {
|
|
|
|
|
if x < y {
|
|
|
|
|
return 0, false // 64-bit underflow
|
|
|
|
|
}
|
|
|
|
|
s := x - y
|
|
|
|
|
if !fitsInBitsU(s, b) {
|
|
|
|
|
return 0, false
|
|
|
|
|
}
|
|
|
|
|
return s, true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// fitsInBits reports whether x fits in b bits (signed).
|
|
|
|
|
func fitsInBits(x int64, b uint) bool {
|
|
|
|
|
if b == 64 {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
m := int64(-1) << (b - 1)
|
|
|
|
|
M := -m - 1
|
|
|
|
|
return x >= m && x <= M
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// fitsInBitsU reports whether x fits in b bits (unsigned).
|
|
|
|
|
func fitsInBitsU(x uint64, b uint) bool {
|
|
|
|
|
return x>>b == 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// add returns the limit obtained by adding a value with limit l
|
|
|
|
|
// to a value with limit l2. The result must fit in b bits.
|
|
|
|
|
func (l limit) add(l2 limit, b uint) limit {
|
|
|
|
|
r := noLimit
|
|
|
|
|
min, minOk := safeAdd(l.min, l2.min, b)
|
|
|
|
|
max, maxOk := safeAdd(l.max, l2.max, b)
|
|
|
|
|
if minOk && maxOk {
|
|
|
|
|
r.min = min
|
|
|
|
|
r.max = max
|
|
|
|
|
}
|
|
|
|
|
umin, uminOk := safeAddU(l.umin, l2.umin, b)
|
|
|
|
|
umax, umaxOk := safeAddU(l.umax, l2.umax, b)
|
|
|
|
|
if uminOk && umaxOk {
|
|
|
|
|
r.umin = umin
|
|
|
|
|
r.umax = umax
|
|
|
|
|
}
|
|
|
|
|
return r
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// same as add but for subtraction.
|
|
|
|
|
func (l limit) sub(l2 limit, b uint) limit {
|
|
|
|
|
r := noLimit
|
|
|
|
|
min, minOk := safeSub(l.min, l2.max, b)
|
|
|
|
|
max, maxOk := safeSub(l.max, l2.min, b)
|
|
|
|
|
if minOk && maxOk {
|
|
|
|
|
r.min = min
|
|
|
|
|
r.max = max
|
|
|
|
|
}
|
|
|
|
|
umin, uminOk := safeSubU(l.umin, l2.umax, b)
|
|
|
|
|
umax, umaxOk := safeSubU(l.umax, l2.umin, b)
|
|
|
|
|
if uminOk && umaxOk {
|
|
|
|
|
r.umin = umin
|
|
|
|
|
r.umax = umax
|
|
|
|
|
}
|
|
|
|
|
return r
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-27 15:53:24 -07:00
|
|
|
// same as add but for multiplication.
|
|
|
|
|
func (l limit) mul(l2 limit, b uint) limit {
|
|
|
|
|
r := noLimit
|
|
|
|
|
umaxhi, umaxlo := bits.Mul64(l.umax, l2.umax)
|
|
|
|
|
if umaxhi == 0 && fitsInBitsU(umaxlo, b) {
|
|
|
|
|
r.umax = umaxlo
|
|
|
|
|
r.umin = l.umin * l2.umin
|
|
|
|
|
// Note: if the code containing this multiply is
|
|
|
|
|
// unreachable, then we may have umin>umax, and this
|
|
|
|
|
// multiply may overflow. But that's ok for
|
|
|
|
|
// unreachable code. If this code is reachable, we
|
|
|
|
|
// know umin<=umax, so this multiply will not overflow
|
|
|
|
|
// because the max multiply didn't.
|
|
|
|
|
}
|
|
|
|
|
// Signed is harder, so don't bother. The only useful
|
|
|
|
|
// case is when we know both multiplicands are nonnegative,
|
|
|
|
|
// but that case is handled above because we would have then
|
|
|
|
|
// previously propagated signed info to the unsigned domain,
|
|
|
|
|
// and will propagate it back after the multiply.
|
|
|
|
|
return r
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Similar to add, but compute 1 << l if it fits without overflow in b bits.
|
|
|
|
|
func (l limit) exp2(b uint) limit {
|
|
|
|
|
r := noLimit
|
|
|
|
|
if l.umax < uint64(b) {
|
|
|
|
|
r.umin = 1 << l.umin
|
|
|
|
|
r.umax = 1 << l.umax
|
|
|
|
|
// Same as above in mul, signed<->unsigned propagation
|
|
|
|
|
// will handle the signed case for us.
|
|
|
|
|
}
|
|
|
|
|
return r
|
|
|
|
|
}
|
|
|
|
|
|
2024-08-14 22:28:00 +02:00
|
|
|
// Similar to add, but computes the complement of the limit for bitsize b.
|
|
|
|
|
func (l limit) com(b uint) limit {
|
|
|
|
|
switch b {
|
|
|
|
|
case 64:
|
|
|
|
|
return limit{
|
|
|
|
|
min: ^l.max,
|
|
|
|
|
max: ^l.min,
|
|
|
|
|
umin: ^l.umax,
|
|
|
|
|
umax: ^l.umin,
|
|
|
|
|
}
|
|
|
|
|
case 32:
|
|
|
|
|
return limit{
|
|
|
|
|
min: int64(^int32(l.max)),
|
|
|
|
|
max: int64(^int32(l.min)),
|
|
|
|
|
umin: uint64(^uint32(l.umax)),
|
|
|
|
|
umax: uint64(^uint32(l.umin)),
|
|
|
|
|
}
|
|
|
|
|
case 16:
|
|
|
|
|
return limit{
|
|
|
|
|
min: int64(^int16(l.max)),
|
|
|
|
|
max: int64(^int16(l.min)),
|
|
|
|
|
umin: uint64(^uint16(l.umax)),
|
|
|
|
|
umax: uint64(^uint16(l.umin)),
|
|
|
|
|
}
|
|
|
|
|
case 8:
|
|
|
|
|
return limit{
|
|
|
|
|
min: int64(^int8(l.max)),
|
|
|
|
|
max: int64(^int8(l.min)),
|
|
|
|
|
umin: uint64(^uint8(l.umax)),
|
|
|
|
|
umax: uint64(^uint8(l.umin)),
|
|
|
|
|
}
|
|
|
|
|
default:
|
|
|
|
|
panic("unreachable")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-03-23 10:20:44 -07:00
|
|
|
var noLimit = limit{math.MinInt64, math.MaxInt64, 0, math.MaxUint64}
|
|
|
|
|
|
|
|
|
|
// a limitFact is a limit known for a particular value.
|
|
|
|
|
type limitFact struct {
|
|
|
|
|
vid ID
|
|
|
|
|
limit limit
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-14 22:29:09 -07:00
|
|
|
// An ordering encodes facts like v < w.
|
|
|
|
|
type ordering struct {
|
|
|
|
|
next *ordering // linked list of all known orderings for v.
|
|
|
|
|
// Note: v is implicit here, determined by which linked list it is in.
|
|
|
|
|
w *Value
|
2024-07-07 14:58:47 -07:00
|
|
|
d domain
|
2024-06-14 22:29:09 -07:00
|
|
|
r relation // one of ==,!=,<,<=,>,>=
|
2024-07-07 14:58:47 -07:00
|
|
|
// if d is boolean or pointer, r can only be ==, !=
|
2024-06-14 22:29:09 -07:00
|
|
|
}
|
|
|
|
|
|
2016-03-07 18:36:16 +01:00
|
|
|
// factsTable keeps track of relations between pairs of values.
|
2018-01-09 14:40:54 -05:00
|
|
|
//
|
|
|
|
|
// The fact table logic is sound, but incomplete. Outside of a few
|
|
|
|
|
// special cases, it performs no deduction or arithmetic. While there
|
|
|
|
|
// are known decision procedures for this, the ad hoc approach taken
|
|
|
|
|
// by the facts table is effective for real code while remaining very
|
|
|
|
|
// efficient.
|
2016-03-07 18:36:16 +01:00
|
|
|
type factsTable struct {
|
cmd/compile: make prove pass use unsatisfiability
Currently the prove pass uses implication queries. For each block, it
collects the set of branch conditions leading to that block, and
queries this fact table for whether any of these facts imply the
block's own branch condition (or its inverse). This works remarkably
well considering it doesn't do any deduction on these facts, but it
has various downsides:
1. It requires an implementation both of adding facts to the table and
determining implications. These are very nearly duals of each
other, but require separate implementations. Likewise, the process
of asserting facts of dominating branch conditions is very nearly
the dual of the process of querying implied branch conditions.
2. It leads to less effective use of derived facts. For example, the
prove pass currently derives facts about the relations between len
and cap, but can't make use of these unless a branch condition is
in the exact form of a derived fact. If one of these derived facts
contradicts another fact, it won't notice or make use of this.
This CL changes the approach of the prove pass to instead use
*contradiction* instead of implication. Rather than ever querying a
branch condition, it simply adds branch conditions to the fact table.
If this leads to a contradiction (specifically, it makes the fact set
unsatisfiable), that branch is impossible and can be cut. As a result,
1. We can eliminate the code for determining implications
(factsTable.get disappears entirely). Also, there is now a single
implementation of visiting and asserting branch conditions, since
we don't have to flip them around to treat them as facts in one
place and queries in another.
2. Derived facts can be used effectively. It doesn't matter *why* the
fact table is unsatisfiable; a contradiction in any of the facts is
enough.
3. As an added benefit, it's now quite easy to avoid traversing beyond
provably-unreachable blocks. In contrast, the current
implementation always visits all blocks.
The prove pass already has nearly all of the mechanism necessary to
compute unsatisfiability, which means this both simplifies the code
and makes it more powerful.
The only complication is that the current implication procedure has a
hack for dealing with the 0 <= Args[0] condition of OpIsInBounds and
OpIsSliceInBounds. We replace this with asserting the appropriate fact
when we process one of these conditions. This seems much cleaner
anyway, and works because we can now take advantage of derived facts.
This has no measurable effect on compiler performance.
Effectiveness:
There is exactly one condition in all of std and cmd that this fails
to prove that the old implementation could: (int64(^uint(0)>>1) < x)
in encoding/gob. This can never be true because x is an int, and it's
basically coincidence that the old code gets this. (For example, it
fails to prove the similar (x < ^int64(^uint(0)>>1)) condition that
immediately precedes it, and even though the conditions are logically
unrelated, it wouldn't get the second one if it hadn't first processed
the first!)
It does, however, prove a few dozen additional branches. These come
from facts that are added to the fact table about the relations
between len and cap. These were almost never queried directly before,
but could lead to contradictions, which the unsat-based approach is
able to use.
There are exactly two branches in std and cmd that this implementation
proves in the *other* direction. This sounds scary, but is okay
because both occur in already-unreachable blocks, so it doesn't matter
what we chose. Because the fact table logic is sound but incomplete,
it fails to prove that the block isn't reachable, even though it is
able to prove that both outgoing branches are impossible. We could
turn these blocks into BlockExit blocks, but it doesn't seem worth the
trouble of the extra proof effort for something that happens twice in
all of std and cmd.
Tests:
This CL updates test/prove.go to change the expected messages because
it can no longer give a "reason" why it proved or disproved a
condition. It also adds a new test of a branch it couldn't prove
before.
It mostly guts test/sliceopt.go, removing everything related to slice
bounds optimizations and moving a few relevant tests to test/prove.go.
Much of this test is actually unreachable. The new prove pass figures
this out and doesn't try to prove anything about the unreachable
parts. The output on the unreachable parts is already suspect because
anything can be proved at that point, so it's really just a regression
test for an algorithm the compiler no longer uses.
This is a step toward fixing #23354. That issue is quite easy to fix
once we can use derived facts effectively.
Change-Id: Ia48a1b9ee081310579fe474e4a61857424ff8ce8
Reviewed-on: https://go-review.googlesource.com/87478
Reviewed-by: Keith Randall <khr@golang.org>
2018-01-10 16:28:58 -05:00
|
|
|
// unsat is true if facts contains a contradiction.
|
|
|
|
|
//
|
|
|
|
|
// Note that the factsTable logic is incomplete, so if unsat
|
|
|
|
|
// is false, the assertions in factsTable could be satisfiable
|
|
|
|
|
// *or* unsatisfiable.
|
|
|
|
|
unsat bool // true if facts contains a contradiction
|
|
|
|
|
unsatDepth int // number of unsat checkpoints
|
|
|
|
|
|
2023-11-18 13:51:35 +08:00
|
|
|
// order* is a couple of partial order sets that record information
|
2018-04-15 23:53:08 +02:00
|
|
|
// about relations between SSA values in the signed and unsigned
|
|
|
|
|
// domain.
|
2019-09-20 00:12:15 +02:00
|
|
|
orderS *poset
|
|
|
|
|
orderU *poset
|
2018-04-15 23:53:08 +02:00
|
|
|
|
2024-06-14 22:29:09 -07:00
|
|
|
// orderings contains a list of known orderings between values.
|
|
|
|
|
// These lists are indexed by v.ID.
|
|
|
|
|
// We do not record transitive orderings. Only explicitly learned
|
|
|
|
|
// orderings are recorded. Transitive orderings can be obtained
|
|
|
|
|
// by walking along the individual orderings.
|
|
|
|
|
orderings map[ID]*ordering
|
|
|
|
|
// stack of IDs which have had an entry added in orderings.
|
|
|
|
|
// In addition, ID==0 are checkpoint markers.
|
|
|
|
|
orderingsStack []ID
|
|
|
|
|
orderingCache *ordering // unused ordering records
|
|
|
|
|
|
|
|
|
|
// known lower and upper constant bounds on individual values.
|
|
|
|
|
limits []limit // indexed by value ID
|
|
|
|
|
limitStack []limitFact // previous entries
|
|
|
|
|
recurseCheck []bool // recursion detector for limit propagation
|
2016-11-27 11:43:08 -08:00
|
|
|
|
|
|
|
|
// For each slice s, a map from s to a len(s)/cap(s) value (if any)
|
|
|
|
|
// TODO: check if there are cases that matter where we have
|
|
|
|
|
// more than one len(s) for a slice. We could keep a list if necessary.
|
|
|
|
|
lens map[ID]*Value
|
|
|
|
|
caps map[ID]*Value
|
2016-02-19 12:14:42 +01:00
|
|
|
}
|
|
|
|
|
|
2024-08-07 14:25:31 -07:00
|
|
|
// checkpointBound is an invalid value used for checkpointing
|
2016-03-07 18:36:16 +01:00
|
|
|
// and restoring factsTable.
|
2016-03-23 10:20:44 -07:00
|
|
|
var checkpointBound = limitFact{}
|
2016-03-07 18:36:16 +01:00
|
|
|
|
2018-05-01 00:57:57 +02:00
|
|
|
func newFactsTable(f *Func) *factsTable {
|
2016-03-07 18:36:16 +01:00
|
|
|
ft := &factsTable{}
|
2019-09-20 00:12:15 +02:00
|
|
|
ft.orderS = f.newPoset()
|
|
|
|
|
ft.orderU = f.newPoset()
|
|
|
|
|
ft.orderS.SetUnsigned(false)
|
|
|
|
|
ft.orderU.SetUnsigned(true)
|
2024-06-14 22:29:09 -07:00
|
|
|
ft.orderings = make(map[ID]*ordering)
|
2024-05-18 08:01:27 -07:00
|
|
|
ft.limits = f.Cache.allocLimitSlice(f.NumValues())
|
2024-06-14 22:29:09 -07:00
|
|
|
for _, b := range f.Blocks {
|
|
|
|
|
for _, v := range b.Values {
|
|
|
|
|
ft.limits[v.ID] = initLimit(v)
|
|
|
|
|
}
|
2024-05-18 08:01:27 -07:00
|
|
|
}
|
2024-06-14 22:29:09 -07:00
|
|
|
ft.limitStack = make([]limitFact, 4)
|
|
|
|
|
ft.recurseCheck = f.Cache.allocBoolSlice(f.NumValues())
|
2016-03-07 18:36:16 +01:00
|
|
|
return ft
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-03 12:10:26 +08:00
|
|
|
// initLimitForNewValue initializes the limits for newly created values,
|
|
|
|
|
// possibly needing to expand the limits slice. Currently used by
|
|
|
|
|
// simplifyBlock when certain provably constant results are folded.
|
|
|
|
|
func (ft *factsTable) initLimitForNewValue(v *Value) {
|
|
|
|
|
if int(v.ID) >= len(ft.limits) {
|
|
|
|
|
f := v.Block.Func
|
|
|
|
|
n := f.NumValues()
|
|
|
|
|
if cap(ft.limits) >= n {
|
|
|
|
|
ft.limits = ft.limits[:n]
|
|
|
|
|
} else {
|
|
|
|
|
old := ft.limits
|
|
|
|
|
ft.limits = f.Cache.allocLimitSlice(n)
|
|
|
|
|
copy(ft.limits, old)
|
|
|
|
|
f.Cache.freeLimitSlice(old)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
ft.limits[v.ID] = initLimit(v)
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-14 22:29:09 -07:00
|
|
|
// signedMin records the fact that we know v is at least
|
|
|
|
|
// min in the signed domain.
|
|
|
|
|
func (ft *factsTable) signedMin(v *Value, min int64) bool {
|
|
|
|
|
return ft.newLimit(v, limit{min: min, max: math.MaxInt64, umin: 0, umax: math.MaxUint64})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// signedMax records the fact that we know v is at most
|
|
|
|
|
// max in the signed domain.
|
|
|
|
|
func (ft *factsTable) signedMax(v *Value, max int64) bool {
|
|
|
|
|
return ft.newLimit(v, limit{min: math.MinInt64, max: max, umin: 0, umax: math.MaxUint64})
|
|
|
|
|
}
|
|
|
|
|
func (ft *factsTable) signedMinMax(v *Value, min, max int64) bool {
|
|
|
|
|
return ft.newLimit(v, limit{min: min, max: max, umin: 0, umax: math.MaxUint64})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// setNonNegative records the fact that v is known to be non-negative.
|
|
|
|
|
func (ft *factsTable) setNonNegative(v *Value) bool {
|
|
|
|
|
return ft.signedMin(v, 0)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// unsignedMin records the fact that we know v is at least
|
|
|
|
|
// min in the unsigned domain.
|
|
|
|
|
func (ft *factsTable) unsignedMin(v *Value, min uint64) bool {
|
|
|
|
|
return ft.newLimit(v, limit{min: math.MinInt64, max: math.MaxInt64, umin: min, umax: math.MaxUint64})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// unsignedMax records the fact that we know v is at most
|
|
|
|
|
// max in the unsigned domain.
|
|
|
|
|
func (ft *factsTable) unsignedMax(v *Value, max uint64) bool {
|
|
|
|
|
return ft.newLimit(v, limit{min: math.MinInt64, max: math.MaxInt64, umin: 0, umax: max})
|
|
|
|
|
}
|
|
|
|
|
func (ft *factsTable) unsignedMinMax(v *Value, min, max uint64) bool {
|
|
|
|
|
return ft.newLimit(v, limit{min: math.MinInt64, max: math.MaxInt64, umin: min, umax: max})
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-07 14:58:47 -07:00
|
|
|
func (ft *factsTable) booleanFalse(v *Value) bool {
|
|
|
|
|
return ft.newLimit(v, limit{min: 0, max: 0, umin: 0, umax: 0})
|
|
|
|
|
}
|
|
|
|
|
func (ft *factsTable) booleanTrue(v *Value) bool {
|
|
|
|
|
return ft.newLimit(v, limit{min: 1, max: 1, umin: 1, umax: 1})
|
|
|
|
|
}
|
|
|
|
|
func (ft *factsTable) pointerNil(v *Value) bool {
|
|
|
|
|
return ft.newLimit(v, limit{min: 0, max: 0, umin: 0, umax: 0})
|
|
|
|
|
}
|
|
|
|
|
func (ft *factsTable) pointerNonNil(v *Value) bool {
|
|
|
|
|
l := noLimit
|
|
|
|
|
l.umin = 1
|
|
|
|
|
return ft.newLimit(v, l)
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-14 22:29:09 -07:00
|
|
|
// newLimit adds new limiting information for v.
|
|
|
|
|
// Returns true if the new limit added any new information.
|
|
|
|
|
func (ft *factsTable) newLimit(v *Value, newLim limit) bool {
|
|
|
|
|
oldLim := ft.limits[v.ID]
|
|
|
|
|
|
|
|
|
|
// Merge old and new information.
|
|
|
|
|
lim := oldLim.intersect(newLim)
|
|
|
|
|
|
|
|
|
|
// signed <-> unsigned propagation
|
|
|
|
|
if lim.min >= 0 {
|
|
|
|
|
lim = lim.unsignedMinMax(uint64(lim.min), uint64(lim.max))
|
|
|
|
|
}
|
|
|
|
|
if fitsInBitsU(lim.umax, uint(8*v.Type.Size()-1)) {
|
|
|
|
|
lim = lim.signedMinMax(int64(lim.umin), int64(lim.umax))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if lim == oldLim {
|
|
|
|
|
return false // nothing new to record
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if lim.unsat() {
|
2025-02-20 11:50:53 +01:00
|
|
|
r := !ft.unsat
|
2024-06-14 22:29:09 -07:00
|
|
|
ft.unsat = true
|
2025-02-20 11:50:53 +01:00
|
|
|
return r
|
2024-06-14 22:29:09 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check for recursion. This normally happens because in unsatisfiable
|
|
|
|
|
// cases we have a < b < a, and every update to a's limits returns
|
|
|
|
|
// here again with the limit increased by 2.
|
|
|
|
|
// Normally this is caught early by the orderS/orderU posets, but in
|
|
|
|
|
// cases where the comparisons jump between signed and unsigned domains,
|
|
|
|
|
// the posets will not notice.
|
|
|
|
|
if ft.recurseCheck[v.ID] {
|
|
|
|
|
// This should only happen for unsatisfiable cases. TODO: check
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
ft.recurseCheck[v.ID] = true
|
|
|
|
|
defer func() {
|
|
|
|
|
ft.recurseCheck[v.ID] = false
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
// Record undo information.
|
|
|
|
|
ft.limitStack = append(ft.limitStack, limitFact{v.ID, oldLim})
|
|
|
|
|
// Record new information.
|
|
|
|
|
ft.limits[v.ID] = lim
|
|
|
|
|
if v.Block.Func.pass.debug > 2 {
|
|
|
|
|
// TODO: pos is probably wrong. This is the position where v is defined,
|
|
|
|
|
// not the position where we learned the fact about it (which was
|
|
|
|
|
// probably some subsequent compare+branch).
|
|
|
|
|
v.Block.Func.Warnl(v.Pos, "new limit %s %s unsat=%v", v, lim.String(), ft.unsat)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Propagate this new constant range to other values
|
|
|
|
|
// that we know are ordered with respect to this one.
|
|
|
|
|
// Note overflow/underflow in the arithmetic below is ok,
|
|
|
|
|
// it will just lead to imprecision (undetected unsatisfiability).
|
|
|
|
|
for o := ft.orderings[v.ID]; o != nil; o = o.next {
|
|
|
|
|
switch o.d {
|
|
|
|
|
case signed:
|
|
|
|
|
switch o.r {
|
|
|
|
|
case eq: // v == w
|
|
|
|
|
ft.signedMinMax(o.w, lim.min, lim.max)
|
|
|
|
|
case lt | eq: // v <= w
|
|
|
|
|
ft.signedMin(o.w, lim.min)
|
|
|
|
|
case lt: // v < w
|
|
|
|
|
ft.signedMin(o.w, lim.min+1)
|
|
|
|
|
case gt | eq: // v >= w
|
|
|
|
|
ft.signedMax(o.w, lim.max)
|
|
|
|
|
case gt: // v > w
|
|
|
|
|
ft.signedMax(o.w, lim.max-1)
|
|
|
|
|
case lt | gt: // v != w
|
|
|
|
|
if lim.min == lim.max { // v is a constant
|
|
|
|
|
c := lim.min
|
|
|
|
|
if ft.limits[o.w.ID].min == c {
|
|
|
|
|
ft.signedMin(o.w, c+1)
|
|
|
|
|
}
|
|
|
|
|
if ft.limits[o.w.ID].max == c {
|
|
|
|
|
ft.signedMax(o.w, c-1)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
case unsigned:
|
|
|
|
|
switch o.r {
|
|
|
|
|
case eq: // v == w
|
|
|
|
|
ft.unsignedMinMax(o.w, lim.umin, lim.umax)
|
|
|
|
|
case lt | eq: // v <= w
|
|
|
|
|
ft.unsignedMin(o.w, lim.umin)
|
|
|
|
|
case lt: // v < w
|
|
|
|
|
ft.unsignedMin(o.w, lim.umin+1)
|
|
|
|
|
case gt | eq: // v >= w
|
|
|
|
|
ft.unsignedMax(o.w, lim.umax)
|
|
|
|
|
case gt: // v > w
|
|
|
|
|
ft.unsignedMax(o.w, lim.umax-1)
|
|
|
|
|
case lt | gt: // v != w
|
|
|
|
|
if lim.umin == lim.umax { // v is a constant
|
|
|
|
|
c := lim.umin
|
|
|
|
|
if ft.limits[o.w.ID].umin == c {
|
|
|
|
|
ft.unsignedMin(o.w, c+1)
|
|
|
|
|
}
|
|
|
|
|
if ft.limits[o.w.ID].umax == c {
|
|
|
|
|
ft.unsignedMax(o.w, c-1)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-07-07 14:58:47 -07:00
|
|
|
case boolean:
|
|
|
|
|
switch o.r {
|
|
|
|
|
case eq:
|
|
|
|
|
if lim.min == 0 && lim.max == 0 { // constant false
|
|
|
|
|
ft.booleanFalse(o.w)
|
|
|
|
|
}
|
|
|
|
|
if lim.min == 1 && lim.max == 1 { // constant true
|
|
|
|
|
ft.booleanTrue(o.w)
|
|
|
|
|
}
|
|
|
|
|
case lt | gt:
|
|
|
|
|
if lim.min == 0 && lim.max == 0 { // constant false
|
|
|
|
|
ft.booleanTrue(o.w)
|
|
|
|
|
}
|
|
|
|
|
if lim.min == 1 && lim.max == 1 { // constant true
|
|
|
|
|
ft.booleanFalse(o.w)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
case pointer:
|
|
|
|
|
switch o.r {
|
|
|
|
|
case eq:
|
|
|
|
|
if lim.umax == 0 { // nil
|
|
|
|
|
ft.pointerNil(o.w)
|
|
|
|
|
}
|
|
|
|
|
if lim.umin > 0 { // non-nil
|
|
|
|
|
ft.pointerNonNil(o.w)
|
|
|
|
|
}
|
|
|
|
|
case lt | gt:
|
|
|
|
|
if lim.umax == 0 { // nil
|
|
|
|
|
ft.pointerNonNil(o.w)
|
|
|
|
|
}
|
|
|
|
|
// note: not equal to non-nil doesn't tell us anything.
|
|
|
|
|
}
|
2024-06-14 22:29:09 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-11 12:10:10 -07:00
|
|
|
// If this is new known constant for a boolean value,
|
|
|
|
|
// extract relation between its args. For example, if
|
|
|
|
|
// We learn v is false, and v is defined as a<b, then we learn a>=b.
|
|
|
|
|
if v.Type.IsBoolean() {
|
2024-08-19 00:07:37 +03:00
|
|
|
// If we reach here, it is because we have a more restrictive
|
2024-07-11 12:10:10 -07:00
|
|
|
// value for v than the default. The only two such values
|
|
|
|
|
// are constant true or constant false.
|
|
|
|
|
if lim.min != lim.max {
|
|
|
|
|
v.Block.Func.Fatalf("boolean not constant %v", v)
|
|
|
|
|
}
|
|
|
|
|
isTrue := lim.min == 1
|
|
|
|
|
if dr, ok := domainRelationTable[v.Op]; ok && v.Op != OpIsInBounds && v.Op != OpIsSliceInBounds {
|
|
|
|
|
d := dr.d
|
|
|
|
|
r := dr.r
|
|
|
|
|
if d == signed && ft.isNonNegative(v.Args[0]) && ft.isNonNegative(v.Args[1]) {
|
|
|
|
|
d |= unsigned
|
|
|
|
|
}
|
|
|
|
|
if !isTrue {
|
|
|
|
|
r ^= (lt | gt | eq)
|
|
|
|
|
}
|
|
|
|
|
// TODO: v.Block is wrong?
|
|
|
|
|
addRestrictions(v.Block, ft, d, v.Args[0], v.Args[1], r)
|
|
|
|
|
}
|
|
|
|
|
switch v.Op {
|
|
|
|
|
case OpIsNonNil:
|
|
|
|
|
if isTrue {
|
|
|
|
|
ft.pointerNonNil(v.Args[0])
|
|
|
|
|
} else {
|
|
|
|
|
ft.pointerNil(v.Args[0])
|
|
|
|
|
}
|
|
|
|
|
case OpIsInBounds, OpIsSliceInBounds:
|
|
|
|
|
// 0 <= a0 < a1 (or 0 <= a0 <= a1)
|
|
|
|
|
r := lt
|
|
|
|
|
if v.Op == OpIsSliceInBounds {
|
|
|
|
|
r |= eq
|
|
|
|
|
}
|
|
|
|
|
if isTrue {
|
|
|
|
|
// On the positive branch, we learn:
|
|
|
|
|
// signed: 0 <= a0 < a1 (or 0 <= a0 <= a1)
|
|
|
|
|
// unsigned: a0 < a1 (or a0 <= a1)
|
|
|
|
|
ft.setNonNegative(v.Args[0])
|
|
|
|
|
ft.update(v.Block, v.Args[0], v.Args[1], signed, r)
|
|
|
|
|
ft.update(v.Block, v.Args[0], v.Args[1], unsigned, r)
|
|
|
|
|
} else {
|
|
|
|
|
// On the negative branch, we learn (0 > a0 ||
|
|
|
|
|
// a0 >= a1). In the unsigned domain, this is
|
|
|
|
|
// simply a0 >= a1 (which is the reverse of the
|
|
|
|
|
// positive branch, so nothing surprising).
|
|
|
|
|
// But in the signed domain, we can't express the ||
|
|
|
|
|
// condition, so check if a0 is non-negative instead,
|
|
|
|
|
// to be able to learn something.
|
|
|
|
|
r ^= (lt | gt | eq) // >= (index) or > (slice)
|
|
|
|
|
if ft.isNonNegative(v.Args[0]) {
|
|
|
|
|
ft.update(v.Block, v.Args[0], v.Args[1], signed, r)
|
|
|
|
|
}
|
|
|
|
|
ft.update(v.Block, v.Args[0], v.Args[1], unsigned, r)
|
|
|
|
|
// TODO: v.Block is wrong here
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-14 22:29:09 -07:00
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (ft *factsTable) addOrdering(v, w *Value, d domain, r relation) {
|
|
|
|
|
o := ft.orderingCache
|
|
|
|
|
if o == nil {
|
|
|
|
|
o = &ordering{}
|
|
|
|
|
} else {
|
|
|
|
|
ft.orderingCache = o.next
|
|
|
|
|
}
|
|
|
|
|
o.w = w
|
|
|
|
|
o.d = d
|
|
|
|
|
o.r = r
|
|
|
|
|
o.next = ft.orderings[v.ID]
|
|
|
|
|
ft.orderings[v.ID] = o
|
|
|
|
|
ft.orderingsStack = append(ft.orderingsStack, v.ID)
|
|
|
|
|
}
|
|
|
|
|
|
2016-03-07 18:36:16 +01:00
|
|
|
// update updates the set of relations between v and w in domain d
|
|
|
|
|
// restricting it to r.
|
2016-10-19 11:47:52 -04:00
|
|
|
func (ft *factsTable) update(parent *Block, v, w *Value, d domain, r relation) {
|
2019-01-02 12:27:55 -05:00
|
|
|
if parent.Func.pass.debug > 2 {
|
|
|
|
|
parent.Func.Warnl(parent.Pos, "parent=%s, update %s %s %s", parent, v, w, r)
|
|
|
|
|
}
|
2018-03-13 00:21:57 +01:00
|
|
|
// No need to do anything else if we already found unsat.
|
|
|
|
|
if ft.unsat {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2018-03-13 00:25:06 +01:00
|
|
|
// Self-fact. It's wasteful to register it into the facts
|
|
|
|
|
// table, so just note whether it's satisfiable
|
|
|
|
|
if v == w {
|
|
|
|
|
if r&eq == 0 {
|
|
|
|
|
ft.unsat = true
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2018-04-15 23:53:08 +02:00
|
|
|
if d == signed || d == unsigned {
|
|
|
|
|
var ok bool
|
2019-09-20 00:12:15 +02:00
|
|
|
order := ft.orderS
|
2018-04-15 23:53:08 +02:00
|
|
|
if d == unsigned {
|
2019-09-20 00:12:15 +02:00
|
|
|
order = ft.orderU
|
2018-04-15 23:53:08 +02:00
|
|
|
}
|
|
|
|
|
switch r {
|
|
|
|
|
case lt:
|
2019-09-20 00:12:15 +02:00
|
|
|
ok = order.SetOrder(v, w)
|
2018-04-15 23:53:08 +02:00
|
|
|
case gt:
|
2019-09-20 00:12:15 +02:00
|
|
|
ok = order.SetOrder(w, v)
|
2018-04-15 23:53:08 +02:00
|
|
|
case lt | eq:
|
2019-09-20 00:12:15 +02:00
|
|
|
ok = order.SetOrderOrEqual(v, w)
|
2018-04-15 23:53:08 +02:00
|
|
|
case gt | eq:
|
2019-09-20 00:12:15 +02:00
|
|
|
ok = order.SetOrderOrEqual(w, v)
|
2018-04-15 23:53:08 +02:00
|
|
|
case eq:
|
2019-09-20 00:12:15 +02:00
|
|
|
ok = order.SetEqual(v, w)
|
2018-04-15 23:53:08 +02:00
|
|
|
case lt | gt:
|
2019-09-20 00:12:15 +02:00
|
|
|
ok = order.SetNonEqual(v, w)
|
2018-04-15 23:53:08 +02:00
|
|
|
default:
|
|
|
|
|
panic("unknown relation")
|
|
|
|
|
}
|
2024-06-14 22:29:09 -07:00
|
|
|
ft.addOrdering(v, w, d, r)
|
|
|
|
|
ft.addOrdering(w, v, d, reverseBits[r])
|
|
|
|
|
|
2018-04-15 23:53:08 +02:00
|
|
|
if !ok {
|
2019-01-02 12:27:55 -05:00
|
|
|
if parent.Func.pass.debug > 2 {
|
|
|
|
|
parent.Func.Warnl(parent.Pos, "unsat %s %s %s", v, w, r)
|
|
|
|
|
}
|
2018-04-15 23:53:08 +02:00
|
|
|
ft.unsat = true
|
|
|
|
|
return
|
|
|
|
|
}
|
2024-07-07 14:58:47 -07:00
|
|
|
}
|
|
|
|
|
if d == boolean || d == pointer {
|
|
|
|
|
for o := ft.orderings[v.ID]; o != nil; o = o.next {
|
|
|
|
|
if o.d == d && o.w == w {
|
|
|
|
|
// We already know a relationship between v and w.
|
|
|
|
|
// Either it is a duplicate, or it is a contradiction,
|
|
|
|
|
// as we only allow eq and lt|gt for these domains,
|
|
|
|
|
if o.r != r {
|
|
|
|
|
ft.unsat = true
|
|
|
|
|
}
|
|
|
|
|
return
|
2019-01-02 12:27:55 -05:00
|
|
|
}
|
cmd/compile: make prove pass use unsatisfiability
Currently the prove pass uses implication queries. For each block, it
collects the set of branch conditions leading to that block, and
queries this fact table for whether any of these facts imply the
block's own branch condition (or its inverse). This works remarkably
well considering it doesn't do any deduction on these facts, but it
has various downsides:
1. It requires an implementation both of adding facts to the table and
determining implications. These are very nearly duals of each
other, but require separate implementations. Likewise, the process
of asserting facts of dominating branch conditions is very nearly
the dual of the process of querying implied branch conditions.
2. It leads to less effective use of derived facts. For example, the
prove pass currently derives facts about the relations between len
and cap, but can't make use of these unless a branch condition is
in the exact form of a derived fact. If one of these derived facts
contradicts another fact, it won't notice or make use of this.
This CL changes the approach of the prove pass to instead use
*contradiction* instead of implication. Rather than ever querying a
branch condition, it simply adds branch conditions to the fact table.
If this leads to a contradiction (specifically, it makes the fact set
unsatisfiable), that branch is impossible and can be cut. As a result,
1. We can eliminate the code for determining implications
(factsTable.get disappears entirely). Also, there is now a single
implementation of visiting and asserting branch conditions, since
we don't have to flip them around to treat them as facts in one
place and queries in another.
2. Derived facts can be used effectively. It doesn't matter *why* the
fact table is unsatisfiable; a contradiction in any of the facts is
enough.
3. As an added benefit, it's now quite easy to avoid traversing beyond
provably-unreachable blocks. In contrast, the current
implementation always visits all blocks.
The prove pass already has nearly all of the mechanism necessary to
compute unsatisfiability, which means this both simplifies the code
and makes it more powerful.
The only complication is that the current implication procedure has a
hack for dealing with the 0 <= Args[0] condition of OpIsInBounds and
OpIsSliceInBounds. We replace this with asserting the appropriate fact
when we process one of these conditions. This seems much cleaner
anyway, and works because we can now take advantage of derived facts.
This has no measurable effect on compiler performance.
Effectiveness:
There is exactly one condition in all of std and cmd that this fails
to prove that the old implementation could: (int64(^uint(0)>>1) < x)
in encoding/gob. This can never be true because x is an int, and it's
basically coincidence that the old code gets this. (For example, it
fails to prove the similar (x < ^int64(^uint(0)>>1)) condition that
immediately precedes it, and even though the conditions are logically
unrelated, it wouldn't get the second one if it hadn't first processed
the first!)
It does, however, prove a few dozen additional branches. These come
from facts that are added to the fact table about the relations
between len and cap. These were almost never queried directly before,
but could lead to contradictions, which the unsat-based approach is
able to use.
There are exactly two branches in std and cmd that this implementation
proves in the *other* direction. This sounds scary, but is okay
because both occur in already-unreachable blocks, so it doesn't matter
what we chose. Because the fact table logic is sound but incomplete,
it fails to prove that the block isn't reachable, even though it is
able to prove that both outgoing branches are impossible. We could
turn these blocks into BlockExit blocks, but it doesn't seem worth the
trouble of the extra proof effort for something that happens twice in
all of std and cmd.
Tests:
This CL updates test/prove.go to change the expected messages because
it can no longer give a "reason" why it proved or disproved a
condition. It also adds a new test of a branch it couldn't prove
before.
It mostly guts test/sliceopt.go, removing everything related to slice
bounds optimizations and moving a few relevant tests to test/prove.go.
Much of this test is actually unreachable. The new prove pass figures
this out and doesn't try to prove anything about the unreachable
parts. The output on the unreachable parts is already suspect because
anything can be proved at that point, so it's really just a regression
test for an algorithm the compiler no longer uses.
This is a step toward fixing #23354. That issue is quite easy to fix
once we can use derived facts effectively.
Change-Id: Ia48a1b9ee081310579fe474e4a61857424ff8ce8
Reviewed-on: https://go-review.googlesource.com/87478
Reviewed-by: Keith Randall <khr@golang.org>
2018-01-10 16:28:58 -05:00
|
|
|
}
|
2024-07-07 14:58:47 -07:00
|
|
|
// TODO: this does not do transitive equality.
|
|
|
|
|
// We could use a poset like above, but somewhat degenerate (==,!= only).
|
|
|
|
|
ft.addOrdering(v, w, d, r)
|
|
|
|
|
ft.addOrdering(w, v, d, r) // note: reverseBits unnecessary for eq and lt|gt.
|
cmd/compile: make prove pass use unsatisfiability
Currently the prove pass uses implication queries. For each block, it
collects the set of branch conditions leading to that block, and
queries this fact table for whether any of these facts imply the
block's own branch condition (or its inverse). This works remarkably
well considering it doesn't do any deduction on these facts, but it
has various downsides:
1. It requires an implementation both of adding facts to the table and
determining implications. These are very nearly duals of each
other, but require separate implementations. Likewise, the process
of asserting facts of dominating branch conditions is very nearly
the dual of the process of querying implied branch conditions.
2. It leads to less effective use of derived facts. For example, the
prove pass currently derives facts about the relations between len
and cap, but can't make use of these unless a branch condition is
in the exact form of a derived fact. If one of these derived facts
contradicts another fact, it won't notice or make use of this.
This CL changes the approach of the prove pass to instead use
*contradiction* instead of implication. Rather than ever querying a
branch condition, it simply adds branch conditions to the fact table.
If this leads to a contradiction (specifically, it makes the fact set
unsatisfiable), that branch is impossible and can be cut. As a result,
1. We can eliminate the code for determining implications
(factsTable.get disappears entirely). Also, there is now a single
implementation of visiting and asserting branch conditions, since
we don't have to flip them around to treat them as facts in one
place and queries in another.
2. Derived facts can be used effectively. It doesn't matter *why* the
fact table is unsatisfiable; a contradiction in any of the facts is
enough.
3. As an added benefit, it's now quite easy to avoid traversing beyond
provably-unreachable blocks. In contrast, the current
implementation always visits all blocks.
The prove pass already has nearly all of the mechanism necessary to
compute unsatisfiability, which means this both simplifies the code
and makes it more powerful.
The only complication is that the current implication procedure has a
hack for dealing with the 0 <= Args[0] condition of OpIsInBounds and
OpIsSliceInBounds. We replace this with asserting the appropriate fact
when we process one of these conditions. This seems much cleaner
anyway, and works because we can now take advantage of derived facts.
This has no measurable effect on compiler performance.
Effectiveness:
There is exactly one condition in all of std and cmd that this fails
to prove that the old implementation could: (int64(^uint(0)>>1) < x)
in encoding/gob. This can never be true because x is an int, and it's
basically coincidence that the old code gets this. (For example, it
fails to prove the similar (x < ^int64(^uint(0)>>1)) condition that
immediately precedes it, and even though the conditions are logically
unrelated, it wouldn't get the second one if it hadn't first processed
the first!)
It does, however, prove a few dozen additional branches. These come
from facts that are added to the fact table about the relations
between len and cap. These were almost never queried directly before,
but could lead to contradictions, which the unsat-based approach is
able to use.
There are exactly two branches in std and cmd that this implementation
proves in the *other* direction. This sounds scary, but is okay
because both occur in already-unreachable blocks, so it doesn't matter
what we chose. Because the fact table logic is sound but incomplete,
it fails to prove that the block isn't reachable, even though it is
able to prove that both outgoing branches are impossible. We could
turn these blocks into BlockExit blocks, but it doesn't seem worth the
trouble of the extra proof effort for something that happens twice in
all of std and cmd.
Tests:
This CL updates test/prove.go to change the expected messages because
it can no longer give a "reason" why it proved or disproved a
condition. It also adds a new test of a branch it couldn't prove
before.
It mostly guts test/sliceopt.go, removing everything related to slice
bounds optimizations and moving a few relevant tests to test/prove.go.
Much of this test is actually unreachable. The new prove pass figures
this out and doesn't try to prove anything about the unreachable
parts. The output on the unreachable parts is already suspect because
anything can be proved at that point, so it's really just a regression
test for an algorithm the compiler no longer uses.
This is a step toward fixing #23354. That issue is quite easy to fix
once we can use derived facts effectively.
Change-Id: Ia48a1b9ee081310579fe474e4a61857424ff8ce8
Reviewed-on: https://go-review.googlesource.com/87478
Reviewed-by: Keith Randall <khr@golang.org>
2018-01-10 16:28:58 -05:00
|
|
|
}
|
2016-03-23 10:20:44 -07:00
|
|
|
|
2024-06-14 22:29:09 -07:00
|
|
|
// Extract new constant limits based on the comparison.
|
2024-07-07 14:58:47 -07:00
|
|
|
vLimit := ft.limits[v.ID]
|
|
|
|
|
wLimit := ft.limits[w.ID]
|
|
|
|
|
// Note: all the +1/-1 below could overflow/underflow. Either will
|
|
|
|
|
// still generate correct results, it will just lead to imprecision.
|
|
|
|
|
// In fact if there is overflow/underflow, the corresponding
|
|
|
|
|
// code is unreachable because the known range is outside the range
|
|
|
|
|
// of the value's type.
|
|
|
|
|
switch d {
|
|
|
|
|
case signed:
|
|
|
|
|
switch r {
|
|
|
|
|
case eq: // v == w
|
|
|
|
|
ft.signedMinMax(v, wLimit.min, wLimit.max)
|
|
|
|
|
ft.signedMinMax(w, vLimit.min, vLimit.max)
|
|
|
|
|
case lt: // v < w
|
|
|
|
|
ft.signedMax(v, wLimit.max-1)
|
|
|
|
|
ft.signedMin(w, vLimit.min+1)
|
|
|
|
|
case lt | eq: // v <= w
|
|
|
|
|
ft.signedMax(v, wLimit.max)
|
|
|
|
|
ft.signedMin(w, vLimit.min)
|
|
|
|
|
case gt: // v > w
|
|
|
|
|
ft.signedMin(v, wLimit.min+1)
|
|
|
|
|
ft.signedMax(w, vLimit.max-1)
|
|
|
|
|
case gt | eq: // v >= w
|
|
|
|
|
ft.signedMin(v, wLimit.min)
|
|
|
|
|
ft.signedMax(w, vLimit.max)
|
|
|
|
|
case lt | gt: // v != w
|
|
|
|
|
if vLimit.min == vLimit.max { // v is a constant
|
|
|
|
|
c := vLimit.min
|
|
|
|
|
if wLimit.min == c {
|
|
|
|
|
ft.signedMin(w, c+1)
|
2016-03-23 10:20:44 -07:00
|
|
|
}
|
2024-07-07 14:58:47 -07:00
|
|
|
if wLimit.max == c {
|
|
|
|
|
ft.signedMax(w, c-1)
|
2016-03-23 10:20:44 -07:00
|
|
|
}
|
2018-01-11 14:23:01 -05:00
|
|
|
}
|
2024-07-07 14:58:47 -07:00
|
|
|
if wLimit.min == wLimit.max { // w is a constant
|
|
|
|
|
c := wLimit.min
|
|
|
|
|
if vLimit.min == c {
|
|
|
|
|
ft.signedMin(v, c+1)
|
2016-03-23 10:20:44 -07:00
|
|
|
}
|
2024-07-07 14:58:47 -07:00
|
|
|
if vLimit.max == c {
|
|
|
|
|
ft.signedMax(v, c-1)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
case unsigned:
|
|
|
|
|
switch r {
|
|
|
|
|
case eq: // v == w
|
|
|
|
|
ft.unsignedMinMax(v, wLimit.umin, wLimit.umax)
|
|
|
|
|
ft.unsignedMinMax(w, vLimit.umin, vLimit.umax)
|
|
|
|
|
case lt: // v < w
|
|
|
|
|
ft.unsignedMax(v, wLimit.umax-1)
|
|
|
|
|
ft.unsignedMin(w, vLimit.umin+1)
|
|
|
|
|
case lt | eq: // v <= w
|
|
|
|
|
ft.unsignedMax(v, wLimit.umax)
|
|
|
|
|
ft.unsignedMin(w, vLimit.umin)
|
|
|
|
|
case gt: // v > w
|
|
|
|
|
ft.unsignedMin(v, wLimit.umin+1)
|
|
|
|
|
ft.unsignedMax(w, vLimit.umax-1)
|
|
|
|
|
case gt | eq: // v >= w
|
|
|
|
|
ft.unsignedMin(v, wLimit.umin)
|
|
|
|
|
ft.unsignedMax(w, vLimit.umax)
|
|
|
|
|
case lt | gt: // v != w
|
|
|
|
|
if vLimit.umin == vLimit.umax { // v is a constant
|
|
|
|
|
c := vLimit.umin
|
|
|
|
|
if wLimit.umin == c {
|
|
|
|
|
ft.unsignedMin(w, c+1)
|
|
|
|
|
}
|
|
|
|
|
if wLimit.umax == c {
|
|
|
|
|
ft.unsignedMax(w, c-1)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if wLimit.umin == wLimit.umax { // w is a constant
|
|
|
|
|
c := wLimit.umin
|
|
|
|
|
if vLimit.umin == c {
|
|
|
|
|
ft.unsignedMin(v, c+1)
|
|
|
|
|
}
|
|
|
|
|
if vLimit.umax == c {
|
|
|
|
|
ft.unsignedMax(v, c-1)
|
2016-03-23 10:20:44 -07:00
|
|
|
}
|
|
|
|
|
}
|
2018-03-13 00:21:57 +01:00
|
|
|
}
|
2024-07-07 14:58:47 -07:00
|
|
|
case boolean:
|
|
|
|
|
switch r {
|
|
|
|
|
case eq: // v == w
|
|
|
|
|
if vLimit.min == 1 { // v is true
|
|
|
|
|
ft.booleanTrue(w)
|
|
|
|
|
}
|
|
|
|
|
if vLimit.max == 0 { // v is false
|
|
|
|
|
ft.booleanFalse(w)
|
|
|
|
|
}
|
|
|
|
|
if wLimit.min == 1 { // w is true
|
|
|
|
|
ft.booleanTrue(v)
|
|
|
|
|
}
|
|
|
|
|
if wLimit.max == 0 { // w is false
|
|
|
|
|
ft.booleanFalse(v)
|
|
|
|
|
}
|
|
|
|
|
case lt | gt: // v != w
|
|
|
|
|
if vLimit.min == 1 { // v is true
|
|
|
|
|
ft.booleanFalse(w)
|
|
|
|
|
}
|
|
|
|
|
if vLimit.max == 0 { // v is false
|
|
|
|
|
ft.booleanTrue(w)
|
|
|
|
|
}
|
|
|
|
|
if wLimit.min == 1 { // w is true
|
|
|
|
|
ft.booleanFalse(v)
|
|
|
|
|
}
|
|
|
|
|
if wLimit.max == 0 { // w is false
|
|
|
|
|
ft.booleanTrue(v)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
case pointer:
|
|
|
|
|
switch r {
|
|
|
|
|
case eq: // v == w
|
|
|
|
|
if vLimit.umax == 0 { // v is nil
|
|
|
|
|
ft.pointerNil(w)
|
|
|
|
|
}
|
|
|
|
|
if vLimit.umin > 0 { // v is non-nil
|
|
|
|
|
ft.pointerNonNil(w)
|
|
|
|
|
}
|
|
|
|
|
if wLimit.umax == 0 { // w is nil
|
|
|
|
|
ft.pointerNil(v)
|
|
|
|
|
}
|
|
|
|
|
if wLimit.umin > 0 { // w is non-nil
|
|
|
|
|
ft.pointerNonNil(v)
|
|
|
|
|
}
|
|
|
|
|
case lt | gt: // v != w
|
|
|
|
|
if vLimit.umax == 0 { // v is nil
|
|
|
|
|
ft.pointerNonNil(w)
|
|
|
|
|
}
|
|
|
|
|
if wLimit.umax == 0 { // w is nil
|
|
|
|
|
ft.pointerNonNil(v)
|
|
|
|
|
}
|
|
|
|
|
// Note: the other direction doesn't work.
|
|
|
|
|
// Being not equal to a non-nil pointer doesn't
|
|
|
|
|
// make you (necessarily) a nil pointer.
|
|
|
|
|
}
|
2016-03-23 10:20:44 -07:00
|
|
|
}
|
2018-01-11 16:32:02 -05:00
|
|
|
|
2018-03-23 17:25:26 -04:00
|
|
|
// Derived facts below here are only about numbers.
|
|
|
|
|
if d != signed && d != unsigned {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Additional facts we know given the relationship between len and cap.
|
|
|
|
|
//
|
|
|
|
|
// TODO: Since prove now derives transitive relations, it
|
|
|
|
|
// should be sufficient to learn that len(w) <= cap(w) at the
|
|
|
|
|
// beginning of prove where we look for all len/cap ops.
|
|
|
|
|
if v.Op == OpSliceLen && r< == 0 && ft.caps[v.Args[0].ID] != nil {
|
|
|
|
|
// len(s) > w implies cap(s) > w
|
|
|
|
|
// len(s) >= w implies cap(s) >= w
|
|
|
|
|
// len(s) == w implies cap(s) >= w
|
|
|
|
|
ft.update(parent, ft.caps[v.Args[0].ID], w, d, r|gt)
|
|
|
|
|
}
|
|
|
|
|
if w.Op == OpSliceLen && r> == 0 && ft.caps[w.Args[0].ID] != nil {
|
|
|
|
|
// same, length on the RHS.
|
|
|
|
|
ft.update(parent, v, ft.caps[w.Args[0].ID], d, r|lt)
|
|
|
|
|
}
|
|
|
|
|
if v.Op == OpSliceCap && r> == 0 && ft.lens[v.Args[0].ID] != nil {
|
|
|
|
|
// cap(s) < w implies len(s) < w
|
|
|
|
|
// cap(s) <= w implies len(s) <= w
|
|
|
|
|
// cap(s) == w implies len(s) <= w
|
|
|
|
|
ft.update(parent, ft.lens[v.Args[0].ID], w, d, r|lt)
|
|
|
|
|
}
|
|
|
|
|
if w.Op == OpSliceCap && r< == 0 && ft.lens[w.Args[0].ID] != nil {
|
|
|
|
|
// same, capacity on the RHS.
|
|
|
|
|
ft.update(parent, v, ft.lens[w.Args[0].ID], d, r|gt)
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-11 16:32:02 -05:00
|
|
|
// Process fence-post implications.
|
|
|
|
|
//
|
|
|
|
|
// First, make the condition > or >=.
|
|
|
|
|
if r == lt || r == lt|eq {
|
|
|
|
|
v, w = w, v
|
|
|
|
|
r = reverseBits[r]
|
|
|
|
|
}
|
|
|
|
|
switch r {
|
|
|
|
|
case gt:
|
|
|
|
|
if x, delta := isConstDelta(v); x != nil && delta == 1 {
|
|
|
|
|
// x+1 > w ⇒ x >= w
|
|
|
|
|
//
|
|
|
|
|
// This is useful for eliminating the
|
|
|
|
|
// growslice branch of append.
|
|
|
|
|
ft.update(parent, x, w, d, gt|eq)
|
|
|
|
|
} else if x, delta := isConstDelta(w); x != nil && delta == -1 {
|
|
|
|
|
// v > x-1 ⇒ v >= x
|
|
|
|
|
ft.update(parent, v, x, d, gt|eq)
|
|
|
|
|
}
|
|
|
|
|
case gt | eq:
|
|
|
|
|
if x, delta := isConstDelta(v); x != nil && delta == -1 {
|
|
|
|
|
// x-1 >= w && x > min ⇒ x > w
|
|
|
|
|
//
|
|
|
|
|
// Useful for i > 0; s[i-1].
|
2024-05-18 08:01:27 -07:00
|
|
|
lim := ft.limits[x.ID]
|
2024-07-11 19:38:33 -07:00
|
|
|
if (d == signed && lim.min > opMin[v.Op]) || (d == unsigned && lim.umin > 0) {
|
2018-01-11 16:32:02 -05:00
|
|
|
ft.update(parent, x, w, d, gt)
|
|
|
|
|
}
|
|
|
|
|
} else if x, delta := isConstDelta(w); x != nil && delta == 1 {
|
|
|
|
|
// v >= x+1 && x < max ⇒ v > x
|
2024-05-18 08:01:27 -07:00
|
|
|
lim := ft.limits[x.ID]
|
2024-07-11 19:38:33 -07:00
|
|
|
if (d == signed && lim.max < opMax[w.Op]) || (d == unsigned && lim.umax < opUMax[w.Op]) {
|
2018-01-11 16:32:02 -05:00
|
|
|
ft.update(parent, v, x, d, gt)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-04-02 01:57:49 +02:00
|
|
|
|
2018-04-15 16:52:49 +02:00
|
|
|
// Process: x+delta > w (with delta constant)
|
|
|
|
|
// Only signed domain for now (useful for accesses to slices in loops).
|
2018-04-02 01:57:49 +02:00
|
|
|
if r == gt || r == gt|eq {
|
2018-04-15 16:52:49 +02:00
|
|
|
if x, delta := isConstDelta(v); x != nil && d == signed {
|
2018-04-02 01:57:49 +02:00
|
|
|
if parent.Func.pass.debug > 1 {
|
2019-01-02 12:27:55 -05:00
|
|
|
parent.Func.Warnl(parent.Pos, "x+d %s w; x:%v %v delta:%v w:%v d:%v", r, x, parent.String(), delta, w.AuxInt, d)
|
2018-04-02 01:57:49 +02:00
|
|
|
}
|
2022-05-13 23:52:48 +08:00
|
|
|
underflow := true
|
2024-07-11 19:38:33 -07:00
|
|
|
if delta < 0 {
|
|
|
|
|
l := ft.limits[x.ID]
|
2022-05-13 23:52:48 +08:00
|
|
|
if (x.Type.Size() == 8 && l.min >= math.MinInt64-delta) ||
|
|
|
|
|
(x.Type.Size() == 4 && l.min >= math.MinInt32-delta) {
|
|
|
|
|
underflow = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if delta < 0 && !underflow {
|
|
|
|
|
// If delta < 0 and x+delta cannot underflow then x > x+delta (that is, x > v)
|
|
|
|
|
ft.update(parent, x, v, signed, gt)
|
|
|
|
|
}
|
2018-04-15 16:52:49 +02:00
|
|
|
if !w.isGenericIntConst() {
|
|
|
|
|
// If we know that x+delta > w but w is not constant, we can derive:
|
2022-05-13 23:52:48 +08:00
|
|
|
// if delta < 0 and x+delta cannot underflow, then x > w
|
2018-04-15 16:52:49 +02:00
|
|
|
// This is useful for loops with bounds "len(slice)-K" (delta = -K)
|
2022-05-13 23:52:48 +08:00
|
|
|
if delta < 0 && !underflow {
|
|
|
|
|
ft.update(parent, x, w, signed, r)
|
2018-04-15 16:52:49 +02:00
|
|
|
}
|
2018-04-02 01:57:49 +02:00
|
|
|
} else {
|
2018-04-15 16:52:49 +02:00
|
|
|
// With w,delta constants, we want to derive: x+delta > w ⇒ x > w-delta
|
|
|
|
|
//
|
|
|
|
|
// We compute (using integers of the correct size):
|
|
|
|
|
// min = w - delta
|
|
|
|
|
// max = MaxInt - delta
|
|
|
|
|
//
|
|
|
|
|
// And we prove that:
|
|
|
|
|
// if min<max: min < x AND x <= max
|
|
|
|
|
// if min>max: min < x OR x <= max
|
|
|
|
|
//
|
|
|
|
|
// This is always correct, even in case of overflow.
|
|
|
|
|
//
|
|
|
|
|
// If the initial fact is x+delta >= w instead, the derived conditions are:
|
|
|
|
|
// if min<max: min <= x AND x <= max
|
|
|
|
|
// if min>max: min <= x OR x <= max
|
|
|
|
|
//
|
|
|
|
|
// Notice the conditions for max are still <=, as they handle overflows.
|
|
|
|
|
var min, max int64
|
|
|
|
|
switch x.Type.Size() {
|
|
|
|
|
case 8:
|
|
|
|
|
min = w.AuxInt - delta
|
|
|
|
|
max = int64(^uint64(0)>>1) - delta
|
|
|
|
|
case 4:
|
|
|
|
|
min = int64(int32(w.AuxInt) - int32(delta))
|
|
|
|
|
max = int64(int32(^uint32(0)>>1) - int32(delta))
|
2022-09-16 12:16:00 +02:00
|
|
|
case 2:
|
|
|
|
|
min = int64(int16(w.AuxInt) - int16(delta))
|
|
|
|
|
max = int64(int16(^uint16(0)>>1) - int16(delta))
|
|
|
|
|
case 1:
|
|
|
|
|
min = int64(int8(w.AuxInt) - int8(delta))
|
|
|
|
|
max = int64(int8(^uint8(0)>>1) - int8(delta))
|
2018-04-15 16:52:49 +02:00
|
|
|
default:
|
|
|
|
|
panic("unimplemented")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if min < max {
|
|
|
|
|
// Record that x > min and max >= x
|
2024-06-14 22:29:09 -07:00
|
|
|
if r == gt {
|
|
|
|
|
min++
|
|
|
|
|
}
|
|
|
|
|
ft.signedMinMax(x, min, max)
|
2018-04-15 16:52:49 +02:00
|
|
|
} else {
|
|
|
|
|
// We know that either x>min OR x<=max. factsTable cannot record OR conditions,
|
|
|
|
|
// so let's see if we can already prove that one of them is false, in which case
|
|
|
|
|
// the other must be true
|
2024-07-11 19:38:33 -07:00
|
|
|
l := ft.limits[x.ID]
|
|
|
|
|
if l.max <= min {
|
|
|
|
|
if r&eq == 0 || l.max < min {
|
|
|
|
|
// x>min (x>=min) is impossible, so it must be x<=max
|
|
|
|
|
ft.signedMax(x, max)
|
2018-04-15 16:52:49 +02:00
|
|
|
}
|
2024-07-11 19:38:33 -07:00
|
|
|
} else if l.min > max {
|
|
|
|
|
// x<=max is impossible, so it must be x>min
|
2024-08-09 10:59:43 -07:00
|
|
|
if r == gt {
|
|
|
|
|
min++
|
|
|
|
|
}
|
2024-07-11 19:38:33 -07:00
|
|
|
ft.signedMin(x, min)
|
2018-04-02 01:57:49 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
cmd/compile: handle sign/zero extensions in prove, via update method
Array accesses with index types smaller than the machine word size may
involve a sign or zero extension of the index value before bounds
checking. Currently, this defeats prove because the facts about the
original index value don't flow through the sign/zero extension.
This CL fixes this by looking back through value-preserving sign/zero
extensions when adding facts via Update and, where appropriate, applying
the same facts using the pre-extension value. This fix is enhanced by
also looking back through value-preserving extensions within
ft.isNonNegative to infer whether the extended value is known to be
non-negative. Without this additional isNonNegative enhancement, this
logic is rendered significantly less effective by the limitation
discussed in the next paragraph.
In Update, the application of facts to pre-extension values is limited
to cases where the domain of the new fact is consistent with the type of
the pre-extension value. There may be cases where this cross-domain
passing of facts is valid, but distinguishing them from the invalid
cases is difficult for me to reason about and to implement.
Assessing which cases to allow requires details about the context and
inferences behind the fact being applied which are not available
within Update. Additional difficulty arises from the fact that the SSA
does not curently differentiate extensions added by the compiler for
indexing operations, extensions added by the compiler for implicit
conversions, or explicit extensions from the source.
Examples of some cases that would need to be filtered correctly for
cross-domain facts:
(1) A uint8 is zero-extended to int for indexing (a value-preserving
zeroExt). When, if ever, can signed domain facts learned about the int be
applied to the uint8?
(2) An int8 is sign-extended to int16 (value-preserving) for an equality
comparison. Equality comparison facts are currently always learned in both
the signed and unsigned domains. When, if ever, can the unsigned facts
learned about the int16, from the int16 != int16 comparison, be applied
to the original int8?
This is an alternative to CL 122695 and CL 174309. Compared to CL 122695,
this CL differs in that the facts added about the pre-extension value will
pass through the Update method, where additional inferences are processed
(e.g. fence-post implications, see #29964). CL 174309 is limited to bounds
checks, so is narrower in application, and makes the code harder to read.
Fixes #26292.
Fixes #29964.
Fixes #15074
Removes 238 bounds checks from std/cmd.
Change-Id: I1f87c32ee672bfb8be397b27eab7a4c2f304893f
Reviewed-on: https://go-review.googlesource.com/c/go/+/174704
Run-TryBot: Zach Jones <zachj1@gmail.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Giovanni Bajo <rasky@develer.com>
2019-04-09 23:19:43 +01:00
|
|
|
// Look through value-preserving extensions.
|
|
|
|
|
// If the domain is appropriate for the pre-extension Type,
|
|
|
|
|
// repeat the update with the pre-extension Value.
|
|
|
|
|
if isCleanExt(v) {
|
|
|
|
|
switch {
|
|
|
|
|
case d == signed && v.Args[0].Type.IsSigned():
|
|
|
|
|
fallthrough
|
|
|
|
|
case d == unsigned && !v.Args[0].Type.IsSigned():
|
|
|
|
|
ft.update(parent, v.Args[0], w, d, r)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if isCleanExt(w) {
|
|
|
|
|
switch {
|
|
|
|
|
case d == signed && w.Args[0].Type.IsSigned():
|
|
|
|
|
fallthrough
|
|
|
|
|
case d == unsigned && !w.Args[0].Type.IsSigned():
|
|
|
|
|
ft.update(parent, v, w.Args[0], d, r)
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-01-11 16:32:02 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var opMin = map[Op]int64{
|
|
|
|
|
OpAdd64: math.MinInt64, OpSub64: math.MinInt64,
|
|
|
|
|
OpAdd32: math.MinInt32, OpSub32: math.MinInt32,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var opMax = map[Op]int64{
|
|
|
|
|
OpAdd64: math.MaxInt64, OpSub64: math.MaxInt64,
|
|
|
|
|
OpAdd32: math.MaxInt32, OpSub32: math.MaxInt32,
|
2016-03-07 18:36:16 +01:00
|
|
|
}
|
|
|
|
|
|
2018-08-31 02:15:26 +02:00
|
|
|
var opUMax = map[Op]uint64{
|
|
|
|
|
OpAdd64: math.MaxUint64, OpSub64: math.MaxUint64,
|
|
|
|
|
OpAdd32: math.MaxUint32, OpSub32: math.MaxUint32,
|
|
|
|
|
}
|
|
|
|
|
|
2018-04-25 11:52:06 -07:00
|
|
|
// isNonNegative reports whether v is known to be non-negative.
|
2016-04-02 10:29:11 +02:00
|
|
|
func (ft *factsTable) isNonNegative(v *Value) bool {
|
2024-06-14 22:29:09 -07:00
|
|
|
return ft.limits[v.ID].min >= 0
|
2016-04-02 10:29:11 +02:00
|
|
|
}
|
|
|
|
|
|
2016-03-07 18:36:16 +01:00
|
|
|
// checkpoint saves the current state of known relations.
|
|
|
|
|
// Called when descending on a branch.
|
|
|
|
|
func (ft *factsTable) checkpoint() {
|
cmd/compile: make prove pass use unsatisfiability
Currently the prove pass uses implication queries. For each block, it
collects the set of branch conditions leading to that block, and
queries this fact table for whether any of these facts imply the
block's own branch condition (or its inverse). This works remarkably
well considering it doesn't do any deduction on these facts, but it
has various downsides:
1. It requires an implementation both of adding facts to the table and
determining implications. These are very nearly duals of each
other, but require separate implementations. Likewise, the process
of asserting facts of dominating branch conditions is very nearly
the dual of the process of querying implied branch conditions.
2. It leads to less effective use of derived facts. For example, the
prove pass currently derives facts about the relations between len
and cap, but can't make use of these unless a branch condition is
in the exact form of a derived fact. If one of these derived facts
contradicts another fact, it won't notice or make use of this.
This CL changes the approach of the prove pass to instead use
*contradiction* instead of implication. Rather than ever querying a
branch condition, it simply adds branch conditions to the fact table.
If this leads to a contradiction (specifically, it makes the fact set
unsatisfiable), that branch is impossible and can be cut. As a result,
1. We can eliminate the code for determining implications
(factsTable.get disappears entirely). Also, there is now a single
implementation of visiting and asserting branch conditions, since
we don't have to flip them around to treat them as facts in one
place and queries in another.
2. Derived facts can be used effectively. It doesn't matter *why* the
fact table is unsatisfiable; a contradiction in any of the facts is
enough.
3. As an added benefit, it's now quite easy to avoid traversing beyond
provably-unreachable blocks. In contrast, the current
implementation always visits all blocks.
The prove pass already has nearly all of the mechanism necessary to
compute unsatisfiability, which means this both simplifies the code
and makes it more powerful.
The only complication is that the current implication procedure has a
hack for dealing with the 0 <= Args[0] condition of OpIsInBounds and
OpIsSliceInBounds. We replace this with asserting the appropriate fact
when we process one of these conditions. This seems much cleaner
anyway, and works because we can now take advantage of derived facts.
This has no measurable effect on compiler performance.
Effectiveness:
There is exactly one condition in all of std and cmd that this fails
to prove that the old implementation could: (int64(^uint(0)>>1) < x)
in encoding/gob. This can never be true because x is an int, and it's
basically coincidence that the old code gets this. (For example, it
fails to prove the similar (x < ^int64(^uint(0)>>1)) condition that
immediately precedes it, and even though the conditions are logically
unrelated, it wouldn't get the second one if it hadn't first processed
the first!)
It does, however, prove a few dozen additional branches. These come
from facts that are added to the fact table about the relations
between len and cap. These were almost never queried directly before,
but could lead to contradictions, which the unsat-based approach is
able to use.
There are exactly two branches in std and cmd that this implementation
proves in the *other* direction. This sounds scary, but is okay
because both occur in already-unreachable blocks, so it doesn't matter
what we chose. Because the fact table logic is sound but incomplete,
it fails to prove that the block isn't reachable, even though it is
able to prove that both outgoing branches are impossible. We could
turn these blocks into BlockExit blocks, but it doesn't seem worth the
trouble of the extra proof effort for something that happens twice in
all of std and cmd.
Tests:
This CL updates test/prove.go to change the expected messages because
it can no longer give a "reason" why it proved or disproved a
condition. It also adds a new test of a branch it couldn't prove
before.
It mostly guts test/sliceopt.go, removing everything related to slice
bounds optimizations and moving a few relevant tests to test/prove.go.
Much of this test is actually unreachable. The new prove pass figures
this out and doesn't try to prove anything about the unreachable
parts. The output on the unreachable parts is already suspect because
anything can be proved at that point, so it's really just a regression
test for an algorithm the compiler no longer uses.
This is a step toward fixing #23354. That issue is quite easy to fix
once we can use derived facts effectively.
Change-Id: Ia48a1b9ee081310579fe474e4a61857424ff8ce8
Reviewed-on: https://go-review.googlesource.com/87478
Reviewed-by: Keith Randall <khr@golang.org>
2018-01-10 16:28:58 -05:00
|
|
|
if ft.unsat {
|
|
|
|
|
ft.unsatDepth++
|
|
|
|
|
}
|
2016-03-23 10:20:44 -07:00
|
|
|
ft.limitStack = append(ft.limitStack, checkpointBound)
|
2019-09-20 00:12:15 +02:00
|
|
|
ft.orderS.Checkpoint()
|
|
|
|
|
ft.orderU.Checkpoint()
|
2024-06-14 22:29:09 -07:00
|
|
|
ft.orderingsStack = append(ft.orderingsStack, 0)
|
2016-03-07 18:36:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// restore restores known relation to the state just
|
|
|
|
|
// before the previous checkpoint.
|
|
|
|
|
// Called when backing up on a branch.
|
|
|
|
|
func (ft *factsTable) restore() {
|
cmd/compile: make prove pass use unsatisfiability
Currently the prove pass uses implication queries. For each block, it
collects the set of branch conditions leading to that block, and
queries this fact table for whether any of these facts imply the
block's own branch condition (or its inverse). This works remarkably
well considering it doesn't do any deduction on these facts, but it
has various downsides:
1. It requires an implementation both of adding facts to the table and
determining implications. These are very nearly duals of each
other, but require separate implementations. Likewise, the process
of asserting facts of dominating branch conditions is very nearly
the dual of the process of querying implied branch conditions.
2. It leads to less effective use of derived facts. For example, the
prove pass currently derives facts about the relations between len
and cap, but can't make use of these unless a branch condition is
in the exact form of a derived fact. If one of these derived facts
contradicts another fact, it won't notice or make use of this.
This CL changes the approach of the prove pass to instead use
*contradiction* instead of implication. Rather than ever querying a
branch condition, it simply adds branch conditions to the fact table.
If this leads to a contradiction (specifically, it makes the fact set
unsatisfiable), that branch is impossible and can be cut. As a result,
1. We can eliminate the code for determining implications
(factsTable.get disappears entirely). Also, there is now a single
implementation of visiting and asserting branch conditions, since
we don't have to flip them around to treat them as facts in one
place and queries in another.
2. Derived facts can be used effectively. It doesn't matter *why* the
fact table is unsatisfiable; a contradiction in any of the facts is
enough.
3. As an added benefit, it's now quite easy to avoid traversing beyond
provably-unreachable blocks. In contrast, the current
implementation always visits all blocks.
The prove pass already has nearly all of the mechanism necessary to
compute unsatisfiability, which means this both simplifies the code
and makes it more powerful.
The only complication is that the current implication procedure has a
hack for dealing with the 0 <= Args[0] condition of OpIsInBounds and
OpIsSliceInBounds. We replace this with asserting the appropriate fact
when we process one of these conditions. This seems much cleaner
anyway, and works because we can now take advantage of derived facts.
This has no measurable effect on compiler performance.
Effectiveness:
There is exactly one condition in all of std and cmd that this fails
to prove that the old implementation could: (int64(^uint(0)>>1) < x)
in encoding/gob. This can never be true because x is an int, and it's
basically coincidence that the old code gets this. (For example, it
fails to prove the similar (x < ^int64(^uint(0)>>1)) condition that
immediately precedes it, and even though the conditions are logically
unrelated, it wouldn't get the second one if it hadn't first processed
the first!)
It does, however, prove a few dozen additional branches. These come
from facts that are added to the fact table about the relations
between len and cap. These were almost never queried directly before,
but could lead to contradictions, which the unsat-based approach is
able to use.
There are exactly two branches in std and cmd that this implementation
proves in the *other* direction. This sounds scary, but is okay
because both occur in already-unreachable blocks, so it doesn't matter
what we chose. Because the fact table logic is sound but incomplete,
it fails to prove that the block isn't reachable, even though it is
able to prove that both outgoing branches are impossible. We could
turn these blocks into BlockExit blocks, but it doesn't seem worth the
trouble of the extra proof effort for something that happens twice in
all of std and cmd.
Tests:
This CL updates test/prove.go to change the expected messages because
it can no longer give a "reason" why it proved or disproved a
condition. It also adds a new test of a branch it couldn't prove
before.
It mostly guts test/sliceopt.go, removing everything related to slice
bounds optimizations and moving a few relevant tests to test/prove.go.
Much of this test is actually unreachable. The new prove pass figures
this out and doesn't try to prove anything about the unreachable
parts. The output on the unreachable parts is already suspect because
anything can be proved at that point, so it's really just a regression
test for an algorithm the compiler no longer uses.
This is a step toward fixing #23354. That issue is quite easy to fix
once we can use derived facts effectively.
Change-Id: Ia48a1b9ee081310579fe474e4a61857424ff8ce8
Reviewed-on: https://go-review.googlesource.com/87478
Reviewed-by: Keith Randall <khr@golang.org>
2018-01-10 16:28:58 -05:00
|
|
|
if ft.unsatDepth > 0 {
|
|
|
|
|
ft.unsatDepth--
|
|
|
|
|
} else {
|
|
|
|
|
ft.unsat = false
|
|
|
|
|
}
|
2016-03-23 10:20:44 -07:00
|
|
|
for {
|
|
|
|
|
old := ft.limitStack[len(ft.limitStack)-1]
|
|
|
|
|
ft.limitStack = ft.limitStack[:len(ft.limitStack)-1]
|
|
|
|
|
if old.vid == 0 { // checkpointBound
|
|
|
|
|
break
|
|
|
|
|
}
|
2024-05-18 08:01:27 -07:00
|
|
|
ft.limits[old.vid] = old.limit
|
2016-03-23 10:20:44 -07:00
|
|
|
}
|
2019-09-20 00:12:15 +02:00
|
|
|
ft.orderS.Undo()
|
|
|
|
|
ft.orderU.Undo()
|
2024-06-14 22:29:09 -07:00
|
|
|
for {
|
|
|
|
|
id := ft.orderingsStack[len(ft.orderingsStack)-1]
|
|
|
|
|
ft.orderingsStack = ft.orderingsStack[:len(ft.orderingsStack)-1]
|
|
|
|
|
if id == 0 { // checkpoint marker
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
o := ft.orderings[id]
|
|
|
|
|
ft.orderings[id] = o.next
|
|
|
|
|
o.next = ft.orderingCache
|
|
|
|
|
ft.orderingCache = o
|
|
|
|
|
}
|
2016-03-07 18:36:16 +01:00
|
|
|
}
|
|
|
|
|
|
2016-02-19 12:14:42 +01:00
|
|
|
var (
|
2016-03-07 18:36:16 +01:00
|
|
|
reverseBits = [...]relation{0, 4, 2, 6, 1, 5, 3, 7}
|
2016-02-19 12:14:42 +01:00
|
|
|
|
|
|
|
|
// maps what we learn when the positive branch is taken.
|
|
|
|
|
// For example:
|
|
|
|
|
// OpLess8: {signed, lt},
|
|
|
|
|
// v1 = (OpLess8 v2 v3).
|
2024-07-11 12:10:10 -07:00
|
|
|
// If we learn that v1 is true, then we can deduce that v2<v3
|
|
|
|
|
// in the signed domain.
|
2016-03-07 18:36:16 +01:00
|
|
|
domainRelationTable = map[Op]struct {
|
|
|
|
|
d domain
|
|
|
|
|
r relation
|
|
|
|
|
}{
|
2016-02-19 12:14:42 +01:00
|
|
|
OpEq8: {signed | unsigned, eq},
|
|
|
|
|
OpEq16: {signed | unsigned, eq},
|
|
|
|
|
OpEq32: {signed | unsigned, eq},
|
|
|
|
|
OpEq64: {signed | unsigned, eq},
|
|
|
|
|
OpEqPtr: {pointer, eq},
|
2024-07-07 14:58:47 -07:00
|
|
|
OpEqB: {boolean, eq},
|
2016-02-19 12:14:42 +01:00
|
|
|
|
|
|
|
|
OpNeq8: {signed | unsigned, lt | gt},
|
|
|
|
|
OpNeq16: {signed | unsigned, lt | gt},
|
|
|
|
|
OpNeq32: {signed | unsigned, lt | gt},
|
|
|
|
|
OpNeq64: {signed | unsigned, lt | gt},
|
|
|
|
|
OpNeqPtr: {pointer, lt | gt},
|
2024-07-07 14:58:47 -07:00
|
|
|
OpNeqB: {boolean, lt | gt},
|
2016-02-19 12:14:42 +01:00
|
|
|
|
|
|
|
|
OpLess8: {signed, lt},
|
|
|
|
|
OpLess8U: {unsigned, lt},
|
|
|
|
|
OpLess16: {signed, lt},
|
|
|
|
|
OpLess16U: {unsigned, lt},
|
|
|
|
|
OpLess32: {signed, lt},
|
|
|
|
|
OpLess32U: {unsigned, lt},
|
|
|
|
|
OpLess64: {signed, lt},
|
|
|
|
|
OpLess64U: {unsigned, lt},
|
|
|
|
|
|
|
|
|
|
OpLeq8: {signed, lt | eq},
|
|
|
|
|
OpLeq8U: {unsigned, lt | eq},
|
|
|
|
|
OpLeq16: {signed, lt | eq},
|
|
|
|
|
OpLeq16U: {unsigned, lt | eq},
|
|
|
|
|
OpLeq32: {signed, lt | eq},
|
|
|
|
|
OpLeq32U: {unsigned, lt | eq},
|
|
|
|
|
OpLeq64: {signed, lt | eq},
|
|
|
|
|
OpLeq64U: {unsigned, lt | eq},
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
cmd/compile/ssa: optimize the derivable known branch of If block
When the control value of a If block is known for a particular inbound edge
because its value can be inferred from the control value of its predecessor,
then this inbound edge can be redirected to the known successor directly,
This CL optimizes this kind of cases to eliminate unnecessary comparision.
For example, the following piece of code comes from runtime.atoi,
if !neg && un > uint(maxInt) {
return 0, false
}
if neg && un > uint(maxInt)+1 {
return 0, false
}
Before this optimization, if the first "if" statement does not return, both
conditions of the second "if" statement will be checked. But obviously the
value of neg is known through the first "if" statement, and there is no need
to check neg repeatedly.
After this optimization, this redundancy check is eliminated, and the execution
logic becomes as follows.
if !neg {
if un > uint(maxInt) {
return 0, false
}
} else {
if un > uint(maxInt)+1 {
return 0, false
}
}
This CL does not bring significant performance changes, but it makes the code
structure look more reasonable.
Statistical data from tool compilecmp on Linux/amd64:
name old time/op new time/op delta
Template 380ms ± 4% 385ms ± 3% +1.16% (p=0.000 n=50+49)
Unicode 168ms ± 9% 169ms ± 9% ~ (p=0.421 n=49+46)
GoTypes 1.99s ± 4% 2.02s ± 4% +1.48% (p=0.000 n=49+49)
Compiler 188ms ± 8% 188ms ± 9% ~ (p=0.997 n=49+50)
SSA 11.8s ± 2% 12.0s ± 2% +1.24% (p=0.000 n=48+50)
Flate 242ms ± 6% 244ms ± 9% ~ (p=0.307 n=46+49)
GoParser 361ms ± 3% 366ms ± 4% +1.23% (p=0.000 n=48+49)
Reflect 836ms ± 3% 842ms ± 3% +0.70% (p=0.004 n=48+48)
Tar 335ms ± 3% 340ms ± 4% +1.47% (p=0.000 n=49+46)
XML 432ms ± 4% 437ms ± 4% +1.11% (p=0.002 n=49+49)
LinkCompiler 701ms ± 4% 704ms ± 5% ~ (p=0.278 n=49+50)
ExternalLinkCompiler 1.83s ± 3% 1.84s ± 3% +0.51% (p=0.034 n=48+49)
LinkWithoutDebugCompiler 436ms ± 6% 438ms ± 6% ~ (p=0.419 n=48+49)
[Geo mean] 612ms 617ms +0.84%
name old alloc/op new alloc/op delta
Template 38.7MB ± 1% 39.1MB ± 1% +1.19% (p=0.000 n=50+50)
Unicode 28.1MB ± 0% 28.1MB ± 0% +0.20% (p=0.000 n=49+45)
GoTypes 168MB ± 1% 170MB ± 1% +1.05% (p=0.000 n=48+49)
Compiler 23.0MB ± 1% 23.1MB ± 1% +0.63% (p=0.000 n=50+50)
SSA 1.54GB ± 1% 1.55GB ± 1% +0.85% (p=0.000 n=50+50)
Flate 23.6MB ± 1% 23.9MB ± 1% +1.36% (p=0.000 n=43+46)
GoParser 35.0MB ± 1% 35.3MB ± 1% +0.94% (p=0.000 n=50+50)
Reflect 84.7MB ± 1% 86.1MB ± 1% +1.72% (p=0.000 n=49+49)
Tar 34.5MB ± 1% 34.9MB ± 1% +1.07% (p=0.000 n=47+48)
XML 44.2MB ± 3% 44.6MB ± 3% +0.70% (p=0.003 n=50+49)
LinkCompiler 128MB ± 0% 128MB ± 0% +0.01% (p=0.004 n=49+50)
ExternalLinkCompiler 120MB ± 0% 120MB ± 0% +0.01% (p=0.000 n=49+50)
LinkWithoutDebugCompiler 77.3MB ± 0% 77.3MB ± 0% +0.02% (p=0.000 n=50+50)
[Geo mean] 69.1MB 69.6MB +0.75%
file before after Δ %
addr2line 4049276 4051308 +2032 +0.050%
api 5248940 5248996 +56 +0.001%
asm 4868093 4868037 -56 -0.001%
buildid 2627666 2626026 -1640 -0.062%
cgo 4614432 4615040 +608 +0.013%
compile 23298888 23301267 +2379 +0.010%
cover 4591609 4591161 -448 -0.010%
dist 3449638 3450254 +616 +0.018%
doc 3925667 3926363 +696 +0.018%
fix 3322936 3323464 +528 +0.016%
link 6628632 6629560 +928 +0.014%
nm 3991753 3996497 +4744 +0.119%
objdump 4396119 4395615 -504 -0.011%
pack 2399719 2399535 -184 -0.008%
pprof 13616418 13622866 +6448 +0.047%
test2json 2646121 2646081 -40 -0.002%
trace 10233087 10226359 -6728 -0.066%
vet 7117994 7121066 +3072 +0.043%
total 111026988 111039495 +12507 +0.011%
On linux arm64:
name old time/op new time/op delta
Template 284ms ± 1% 286ms ± 1% +0.70% (p=0.000 n=49+50)
Unicode 125ms ± 3% 125ms ± 2% ~ (p=0.548 n=50+50)
GoTypes 1.69s ± 1% 1.71s ± 1% +1.02% (p=0.000 n=49+49)
Compiler 125ms ± 1% 124ms ± 2% -0.35% (p=0.020 n=50+50)
SSA 12.7s ± 1% 12.8s ± 1% +1.21% (p=0.000 n=49+49)
Flate 172ms ± 1% 173ms ± 1% +0.20% (p=0.047 n=50+50)
GoParser 265ms ± 1% 266ms ± 1% +0.64% (p=0.000 n=50+50)
Reflect 651ms ± 1% 650ms ± 1% ~ (p=0.079 n=48+48)
Tar 246ms ± 1% 246ms ± 1% ~ (p=0.202 n=50+46)
XML 328ms ± 1% 332ms ± 1% +1.28% (p=0.000 n=50+49)
LinkCompiler 600ms ± 1% 599ms ± 1% ~ (p=0.264 n=50+50)
ExternalLinkCompiler 1.88s ± 1% 1.90s ± 0% +1.36% (p=0.000 n=50+50)
LinkWithoutDebugCompiler 365ms ± 1% 365ms ± 1% ~ (p=0.602 n=50+46)
[Geo mean] 490ms 492ms +0.47%
name old alloc/op new alloc/op delta
Template 38.8MB ± 1% 39.1MB ± 1% +0.92% (p=0.000 n=44+42)
Unicode 28.4MB ± 0% 28.4MB ± 0% +0.22% (p=0.000 n=44+45)
GoTypes 169MB ± 1% 171MB ± 1% +1.12% (p=0.000 n=50+50)
Compiler 23.2MB ± 1% 23.3MB ± 1% +0.56% (p=0.000 n=42+43)
SSA 1.55GB ± 0% 1.56GB ± 0% +0.91% (p=0.000 n=48+49)
Flate 23.7MB ± 2% 24.0MB ± 1% +1.20% (p=0.000 n=50+50)
GoParser 35.3MB ± 1% 35.6MB ± 1% +0.88% (p=0.000 n=50+50)
Reflect 85.0MB ± 0% 86.5MB ± 0% +1.70% (p=0.000 n=49+48)
Tar 34.5MB ± 1% 34.9MB ± 1% +1.03% (p=0.000 n=47+50)
XML 43.8MB ± 2% 44.0MB ± 0% +0.41% (p=0.002 n=49+38)
LinkCompiler 136MB ± 0% 136MB ± 0% +0.01% (p=0.006 n=50+49)
ExternalLinkCompiler 127MB ± 0% 127MB ± 0% +0.02% (p=0.000 n=49+50)
LinkWithoutDebugCompiler 84.1MB ± 0% 84.1MB ± 0% ~ (p=0.534 n=50+50)
[Geo mean] 70.4MB 70.9MB +0.69%
file before after Δ %
addr2line 4006004 4004556 -1448 -0.036%
api 5029716 5028828 -888 -0.018%
asm 4936863 4934943 -1920 -0.039%
buildid 2594947 2594099 -848 -0.033%
cgo 4399702 4399502 -200 -0.005%
compile 22233139 22230486 -2653 -0.012%
cover 4443681 4443881 +200 +0.005%
dist 3365902 3365486 -416 -0.012%
doc 3776175 3776151 -24 -0.001%
fix 3218624 3218600 -24 -0.001%
link 6365001 6361409 -3592 -0.056%
nm 3923345 3923065 -280 -0.007%
objdump 4295473 4296673 +1200 +0.028%
pack 2390561 2389393 -1168 -0.049%
pprof 12866419 12865115 -1304 -0.010%
test2json 2587113 2585561 -1552 -0.060%
trace 9609814 9610846 +1032 +0.011%
vet 6790272 6789760 -512 -0.008%
total 106832751 106818354 -14397 -0.013%
Update: #37608
Change-Id: I2831238b12e3af5aef2261f64f804bf0a8b43f86
Reviewed-on: https://go-review.googlesource.com/c/go/+/244737
Reviewed-by: eric fang <eric.fang@arm.com>
Reviewed-by: Keith Randall <khr@golang.org>
Trust: eric fang <eric.fang@arm.com>
Run-TryBot: eric fang <eric.fang@arm.com>
TryBot-Result: Go Bot <gobot@golang.org>
2020-07-09 11:27:03 +00:00
|
|
|
// cleanup returns the posets to the free list
|
|
|
|
|
func (ft *factsTable) cleanup(f *Func) {
|
|
|
|
|
for _, po := range []*poset{ft.orderS, ft.orderU} {
|
|
|
|
|
// Make sure it's empty as it should be. A non-empty poset
|
|
|
|
|
// might cause errors and miscompilations if reused.
|
|
|
|
|
if checkEnabled {
|
|
|
|
|
if err := po.CheckEmpty(); err != nil {
|
|
|
|
|
f.Fatalf("poset not empty after function %s: %v", f.Name, err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
f.retPoset(po)
|
|
|
|
|
}
|
2024-05-18 08:01:27 -07:00
|
|
|
f.Cache.freeLimitSlice(ft.limits)
|
2024-06-14 22:29:09 -07:00
|
|
|
f.Cache.freeBoolSlice(ft.recurseCheck)
|
cmd/compile/ssa: optimize the derivable known branch of If block
When the control value of a If block is known for a particular inbound edge
because its value can be inferred from the control value of its predecessor,
then this inbound edge can be redirected to the known successor directly,
This CL optimizes this kind of cases to eliminate unnecessary comparision.
For example, the following piece of code comes from runtime.atoi,
if !neg && un > uint(maxInt) {
return 0, false
}
if neg && un > uint(maxInt)+1 {
return 0, false
}
Before this optimization, if the first "if" statement does not return, both
conditions of the second "if" statement will be checked. But obviously the
value of neg is known through the first "if" statement, and there is no need
to check neg repeatedly.
After this optimization, this redundancy check is eliminated, and the execution
logic becomes as follows.
if !neg {
if un > uint(maxInt) {
return 0, false
}
} else {
if un > uint(maxInt)+1 {
return 0, false
}
}
This CL does not bring significant performance changes, but it makes the code
structure look more reasonable.
Statistical data from tool compilecmp on Linux/amd64:
name old time/op new time/op delta
Template 380ms ± 4% 385ms ± 3% +1.16% (p=0.000 n=50+49)
Unicode 168ms ± 9% 169ms ± 9% ~ (p=0.421 n=49+46)
GoTypes 1.99s ± 4% 2.02s ± 4% +1.48% (p=0.000 n=49+49)
Compiler 188ms ± 8% 188ms ± 9% ~ (p=0.997 n=49+50)
SSA 11.8s ± 2% 12.0s ± 2% +1.24% (p=0.000 n=48+50)
Flate 242ms ± 6% 244ms ± 9% ~ (p=0.307 n=46+49)
GoParser 361ms ± 3% 366ms ± 4% +1.23% (p=0.000 n=48+49)
Reflect 836ms ± 3% 842ms ± 3% +0.70% (p=0.004 n=48+48)
Tar 335ms ± 3% 340ms ± 4% +1.47% (p=0.000 n=49+46)
XML 432ms ± 4% 437ms ± 4% +1.11% (p=0.002 n=49+49)
LinkCompiler 701ms ± 4% 704ms ± 5% ~ (p=0.278 n=49+50)
ExternalLinkCompiler 1.83s ± 3% 1.84s ± 3% +0.51% (p=0.034 n=48+49)
LinkWithoutDebugCompiler 436ms ± 6% 438ms ± 6% ~ (p=0.419 n=48+49)
[Geo mean] 612ms 617ms +0.84%
name old alloc/op new alloc/op delta
Template 38.7MB ± 1% 39.1MB ± 1% +1.19% (p=0.000 n=50+50)
Unicode 28.1MB ± 0% 28.1MB ± 0% +0.20% (p=0.000 n=49+45)
GoTypes 168MB ± 1% 170MB ± 1% +1.05% (p=0.000 n=48+49)
Compiler 23.0MB ± 1% 23.1MB ± 1% +0.63% (p=0.000 n=50+50)
SSA 1.54GB ± 1% 1.55GB ± 1% +0.85% (p=0.000 n=50+50)
Flate 23.6MB ± 1% 23.9MB ± 1% +1.36% (p=0.000 n=43+46)
GoParser 35.0MB ± 1% 35.3MB ± 1% +0.94% (p=0.000 n=50+50)
Reflect 84.7MB ± 1% 86.1MB ± 1% +1.72% (p=0.000 n=49+49)
Tar 34.5MB ± 1% 34.9MB ± 1% +1.07% (p=0.000 n=47+48)
XML 44.2MB ± 3% 44.6MB ± 3% +0.70% (p=0.003 n=50+49)
LinkCompiler 128MB ± 0% 128MB ± 0% +0.01% (p=0.004 n=49+50)
ExternalLinkCompiler 120MB ± 0% 120MB ± 0% +0.01% (p=0.000 n=49+50)
LinkWithoutDebugCompiler 77.3MB ± 0% 77.3MB ± 0% +0.02% (p=0.000 n=50+50)
[Geo mean] 69.1MB 69.6MB +0.75%
file before after Δ %
addr2line 4049276 4051308 +2032 +0.050%
api 5248940 5248996 +56 +0.001%
asm 4868093 4868037 -56 -0.001%
buildid 2627666 2626026 -1640 -0.062%
cgo 4614432 4615040 +608 +0.013%
compile 23298888 23301267 +2379 +0.010%
cover 4591609 4591161 -448 -0.010%
dist 3449638 3450254 +616 +0.018%
doc 3925667 3926363 +696 +0.018%
fix 3322936 3323464 +528 +0.016%
link 6628632 6629560 +928 +0.014%
nm 3991753 3996497 +4744 +0.119%
objdump 4396119 4395615 -504 -0.011%
pack 2399719 2399535 -184 -0.008%
pprof 13616418 13622866 +6448 +0.047%
test2json 2646121 2646081 -40 -0.002%
trace 10233087 10226359 -6728 -0.066%
vet 7117994 7121066 +3072 +0.043%
total 111026988 111039495 +12507 +0.011%
On linux arm64:
name old time/op new time/op delta
Template 284ms ± 1% 286ms ± 1% +0.70% (p=0.000 n=49+50)
Unicode 125ms ± 3% 125ms ± 2% ~ (p=0.548 n=50+50)
GoTypes 1.69s ± 1% 1.71s ± 1% +1.02% (p=0.000 n=49+49)
Compiler 125ms ± 1% 124ms ± 2% -0.35% (p=0.020 n=50+50)
SSA 12.7s ± 1% 12.8s ± 1% +1.21% (p=0.000 n=49+49)
Flate 172ms ± 1% 173ms ± 1% +0.20% (p=0.047 n=50+50)
GoParser 265ms ± 1% 266ms ± 1% +0.64% (p=0.000 n=50+50)
Reflect 651ms ± 1% 650ms ± 1% ~ (p=0.079 n=48+48)
Tar 246ms ± 1% 246ms ± 1% ~ (p=0.202 n=50+46)
XML 328ms ± 1% 332ms ± 1% +1.28% (p=0.000 n=50+49)
LinkCompiler 600ms ± 1% 599ms ± 1% ~ (p=0.264 n=50+50)
ExternalLinkCompiler 1.88s ± 1% 1.90s ± 0% +1.36% (p=0.000 n=50+50)
LinkWithoutDebugCompiler 365ms ± 1% 365ms ± 1% ~ (p=0.602 n=50+46)
[Geo mean] 490ms 492ms +0.47%
name old alloc/op new alloc/op delta
Template 38.8MB ± 1% 39.1MB ± 1% +0.92% (p=0.000 n=44+42)
Unicode 28.4MB ± 0% 28.4MB ± 0% +0.22% (p=0.000 n=44+45)
GoTypes 169MB ± 1% 171MB ± 1% +1.12% (p=0.000 n=50+50)
Compiler 23.2MB ± 1% 23.3MB ± 1% +0.56% (p=0.000 n=42+43)
SSA 1.55GB ± 0% 1.56GB ± 0% +0.91% (p=0.000 n=48+49)
Flate 23.7MB ± 2% 24.0MB ± 1% +1.20% (p=0.000 n=50+50)
GoParser 35.3MB ± 1% 35.6MB ± 1% +0.88% (p=0.000 n=50+50)
Reflect 85.0MB ± 0% 86.5MB ± 0% +1.70% (p=0.000 n=49+48)
Tar 34.5MB ± 1% 34.9MB ± 1% +1.03% (p=0.000 n=47+50)
XML 43.8MB ± 2% 44.0MB ± 0% +0.41% (p=0.002 n=49+38)
LinkCompiler 136MB ± 0% 136MB ± 0% +0.01% (p=0.006 n=50+49)
ExternalLinkCompiler 127MB ± 0% 127MB ± 0% +0.02% (p=0.000 n=49+50)
LinkWithoutDebugCompiler 84.1MB ± 0% 84.1MB ± 0% ~ (p=0.534 n=50+50)
[Geo mean] 70.4MB 70.9MB +0.69%
file before after Δ %
addr2line 4006004 4004556 -1448 -0.036%
api 5029716 5028828 -888 -0.018%
asm 4936863 4934943 -1920 -0.039%
buildid 2594947 2594099 -848 -0.033%
cgo 4399702 4399502 -200 -0.005%
compile 22233139 22230486 -2653 -0.012%
cover 4443681 4443881 +200 +0.005%
dist 3365902 3365486 -416 -0.012%
doc 3776175 3776151 -24 -0.001%
fix 3218624 3218600 -24 -0.001%
link 6365001 6361409 -3592 -0.056%
nm 3923345 3923065 -280 -0.007%
objdump 4295473 4296673 +1200 +0.028%
pack 2390561 2389393 -1168 -0.049%
pprof 12866419 12865115 -1304 -0.010%
test2json 2587113 2585561 -1552 -0.060%
trace 9609814 9610846 +1032 +0.011%
vet 6790272 6789760 -512 -0.008%
total 106832751 106818354 -14397 -0.013%
Update: #37608
Change-Id: I2831238b12e3af5aef2261f64f804bf0a8b43f86
Reviewed-on: https://go-review.googlesource.com/c/go/+/244737
Reviewed-by: eric fang <eric.fang@arm.com>
Reviewed-by: Keith Randall <khr@golang.org>
Trust: eric fang <eric.fang@arm.com>
Run-TryBot: eric fang <eric.fang@arm.com>
TryBot-Result: Go Bot <gobot@golang.org>
2020-07-09 11:27:03 +00:00
|
|
|
}
|
|
|
|
|
|
2016-11-27 11:43:08 -08:00
|
|
|
// prove removes redundant BlockIf branches that can be inferred
|
|
|
|
|
// from previous dominating comparisons.
|
2016-02-19 12:14:42 +01:00
|
|
|
//
|
2016-03-07 18:36:16 +01:00
|
|
|
// By far, the most common redundant pair are generated by bounds checking.
|
2016-02-19 12:14:42 +01:00
|
|
|
// For example for the code:
|
|
|
|
|
//
|
2022-02-03 14:12:08 -05:00
|
|
|
// a[i] = 4
|
|
|
|
|
// foo(a[i])
|
2016-02-19 12:14:42 +01:00
|
|
|
//
|
|
|
|
|
// The compiler will generate the following code:
|
|
|
|
|
//
|
2022-02-03 14:12:08 -05:00
|
|
|
// if i >= len(a) {
|
|
|
|
|
// panic("not in bounds")
|
|
|
|
|
// }
|
|
|
|
|
// a[i] = 4
|
|
|
|
|
// if i >= len(a) {
|
|
|
|
|
// panic("not in bounds")
|
|
|
|
|
// }
|
|
|
|
|
// foo(a[i])
|
2016-02-19 12:14:42 +01:00
|
|
|
//
|
|
|
|
|
// The second comparison i >= len(a) is clearly redundant because if the
|
|
|
|
|
// else branch of the first comparison is executed, we already know that i < len(a).
|
|
|
|
|
// The code for the second panic can be removed.
|
cmd/compile: make prove pass use unsatisfiability
Currently the prove pass uses implication queries. For each block, it
collects the set of branch conditions leading to that block, and
queries this fact table for whether any of these facts imply the
block's own branch condition (or its inverse). This works remarkably
well considering it doesn't do any deduction on these facts, but it
has various downsides:
1. It requires an implementation both of adding facts to the table and
determining implications. These are very nearly duals of each
other, but require separate implementations. Likewise, the process
of asserting facts of dominating branch conditions is very nearly
the dual of the process of querying implied branch conditions.
2. It leads to less effective use of derived facts. For example, the
prove pass currently derives facts about the relations between len
and cap, but can't make use of these unless a branch condition is
in the exact form of a derived fact. If one of these derived facts
contradicts another fact, it won't notice or make use of this.
This CL changes the approach of the prove pass to instead use
*contradiction* instead of implication. Rather than ever querying a
branch condition, it simply adds branch conditions to the fact table.
If this leads to a contradiction (specifically, it makes the fact set
unsatisfiable), that branch is impossible and can be cut. As a result,
1. We can eliminate the code for determining implications
(factsTable.get disappears entirely). Also, there is now a single
implementation of visiting and asserting branch conditions, since
we don't have to flip them around to treat them as facts in one
place and queries in another.
2. Derived facts can be used effectively. It doesn't matter *why* the
fact table is unsatisfiable; a contradiction in any of the facts is
enough.
3. As an added benefit, it's now quite easy to avoid traversing beyond
provably-unreachable blocks. In contrast, the current
implementation always visits all blocks.
The prove pass already has nearly all of the mechanism necessary to
compute unsatisfiability, which means this both simplifies the code
and makes it more powerful.
The only complication is that the current implication procedure has a
hack for dealing with the 0 <= Args[0] condition of OpIsInBounds and
OpIsSliceInBounds. We replace this with asserting the appropriate fact
when we process one of these conditions. This seems much cleaner
anyway, and works because we can now take advantage of derived facts.
This has no measurable effect on compiler performance.
Effectiveness:
There is exactly one condition in all of std and cmd that this fails
to prove that the old implementation could: (int64(^uint(0)>>1) < x)
in encoding/gob. This can never be true because x is an int, and it's
basically coincidence that the old code gets this. (For example, it
fails to prove the similar (x < ^int64(^uint(0)>>1)) condition that
immediately precedes it, and even though the conditions are logically
unrelated, it wouldn't get the second one if it hadn't first processed
the first!)
It does, however, prove a few dozen additional branches. These come
from facts that are added to the fact table about the relations
between len and cap. These were almost never queried directly before,
but could lead to contradictions, which the unsat-based approach is
able to use.
There are exactly two branches in std and cmd that this implementation
proves in the *other* direction. This sounds scary, but is okay
because both occur in already-unreachable blocks, so it doesn't matter
what we chose. Because the fact table logic is sound but incomplete,
it fails to prove that the block isn't reachable, even though it is
able to prove that both outgoing branches are impossible. We could
turn these blocks into BlockExit blocks, but it doesn't seem worth the
trouble of the extra proof effort for something that happens twice in
all of std and cmd.
Tests:
This CL updates test/prove.go to change the expected messages because
it can no longer give a "reason" why it proved or disproved a
condition. It also adds a new test of a branch it couldn't prove
before.
It mostly guts test/sliceopt.go, removing everything related to slice
bounds optimizations and moving a few relevant tests to test/prove.go.
Much of this test is actually unreachable. The new prove pass figures
this out and doesn't try to prove anything about the unreachable
parts. The output on the unreachable parts is already suspect because
anything can be proved at that point, so it's really just a regression
test for an algorithm the compiler no longer uses.
This is a step toward fixing #23354. That issue is quite easy to fix
once we can use derived facts effectively.
Change-Id: Ia48a1b9ee081310579fe474e4a61857424ff8ce8
Reviewed-on: https://go-review.googlesource.com/87478
Reviewed-by: Keith Randall <khr@golang.org>
2018-01-10 16:28:58 -05:00
|
|
|
//
|
|
|
|
|
// prove works by finding contradictions and trimming branches whose
|
|
|
|
|
// conditions are unsatisfiable given the branches leading up to them.
|
|
|
|
|
// It tracks a "fact table" of branch conditions. For each branching
|
|
|
|
|
// block, it asserts the branch conditions that uniquely dominate that
|
|
|
|
|
// block, and then separately asserts the block's branch condition and
|
|
|
|
|
// its negation. If either leads to a contradiction, it can trim that
|
|
|
|
|
// successor.
|
2016-02-19 12:14:42 +01:00
|
|
|
func prove(f *Func) {
|
2023-07-25 16:19:10 +02:00
|
|
|
// Find induction variables. Currently, findIndVars
|
|
|
|
|
// is limited to one induction variable per block.
|
|
|
|
|
var indVars map[*Block]indVar
|
|
|
|
|
for _, v := range findIndVar(f) {
|
|
|
|
|
ind := v.ind
|
|
|
|
|
if len(ind.Args) != 2 {
|
|
|
|
|
// the rewrite code assumes there is only ever two parents to loops
|
|
|
|
|
panic("unexpected induction with too many parents")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
nxt := v.nxt
|
|
|
|
|
if !(ind.Uses == 2 && // 2 used by comparison and next
|
|
|
|
|
nxt.Uses == 1) { // 1 used by induction
|
|
|
|
|
// ind or nxt is used inside the loop, add it for the facts table
|
|
|
|
|
if indVars == nil {
|
|
|
|
|
indVars = make(map[*Block]indVar)
|
|
|
|
|
}
|
|
|
|
|
indVars[v.entry] = v
|
|
|
|
|
continue
|
|
|
|
|
} else {
|
|
|
|
|
// Since this induction variable is not used for anything but counting the iterations,
|
|
|
|
|
// no point in putting it into the facts table.
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// try to rewrite to a downward counting loop checking against start if the
|
|
|
|
|
// loop body does not depends on ind or nxt and end is known before the loop.
|
|
|
|
|
// This reduce pressure on the register allocator because this do not need
|
|
|
|
|
// to use end on each iteration anymore. We compare against the start constant instead.
|
|
|
|
|
// That means this code:
|
|
|
|
|
//
|
|
|
|
|
// loop:
|
|
|
|
|
// ind = (Phi (Const [x]) nxt),
|
|
|
|
|
// if ind < end
|
|
|
|
|
// then goto enter_loop
|
|
|
|
|
// else goto exit_loop
|
|
|
|
|
//
|
|
|
|
|
// enter_loop:
|
|
|
|
|
// do something without using ind nor nxt
|
|
|
|
|
// nxt = inc + ind
|
|
|
|
|
// goto loop
|
|
|
|
|
//
|
|
|
|
|
// exit_loop:
|
|
|
|
|
//
|
|
|
|
|
// is rewritten to:
|
|
|
|
|
//
|
|
|
|
|
// loop:
|
|
|
|
|
// ind = (Phi end nxt)
|
|
|
|
|
// if (Const [x]) < ind
|
|
|
|
|
// then goto enter_loop
|
|
|
|
|
// else goto exit_loop
|
|
|
|
|
//
|
|
|
|
|
// enter_loop:
|
|
|
|
|
// do something without using ind nor nxt
|
|
|
|
|
// nxt = ind - inc
|
|
|
|
|
// goto loop
|
|
|
|
|
//
|
|
|
|
|
// exit_loop:
|
|
|
|
|
//
|
|
|
|
|
// this is better because it only require to keep ind then nxt alive while looping,
|
|
|
|
|
// while the original form keeps ind then nxt and end alive
|
|
|
|
|
start, end := v.min, v.max
|
|
|
|
|
if v.flags&indVarCountDown != 0 {
|
|
|
|
|
start, end = end, start
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-14 21:29:50 +01:00
|
|
|
if !start.isGenericIntConst() {
|
2023-07-25 16:19:10 +02:00
|
|
|
// if start is not a constant we would be winning nothing from inverting the loop
|
|
|
|
|
continue
|
|
|
|
|
}
|
2025-01-14 21:29:50 +01:00
|
|
|
if end.isGenericIntConst() {
|
2023-07-25 16:19:10 +02:00
|
|
|
// TODO: if both start and end are constants we should rewrite such that the comparison
|
|
|
|
|
// is against zero and nxt is ++ or -- operation
|
|
|
|
|
// That means:
|
|
|
|
|
// for i := 2; i < 11; i += 2 {
|
|
|
|
|
// should be rewritten to:
|
|
|
|
|
// for i := 5; 0 < i; i-- {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if end.Block == ind.Block {
|
|
|
|
|
// we can't rewrite loops where the condition depends on the loop body
|
|
|
|
|
// this simple check is forced to work because if this is true a Phi in ind.Block must exists
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-03 09:06:30 +01:00
|
|
|
check := ind.Block.Controls[0]
|
2023-07-25 16:19:10 +02:00
|
|
|
// invert the check
|
|
|
|
|
check.Args[0], check.Args[1] = check.Args[1], check.Args[0]
|
|
|
|
|
|
2024-02-03 09:06:30 +01:00
|
|
|
// swap start and end in the loop
|
2023-07-25 16:19:10 +02:00
|
|
|
for i, v := range check.Args {
|
|
|
|
|
if v != end {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
check.SetArg(i, start)
|
|
|
|
|
goto replacedEnd
|
|
|
|
|
}
|
|
|
|
|
panic(fmt.Sprintf("unreachable, ind: %v, start: %v, end: %v", ind, start, end))
|
|
|
|
|
replacedEnd:
|
|
|
|
|
|
|
|
|
|
for i, v := range ind.Args {
|
|
|
|
|
if v != start {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ind.SetArg(i, end)
|
|
|
|
|
goto replacedStart
|
|
|
|
|
}
|
|
|
|
|
panic(fmt.Sprintf("unreachable, ind: %v, start: %v, end: %v", ind, start, end))
|
|
|
|
|
replacedStart:
|
|
|
|
|
|
|
|
|
|
if nxt.Args[0] != ind {
|
|
|
|
|
// unlike additions subtractions are not commutative so be sure we get it right
|
|
|
|
|
nxt.Args[0], nxt.Args[1] = nxt.Args[1], nxt.Args[0]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch nxt.Op {
|
|
|
|
|
case OpAdd8:
|
|
|
|
|
nxt.Op = OpSub8
|
|
|
|
|
case OpAdd16:
|
|
|
|
|
nxt.Op = OpSub16
|
|
|
|
|
case OpAdd32:
|
|
|
|
|
nxt.Op = OpSub32
|
|
|
|
|
case OpAdd64:
|
|
|
|
|
nxt.Op = OpSub64
|
|
|
|
|
case OpSub8:
|
|
|
|
|
nxt.Op = OpAdd8
|
|
|
|
|
case OpSub16:
|
|
|
|
|
nxt.Op = OpAdd16
|
|
|
|
|
case OpSub32:
|
|
|
|
|
nxt.Op = OpAdd32
|
|
|
|
|
case OpSub64:
|
|
|
|
|
nxt.Op = OpAdd64
|
|
|
|
|
default:
|
|
|
|
|
panic("unreachable")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if f.pass.debug > 0 {
|
|
|
|
|
f.Warnl(ind.Pos, "Inverted loop iteration")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-05-01 00:57:57 +02:00
|
|
|
ft := newFactsTable(f)
|
|
|
|
|
ft.checkpoint()
|
2016-11-27 11:43:08 -08:00
|
|
|
|
|
|
|
|
// Find length and capacity ops.
|
|
|
|
|
for _, b := range f.Blocks {
|
|
|
|
|
for _, v := range b.Values {
|
|
|
|
|
if v.Uses == 0 {
|
|
|
|
|
// We don't care about dead values.
|
|
|
|
|
// (There can be some that are CSEd but not removed yet.)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
switch v.Op {
|
|
|
|
|
case OpSliceLen:
|
|
|
|
|
if ft.lens == nil {
|
|
|
|
|
ft.lens = map[ID]*Value{}
|
|
|
|
|
}
|
2020-11-28 02:46:00 +07:00
|
|
|
// Set all len Values for the same slice as equal in the poset.
|
|
|
|
|
// The poset handles transitive relations, so Values related to
|
|
|
|
|
// any OpSliceLen for this slice will be correctly related to others.
|
|
|
|
|
if l, ok := ft.lens[v.Args[0].ID]; ok {
|
|
|
|
|
ft.update(b, v, l, signed, eq)
|
|
|
|
|
} else {
|
|
|
|
|
ft.lens[v.Args[0].ID] = v
|
|
|
|
|
}
|
2016-11-27 11:43:08 -08:00
|
|
|
case OpSliceCap:
|
|
|
|
|
if ft.caps == nil {
|
|
|
|
|
ft.caps = map[ID]*Value{}
|
|
|
|
|
}
|
2020-11-28 02:46:00 +07:00
|
|
|
// Same as case OpSliceLen above, but for slice cap.
|
|
|
|
|
if c, ok := ft.caps[v.Args[0].ID]; ok {
|
|
|
|
|
ft.update(b, v, c, signed, eq)
|
|
|
|
|
} else {
|
|
|
|
|
ft.caps[v.Args[0].ID] = v
|
|
|
|
|
}
|
2016-11-27 11:43:08 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-04-02 01:57:49 +02:00
|
|
|
|
2016-02-19 12:14:42 +01:00
|
|
|
// current node state
|
|
|
|
|
type walkState int
|
|
|
|
|
const (
|
|
|
|
|
descend walkState = iota
|
|
|
|
|
simplify
|
|
|
|
|
)
|
|
|
|
|
// work maintains the DFS stack.
|
|
|
|
|
type bp struct {
|
2016-03-07 18:36:16 +01:00
|
|
|
block *Block // current handled block
|
|
|
|
|
state walkState // what's to do
|
2016-02-19 12:14:42 +01:00
|
|
|
}
|
|
|
|
|
work := make([]bp, 0, 256)
|
|
|
|
|
work = append(work, bp{
|
|
|
|
|
block: f.Entry,
|
|
|
|
|
state: descend,
|
|
|
|
|
})
|
|
|
|
|
|
2016-10-09 01:09:52 +09:00
|
|
|
idom := f.Idom()
|
2019-11-01 14:04:08 -07:00
|
|
|
sdom := f.Sdom()
|
2016-02-19 12:14:42 +01:00
|
|
|
|
|
|
|
|
// DFS on the dominator tree.
|
cmd/compile: make prove pass use unsatisfiability
Currently the prove pass uses implication queries. For each block, it
collects the set of branch conditions leading to that block, and
queries this fact table for whether any of these facts imply the
block's own branch condition (or its inverse). This works remarkably
well considering it doesn't do any deduction on these facts, but it
has various downsides:
1. It requires an implementation both of adding facts to the table and
determining implications. These are very nearly duals of each
other, but require separate implementations. Likewise, the process
of asserting facts of dominating branch conditions is very nearly
the dual of the process of querying implied branch conditions.
2. It leads to less effective use of derived facts. For example, the
prove pass currently derives facts about the relations between len
and cap, but can't make use of these unless a branch condition is
in the exact form of a derived fact. If one of these derived facts
contradicts another fact, it won't notice or make use of this.
This CL changes the approach of the prove pass to instead use
*contradiction* instead of implication. Rather than ever querying a
branch condition, it simply adds branch conditions to the fact table.
If this leads to a contradiction (specifically, it makes the fact set
unsatisfiable), that branch is impossible and can be cut. As a result,
1. We can eliminate the code for determining implications
(factsTable.get disappears entirely). Also, there is now a single
implementation of visiting and asserting branch conditions, since
we don't have to flip them around to treat them as facts in one
place and queries in another.
2. Derived facts can be used effectively. It doesn't matter *why* the
fact table is unsatisfiable; a contradiction in any of the facts is
enough.
3. As an added benefit, it's now quite easy to avoid traversing beyond
provably-unreachable blocks. In contrast, the current
implementation always visits all blocks.
The prove pass already has nearly all of the mechanism necessary to
compute unsatisfiability, which means this both simplifies the code
and makes it more powerful.
The only complication is that the current implication procedure has a
hack for dealing with the 0 <= Args[0] condition of OpIsInBounds and
OpIsSliceInBounds. We replace this with asserting the appropriate fact
when we process one of these conditions. This seems much cleaner
anyway, and works because we can now take advantage of derived facts.
This has no measurable effect on compiler performance.
Effectiveness:
There is exactly one condition in all of std and cmd that this fails
to prove that the old implementation could: (int64(^uint(0)>>1) < x)
in encoding/gob. This can never be true because x is an int, and it's
basically coincidence that the old code gets this. (For example, it
fails to prove the similar (x < ^int64(^uint(0)>>1)) condition that
immediately precedes it, and even though the conditions are logically
unrelated, it wouldn't get the second one if it hadn't first processed
the first!)
It does, however, prove a few dozen additional branches. These come
from facts that are added to the fact table about the relations
between len and cap. These were almost never queried directly before,
but could lead to contradictions, which the unsat-based approach is
able to use.
There are exactly two branches in std and cmd that this implementation
proves in the *other* direction. This sounds scary, but is okay
because both occur in already-unreachable blocks, so it doesn't matter
what we chose. Because the fact table logic is sound but incomplete,
it fails to prove that the block isn't reachable, even though it is
able to prove that both outgoing branches are impossible. We could
turn these blocks into BlockExit blocks, but it doesn't seem worth the
trouble of the extra proof effort for something that happens twice in
all of std and cmd.
Tests:
This CL updates test/prove.go to change the expected messages because
it can no longer give a "reason" why it proved or disproved a
condition. It also adds a new test of a branch it couldn't prove
before.
It mostly guts test/sliceopt.go, removing everything related to slice
bounds optimizations and moving a few relevant tests to test/prove.go.
Much of this test is actually unreachable. The new prove pass figures
this out and doesn't try to prove anything about the unreachable
parts. The output on the unreachable parts is already suspect because
anything can be proved at that point, so it's really just a regression
test for an algorithm the compiler no longer uses.
This is a step toward fixing #23354. That issue is quite easy to fix
once we can use derived facts effectively.
Change-Id: Ia48a1b9ee081310579fe474e4a61857424ff8ce8
Reviewed-on: https://go-review.googlesource.com/87478
Reviewed-by: Keith Randall <khr@golang.org>
2018-01-10 16:28:58 -05:00
|
|
|
//
|
|
|
|
|
// For efficiency, we consider only the dominator tree rather
|
|
|
|
|
// than the entire flow graph. On the way down, we consider
|
|
|
|
|
// incoming branches and accumulate conditions that uniquely
|
|
|
|
|
// dominate the current block. If we discover a contradiction,
|
|
|
|
|
// we can eliminate the entire block and all of its children.
|
|
|
|
|
// On the way back up, we consider outgoing branches that
|
|
|
|
|
// haven't already been considered. This way we consider each
|
|
|
|
|
// branch condition only once.
|
2016-02-19 12:14:42 +01:00
|
|
|
for len(work) > 0 {
|
|
|
|
|
node := work[len(work)-1]
|
|
|
|
|
work = work[:len(work)-1]
|
2016-09-16 13:50:18 -07:00
|
|
|
parent := idom[node.block.ID]
|
|
|
|
|
branch := getBranch(sdom, parent, node.block)
|
2016-02-19 12:14:42 +01:00
|
|
|
|
|
|
|
|
switch node.state {
|
|
|
|
|
case descend:
|
2018-04-02 01:39:03 +02:00
|
|
|
ft.checkpoint()
|
2019-09-22 15:39:16 +02:00
|
|
|
|
2024-06-14 22:29:09 -07:00
|
|
|
// Entering the block, add facts about the induction variable
|
|
|
|
|
// that is bound to this block.
|
2018-04-02 01:57:49 +02:00
|
|
|
if iv, ok := indVars[node.block]; ok {
|
|
|
|
|
addIndVarRestrictions(ft, parent, iv)
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-14 22:29:09 -07:00
|
|
|
// Add results of reaching this block via a branch from
|
|
|
|
|
// its immediate dominator (if any).
|
2016-03-07 18:36:16 +01:00
|
|
|
if branch != unknown {
|
2018-04-02 01:39:03 +02:00
|
|
|
addBranchRestrictions(ft, parent, branch)
|
2016-03-07 18:36:16 +01:00
|
|
|
}
|
2016-02-19 12:14:42 +01:00
|
|
|
|
2024-06-14 22:29:09 -07:00
|
|
|
if ft.unsat {
|
|
|
|
|
// node.block is unreachable.
|
|
|
|
|
// Remove it and don't visit
|
|
|
|
|
// its children.
|
|
|
|
|
removeBranch(parent, branch)
|
|
|
|
|
ft.restore()
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
// Otherwise, we can now commit to
|
|
|
|
|
// taking this branch. We'll restore
|
|
|
|
|
// ft when we unwind.
|
cmd/compile: detect OFORUNTIL inductive facts in prove
Currently, we compile range loops into for loops with the obvious
initialization and update of the index variable. In this form, the
prove pass can see that the body is dominated by an i < len condition,
and findIndVar can detect that i is an induction variable and that
0 <= i < len.
GOEXPERIMENT=preemptibleloops compiles range loops to OFORUNTIL and
we're preparing to unconditionally switch to a variation of this for
#24543. OFORUNTIL moves the increment and condition *after* the body,
which makes the bounds on the index variable much less obvious. With
OFORUNTIL, proving anything about the index variable requires
understanding the phi that joins the index values at the top of the
loop body block.
This interferes with both prove's ability to see that i < len (this is
true on both paths that enter the body, but from two different
conditional checks) and with findIndVar's ability to detect the
induction pattern.
Fix this by teaching prove to detect that the index in the pattern
constructed by OFORUNTIL is an induction variable and add both bounds
to the facts table. Currently this is done separately from findIndVar
because it depends on prove's factsTable, while findIndVar runs before
visiting blocks and building the factsTable.
Without any GOEXPERIMENT, this has no effect on std or cmd. However,
with GOEXPERIMENT=preemptibleloops, this change becomes necessary to
prove 90 conditions in std and cmd.
Change-Id: Ic025d669f81b53426309da5a6e8010e5ccaf4f49
Reviewed-on: https://go-review.googlesource.com/102603
Run-TryBot: Austin Clements <austin@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Keith Randall <khr@golang.org>
2018-03-23 17:38:55 -04:00
|
|
|
|
2024-08-09 13:11:03 -07:00
|
|
|
// Add facts about the values in the current block.
|
|
|
|
|
addLocalFacts(ft, node.block)
|
|
|
|
|
|
2016-02-19 12:14:42 +01:00
|
|
|
work = append(work, bp{
|
|
|
|
|
block: node.block,
|
|
|
|
|
state: simplify,
|
|
|
|
|
})
|
2016-09-16 13:50:18 -07:00
|
|
|
for s := sdom.Child(node.block); s != nil; s = sdom.Sibling(s) {
|
2016-02-19 12:14:42 +01:00
|
|
|
work = append(work, bp{
|
|
|
|
|
block: s,
|
|
|
|
|
state: descend,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case simplify:
|
cmd/compile: make prove pass use unsatisfiability
Currently the prove pass uses implication queries. For each block, it
collects the set of branch conditions leading to that block, and
queries this fact table for whether any of these facts imply the
block's own branch condition (or its inverse). This works remarkably
well considering it doesn't do any deduction on these facts, but it
has various downsides:
1. It requires an implementation both of adding facts to the table and
determining implications. These are very nearly duals of each
other, but require separate implementations. Likewise, the process
of asserting facts of dominating branch conditions is very nearly
the dual of the process of querying implied branch conditions.
2. It leads to less effective use of derived facts. For example, the
prove pass currently derives facts about the relations between len
and cap, but can't make use of these unless a branch condition is
in the exact form of a derived fact. If one of these derived facts
contradicts another fact, it won't notice or make use of this.
This CL changes the approach of the prove pass to instead use
*contradiction* instead of implication. Rather than ever querying a
branch condition, it simply adds branch conditions to the fact table.
If this leads to a contradiction (specifically, it makes the fact set
unsatisfiable), that branch is impossible and can be cut. As a result,
1. We can eliminate the code for determining implications
(factsTable.get disappears entirely). Also, there is now a single
implementation of visiting and asserting branch conditions, since
we don't have to flip them around to treat them as facts in one
place and queries in another.
2. Derived facts can be used effectively. It doesn't matter *why* the
fact table is unsatisfiable; a contradiction in any of the facts is
enough.
3. As an added benefit, it's now quite easy to avoid traversing beyond
provably-unreachable blocks. In contrast, the current
implementation always visits all blocks.
The prove pass already has nearly all of the mechanism necessary to
compute unsatisfiability, which means this both simplifies the code
and makes it more powerful.
The only complication is that the current implication procedure has a
hack for dealing with the 0 <= Args[0] condition of OpIsInBounds and
OpIsSliceInBounds. We replace this with asserting the appropriate fact
when we process one of these conditions. This seems much cleaner
anyway, and works because we can now take advantage of derived facts.
This has no measurable effect on compiler performance.
Effectiveness:
There is exactly one condition in all of std and cmd that this fails
to prove that the old implementation could: (int64(^uint(0)>>1) < x)
in encoding/gob. This can never be true because x is an int, and it's
basically coincidence that the old code gets this. (For example, it
fails to prove the similar (x < ^int64(^uint(0)>>1)) condition that
immediately precedes it, and even though the conditions are logically
unrelated, it wouldn't get the second one if it hadn't first processed
the first!)
It does, however, prove a few dozen additional branches. These come
from facts that are added to the fact table about the relations
between len and cap. These were almost never queried directly before,
but could lead to contradictions, which the unsat-based approach is
able to use.
There are exactly two branches in std and cmd that this implementation
proves in the *other* direction. This sounds scary, but is okay
because both occur in already-unreachable blocks, so it doesn't matter
what we chose. Because the fact table logic is sound but incomplete,
it fails to prove that the block isn't reachable, even though it is
able to prove that both outgoing branches are impossible. We could
turn these blocks into BlockExit blocks, but it doesn't seem worth the
trouble of the extra proof effort for something that happens twice in
all of std and cmd.
Tests:
This CL updates test/prove.go to change the expected messages because
it can no longer give a "reason" why it proved or disproved a
condition. It also adds a new test of a branch it couldn't prove
before.
It mostly guts test/sliceopt.go, removing everything related to slice
bounds optimizations and moving a few relevant tests to test/prove.go.
Much of this test is actually unreachable. The new prove pass figures
this out and doesn't try to prove anything about the unreachable
parts. The output on the unreachable parts is already suspect because
anything can be proved at that point, so it's really just a regression
test for an algorithm the compiler no longer uses.
This is a step toward fixing #23354. That issue is quite easy to fix
once we can use derived facts effectively.
Change-Id: Ia48a1b9ee081310579fe474e4a61857424ff8ce8
Reviewed-on: https://go-review.googlesource.com/87478
Reviewed-by: Keith Randall <khr@golang.org>
2018-01-10 16:28:58 -05:00
|
|
|
simplifyBlock(sdom, ft, node.block)
|
2018-04-02 01:39:03 +02:00
|
|
|
ft.restore()
|
2016-02-19 12:14:42 +01:00
|
|
|
}
|
|
|
|
|
}
|
2018-05-01 00:57:57 +02:00
|
|
|
|
|
|
|
|
ft.restore()
|
|
|
|
|
|
cmd/compile/ssa: optimize the derivable known branch of If block
When the control value of a If block is known for a particular inbound edge
because its value can be inferred from the control value of its predecessor,
then this inbound edge can be redirected to the known successor directly,
This CL optimizes this kind of cases to eliminate unnecessary comparision.
For example, the following piece of code comes from runtime.atoi,
if !neg && un > uint(maxInt) {
return 0, false
}
if neg && un > uint(maxInt)+1 {
return 0, false
}
Before this optimization, if the first "if" statement does not return, both
conditions of the second "if" statement will be checked. But obviously the
value of neg is known through the first "if" statement, and there is no need
to check neg repeatedly.
After this optimization, this redundancy check is eliminated, and the execution
logic becomes as follows.
if !neg {
if un > uint(maxInt) {
return 0, false
}
} else {
if un > uint(maxInt)+1 {
return 0, false
}
}
This CL does not bring significant performance changes, but it makes the code
structure look more reasonable.
Statistical data from tool compilecmp on Linux/amd64:
name old time/op new time/op delta
Template 380ms ± 4% 385ms ± 3% +1.16% (p=0.000 n=50+49)
Unicode 168ms ± 9% 169ms ± 9% ~ (p=0.421 n=49+46)
GoTypes 1.99s ± 4% 2.02s ± 4% +1.48% (p=0.000 n=49+49)
Compiler 188ms ± 8% 188ms ± 9% ~ (p=0.997 n=49+50)
SSA 11.8s ± 2% 12.0s ± 2% +1.24% (p=0.000 n=48+50)
Flate 242ms ± 6% 244ms ± 9% ~ (p=0.307 n=46+49)
GoParser 361ms ± 3% 366ms ± 4% +1.23% (p=0.000 n=48+49)
Reflect 836ms ± 3% 842ms ± 3% +0.70% (p=0.004 n=48+48)
Tar 335ms ± 3% 340ms ± 4% +1.47% (p=0.000 n=49+46)
XML 432ms ± 4% 437ms ± 4% +1.11% (p=0.002 n=49+49)
LinkCompiler 701ms ± 4% 704ms ± 5% ~ (p=0.278 n=49+50)
ExternalLinkCompiler 1.83s ± 3% 1.84s ± 3% +0.51% (p=0.034 n=48+49)
LinkWithoutDebugCompiler 436ms ± 6% 438ms ± 6% ~ (p=0.419 n=48+49)
[Geo mean] 612ms 617ms +0.84%
name old alloc/op new alloc/op delta
Template 38.7MB ± 1% 39.1MB ± 1% +1.19% (p=0.000 n=50+50)
Unicode 28.1MB ± 0% 28.1MB ± 0% +0.20% (p=0.000 n=49+45)
GoTypes 168MB ± 1% 170MB ± 1% +1.05% (p=0.000 n=48+49)
Compiler 23.0MB ± 1% 23.1MB ± 1% +0.63% (p=0.000 n=50+50)
SSA 1.54GB ± 1% 1.55GB ± 1% +0.85% (p=0.000 n=50+50)
Flate 23.6MB ± 1% 23.9MB ± 1% +1.36% (p=0.000 n=43+46)
GoParser 35.0MB ± 1% 35.3MB ± 1% +0.94% (p=0.000 n=50+50)
Reflect 84.7MB ± 1% 86.1MB ± 1% +1.72% (p=0.000 n=49+49)
Tar 34.5MB ± 1% 34.9MB ± 1% +1.07% (p=0.000 n=47+48)
XML 44.2MB ± 3% 44.6MB ± 3% +0.70% (p=0.003 n=50+49)
LinkCompiler 128MB ± 0% 128MB ± 0% +0.01% (p=0.004 n=49+50)
ExternalLinkCompiler 120MB ± 0% 120MB ± 0% +0.01% (p=0.000 n=49+50)
LinkWithoutDebugCompiler 77.3MB ± 0% 77.3MB ± 0% +0.02% (p=0.000 n=50+50)
[Geo mean] 69.1MB 69.6MB +0.75%
file before after Δ %
addr2line 4049276 4051308 +2032 +0.050%
api 5248940 5248996 +56 +0.001%
asm 4868093 4868037 -56 -0.001%
buildid 2627666 2626026 -1640 -0.062%
cgo 4614432 4615040 +608 +0.013%
compile 23298888 23301267 +2379 +0.010%
cover 4591609 4591161 -448 -0.010%
dist 3449638 3450254 +616 +0.018%
doc 3925667 3926363 +696 +0.018%
fix 3322936 3323464 +528 +0.016%
link 6628632 6629560 +928 +0.014%
nm 3991753 3996497 +4744 +0.119%
objdump 4396119 4395615 -504 -0.011%
pack 2399719 2399535 -184 -0.008%
pprof 13616418 13622866 +6448 +0.047%
test2json 2646121 2646081 -40 -0.002%
trace 10233087 10226359 -6728 -0.066%
vet 7117994 7121066 +3072 +0.043%
total 111026988 111039495 +12507 +0.011%
On linux arm64:
name old time/op new time/op delta
Template 284ms ± 1% 286ms ± 1% +0.70% (p=0.000 n=49+50)
Unicode 125ms ± 3% 125ms ± 2% ~ (p=0.548 n=50+50)
GoTypes 1.69s ± 1% 1.71s ± 1% +1.02% (p=0.000 n=49+49)
Compiler 125ms ± 1% 124ms ± 2% -0.35% (p=0.020 n=50+50)
SSA 12.7s ± 1% 12.8s ± 1% +1.21% (p=0.000 n=49+49)
Flate 172ms ± 1% 173ms ± 1% +0.20% (p=0.047 n=50+50)
GoParser 265ms ± 1% 266ms ± 1% +0.64% (p=0.000 n=50+50)
Reflect 651ms ± 1% 650ms ± 1% ~ (p=0.079 n=48+48)
Tar 246ms ± 1% 246ms ± 1% ~ (p=0.202 n=50+46)
XML 328ms ± 1% 332ms ± 1% +1.28% (p=0.000 n=50+49)
LinkCompiler 600ms ± 1% 599ms ± 1% ~ (p=0.264 n=50+50)
ExternalLinkCompiler 1.88s ± 1% 1.90s ± 0% +1.36% (p=0.000 n=50+50)
LinkWithoutDebugCompiler 365ms ± 1% 365ms ± 1% ~ (p=0.602 n=50+46)
[Geo mean] 490ms 492ms +0.47%
name old alloc/op new alloc/op delta
Template 38.8MB ± 1% 39.1MB ± 1% +0.92% (p=0.000 n=44+42)
Unicode 28.4MB ± 0% 28.4MB ± 0% +0.22% (p=0.000 n=44+45)
GoTypes 169MB ± 1% 171MB ± 1% +1.12% (p=0.000 n=50+50)
Compiler 23.2MB ± 1% 23.3MB ± 1% +0.56% (p=0.000 n=42+43)
SSA 1.55GB ± 0% 1.56GB ± 0% +0.91% (p=0.000 n=48+49)
Flate 23.7MB ± 2% 24.0MB ± 1% +1.20% (p=0.000 n=50+50)
GoParser 35.3MB ± 1% 35.6MB ± 1% +0.88% (p=0.000 n=50+50)
Reflect 85.0MB ± 0% 86.5MB ± 0% +1.70% (p=0.000 n=49+48)
Tar 34.5MB ± 1% 34.9MB ± 1% +1.03% (p=0.000 n=47+50)
XML 43.8MB ± 2% 44.0MB ± 0% +0.41% (p=0.002 n=49+38)
LinkCompiler 136MB ± 0% 136MB ± 0% +0.01% (p=0.006 n=50+49)
ExternalLinkCompiler 127MB ± 0% 127MB ± 0% +0.02% (p=0.000 n=49+50)
LinkWithoutDebugCompiler 84.1MB ± 0% 84.1MB ± 0% ~ (p=0.534 n=50+50)
[Geo mean] 70.4MB 70.9MB +0.69%
file before after Δ %
addr2line 4006004 4004556 -1448 -0.036%
api 5029716 5028828 -888 -0.018%
asm 4936863 4934943 -1920 -0.039%
buildid 2594947 2594099 -848 -0.033%
cgo 4399702 4399502 -200 -0.005%
compile 22233139 22230486 -2653 -0.012%
cover 4443681 4443881 +200 +0.005%
dist 3365902 3365486 -416 -0.012%
doc 3776175 3776151 -24 -0.001%
fix 3218624 3218600 -24 -0.001%
link 6365001 6361409 -3592 -0.056%
nm 3923345 3923065 -280 -0.007%
objdump 4295473 4296673 +1200 +0.028%
pack 2390561 2389393 -1168 -0.049%
pprof 12866419 12865115 -1304 -0.010%
test2json 2587113 2585561 -1552 -0.060%
trace 9609814 9610846 +1032 +0.011%
vet 6790272 6789760 -512 -0.008%
total 106832751 106818354 -14397 -0.013%
Update: #37608
Change-Id: I2831238b12e3af5aef2261f64f804bf0a8b43f86
Reviewed-on: https://go-review.googlesource.com/c/go/+/244737
Reviewed-by: eric fang <eric.fang@arm.com>
Reviewed-by: Keith Randall <khr@golang.org>
Trust: eric fang <eric.fang@arm.com>
Run-TryBot: eric fang <eric.fang@arm.com>
TryBot-Result: Go Bot <gobot@golang.org>
2020-07-09 11:27:03 +00:00
|
|
|
ft.cleanup(f)
|
2016-02-19 12:14:42 +01:00
|
|
|
}
|
|
|
|
|
|
2024-06-14 22:29:09 -07:00
|
|
|
// initLimit sets initial constant limit for v. This limit is based
|
|
|
|
|
// only on the operation itself, not any of its input arguments. This
|
2024-11-04 07:43:43 +08:00
|
|
|
// method is only used in two places, once when the prove pass startup
|
|
|
|
|
// and the other when a new ssa value is created, both for init. (unlike
|
2024-06-14 22:29:09 -07:00
|
|
|
// flowLimit, below, which computes additional constraints based on
|
|
|
|
|
// ranges of opcode arguments).
|
|
|
|
|
func initLimit(v *Value) limit {
|
2024-07-07 14:58:47 -07:00
|
|
|
if v.Type.IsBoolean() {
|
|
|
|
|
switch v.Op {
|
|
|
|
|
case OpConstBool:
|
|
|
|
|
b := v.AuxInt
|
|
|
|
|
return limit{min: b, max: b, umin: uint64(b), umax: uint64(b)}
|
|
|
|
|
default:
|
|
|
|
|
return limit{min: 0, max: 1, umin: 0, umax: 1}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if v.Type.IsPtrShaped() { // These are the types that EqPtr/NeqPtr operate on, except uintptr.
|
|
|
|
|
switch v.Op {
|
|
|
|
|
case OpConstNil:
|
|
|
|
|
return limit{min: 0, max: 0, umin: 0, umax: 0}
|
|
|
|
|
case OpAddr, OpLocalAddr: // TODO: others?
|
|
|
|
|
l := noLimit
|
|
|
|
|
l.umin = 1
|
|
|
|
|
return l
|
|
|
|
|
default:
|
|
|
|
|
return noLimit
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-06-14 22:29:09 -07:00
|
|
|
if !v.Type.IsInteger() {
|
|
|
|
|
return noLimit
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Default limits based on type.
|
|
|
|
|
var lim limit
|
|
|
|
|
switch v.Type.Size() {
|
|
|
|
|
case 8:
|
|
|
|
|
lim = limit{min: math.MinInt64, max: math.MaxInt64, umin: 0, umax: math.MaxUint64}
|
|
|
|
|
case 4:
|
|
|
|
|
lim = limit{min: math.MinInt32, max: math.MaxInt32, umin: 0, umax: math.MaxUint32}
|
|
|
|
|
case 2:
|
|
|
|
|
lim = limit{min: math.MinInt16, max: math.MaxInt16, umin: 0, umax: math.MaxUint16}
|
|
|
|
|
case 1:
|
|
|
|
|
lim = limit{min: math.MinInt8, max: math.MaxInt8, umin: 0, umax: math.MaxUint8}
|
|
|
|
|
default:
|
|
|
|
|
panic("bad")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Tighter limits on some opcodes.
|
|
|
|
|
switch v.Op {
|
|
|
|
|
// constants
|
|
|
|
|
case OpConst64:
|
|
|
|
|
lim = limit{min: v.AuxInt, max: v.AuxInt, umin: uint64(v.AuxInt), umax: uint64(v.AuxInt)}
|
|
|
|
|
case OpConst32:
|
|
|
|
|
lim = limit{min: v.AuxInt, max: v.AuxInt, umin: uint64(uint32(v.AuxInt)), umax: uint64(uint32(v.AuxInt))}
|
|
|
|
|
case OpConst16:
|
|
|
|
|
lim = limit{min: v.AuxInt, max: v.AuxInt, umin: uint64(uint16(v.AuxInt)), umax: uint64(uint16(v.AuxInt))}
|
|
|
|
|
case OpConst8:
|
|
|
|
|
lim = limit{min: v.AuxInt, max: v.AuxInt, umin: uint64(uint8(v.AuxInt)), umax: uint64(uint8(v.AuxInt))}
|
|
|
|
|
|
|
|
|
|
// extensions
|
|
|
|
|
case OpZeroExt8to64, OpZeroExt8to32, OpZeroExt8to16:
|
|
|
|
|
lim = lim.signedMinMax(0, 1<<8-1)
|
|
|
|
|
lim = lim.unsignedMax(1<<8 - 1)
|
|
|
|
|
case OpZeroExt16to64, OpZeroExt16to32:
|
|
|
|
|
lim = lim.signedMinMax(0, 1<<16-1)
|
|
|
|
|
lim = lim.unsignedMax(1<<16 - 1)
|
|
|
|
|
case OpZeroExt32to64:
|
|
|
|
|
lim = lim.signedMinMax(0, 1<<32-1)
|
|
|
|
|
lim = lim.unsignedMax(1<<32 - 1)
|
|
|
|
|
case OpSignExt8to64, OpSignExt8to32, OpSignExt8to16:
|
|
|
|
|
lim = lim.signedMinMax(math.MinInt8, math.MaxInt8)
|
|
|
|
|
case OpSignExt16to64, OpSignExt16to32:
|
|
|
|
|
lim = lim.signedMinMax(math.MinInt16, math.MaxInt16)
|
|
|
|
|
case OpSignExt32to64:
|
|
|
|
|
lim = lim.signedMinMax(math.MinInt32, math.MaxInt32)
|
|
|
|
|
|
|
|
|
|
// math/bits intrinsics
|
|
|
|
|
case OpCtz64, OpBitLen64:
|
|
|
|
|
lim = lim.unsignedMax(64)
|
|
|
|
|
case OpCtz32, OpBitLen32:
|
|
|
|
|
lim = lim.unsignedMax(32)
|
|
|
|
|
case OpCtz16, OpBitLen16:
|
|
|
|
|
lim = lim.unsignedMax(16)
|
|
|
|
|
case OpCtz8, OpBitLen8:
|
|
|
|
|
lim = lim.unsignedMax(8)
|
|
|
|
|
|
2025-01-14 22:13:29 +01:00
|
|
|
// bool to uint8 conversion
|
|
|
|
|
case OpCvtBoolToUint8:
|
|
|
|
|
lim = lim.unsignedMax(1)
|
|
|
|
|
|
2024-06-14 22:29:09 -07:00
|
|
|
// length operations
|
|
|
|
|
case OpStringLen, OpSliceLen, OpSliceCap:
|
|
|
|
|
lim = lim.signedMin(0)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// signed <-> unsigned propagation
|
|
|
|
|
if lim.min >= 0 {
|
|
|
|
|
lim = lim.unsignedMinMax(uint64(lim.min), uint64(lim.max))
|
|
|
|
|
}
|
|
|
|
|
if fitsInBitsU(lim.umax, uint(8*v.Type.Size()-1)) {
|
|
|
|
|
lim = lim.signedMinMax(int64(lim.umin), int64(lim.umax))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return lim
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// flowLimit updates the known limits of v in ft. Returns true if anything changed.
|
|
|
|
|
// flowLimit can use the ranges of input arguments.
|
|
|
|
|
//
|
|
|
|
|
// Note: this calculation only happens at the point the value is defined. We do not reevaluate
|
|
|
|
|
// it later. So for example:
|
|
|
|
|
//
|
|
|
|
|
// v := x + y
|
|
|
|
|
// if 0 <= x && x < 5 && 0 <= y && y < 5 { ... use v ... }
|
|
|
|
|
//
|
|
|
|
|
// we don't discover that the range of v is bounded in the conditioned
|
|
|
|
|
// block. We could recompute the range of v once we enter the block so
|
|
|
|
|
// we know that it is 0 <= v <= 8, but we don't have a mechanism to do
|
|
|
|
|
// that right now.
|
|
|
|
|
func (ft *factsTable) flowLimit(v *Value) bool {
|
|
|
|
|
if !v.Type.IsInteger() {
|
|
|
|
|
// TODO: boolean?
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Additional limits based on opcode and argument.
|
|
|
|
|
// No need to repeat things here already done in initLimit.
|
|
|
|
|
switch v.Op {
|
|
|
|
|
|
|
|
|
|
// extensions
|
|
|
|
|
case OpZeroExt8to64, OpZeroExt8to32, OpZeroExt8to16, OpZeroExt16to64, OpZeroExt16to32, OpZeroExt32to64:
|
|
|
|
|
a := ft.limits[v.Args[0].ID]
|
|
|
|
|
return ft.unsignedMinMax(v, a.umin, a.umax)
|
|
|
|
|
case OpSignExt8to64, OpSignExt8to32, OpSignExt8to16, OpSignExt16to64, OpSignExt16to32, OpSignExt32to64:
|
|
|
|
|
a := ft.limits[v.Args[0].ID]
|
|
|
|
|
return ft.signedMinMax(v, a.min, a.max)
|
2024-08-14 21:25:08 +02:00
|
|
|
case OpTrunc64to8, OpTrunc64to16, OpTrunc64to32, OpTrunc32to8, OpTrunc32to16, OpTrunc16to8:
|
|
|
|
|
a := ft.limits[v.Args[0].ID]
|
|
|
|
|
if a.umax <= 1<<(uint64(v.Type.Size())*8)-1 {
|
|
|
|
|
return ft.unsignedMinMax(v, a.umin, a.umax)
|
|
|
|
|
}
|
2024-06-14 22:29:09 -07:00
|
|
|
|
|
|
|
|
// math/bits
|
|
|
|
|
case OpCtz64:
|
|
|
|
|
a := ft.limits[v.Args[0].ID]
|
|
|
|
|
if a.nonzero() {
|
2024-08-07 22:16:00 +02:00
|
|
|
return ft.unsignedMax(v, uint64(bits.Len64(a.umax)-1))
|
2024-06-14 22:29:09 -07:00
|
|
|
}
|
|
|
|
|
case OpCtz32:
|
|
|
|
|
a := ft.limits[v.Args[0].ID]
|
|
|
|
|
if a.nonzero() {
|
2024-08-07 22:16:00 +02:00
|
|
|
return ft.unsignedMax(v, uint64(bits.Len32(uint32(a.umax))-1))
|
2024-06-14 22:29:09 -07:00
|
|
|
}
|
|
|
|
|
case OpCtz16:
|
|
|
|
|
a := ft.limits[v.Args[0].ID]
|
|
|
|
|
if a.nonzero() {
|
2024-08-07 22:16:00 +02:00
|
|
|
return ft.unsignedMax(v, uint64(bits.Len16(uint16(a.umax))-1))
|
2024-06-14 22:29:09 -07:00
|
|
|
}
|
|
|
|
|
case OpCtz8:
|
|
|
|
|
a := ft.limits[v.Args[0].ID]
|
|
|
|
|
if a.nonzero() {
|
2024-08-07 22:16:00 +02:00
|
|
|
return ft.unsignedMax(v, uint64(bits.Len8(uint8(a.umax))-1))
|
2024-06-14 22:29:09 -07:00
|
|
|
}
|
|
|
|
|
|
2024-08-07 20:20:21 +02:00
|
|
|
case OpBitLen64:
|
|
|
|
|
a := ft.limits[v.Args[0].ID]
|
|
|
|
|
return ft.unsignedMinMax(v,
|
|
|
|
|
uint64(bits.Len64(a.umin)),
|
|
|
|
|
uint64(bits.Len64(a.umax)))
|
|
|
|
|
case OpBitLen32:
|
|
|
|
|
a := ft.limits[v.Args[0].ID]
|
|
|
|
|
return ft.unsignedMinMax(v,
|
|
|
|
|
uint64(bits.Len32(uint32(a.umin))),
|
|
|
|
|
uint64(bits.Len32(uint32(a.umax))))
|
|
|
|
|
case OpBitLen16:
|
|
|
|
|
a := ft.limits[v.Args[0].ID]
|
|
|
|
|
return ft.unsignedMinMax(v,
|
|
|
|
|
uint64(bits.Len16(uint16(a.umin))),
|
|
|
|
|
uint64(bits.Len16(uint16(a.umax))))
|
|
|
|
|
case OpBitLen8:
|
|
|
|
|
a := ft.limits[v.Args[0].ID]
|
|
|
|
|
return ft.unsignedMinMax(v,
|
|
|
|
|
uint64(bits.Len8(uint8(a.umin))),
|
|
|
|
|
uint64(bits.Len8(uint8(a.umax))))
|
|
|
|
|
|
2024-06-14 22:29:09 -07:00
|
|
|
// Masks.
|
2024-08-09 15:45:39 +02:00
|
|
|
|
|
|
|
|
// TODO: if y.umax and y.umin share a leading bit pattern, y also has that leading bit pattern.
|
|
|
|
|
// we could compare the patterns of always set bits in a and b and learn more about minimum and maximum.
|
|
|
|
|
// But I doubt this help any real world code.
|
2024-06-14 22:29:09 -07:00
|
|
|
case OpAnd64, OpAnd32, OpAnd16, OpAnd8:
|
|
|
|
|
// AND can only make the value smaller.
|
|
|
|
|
a := ft.limits[v.Args[0].ID]
|
|
|
|
|
b := ft.limits[v.Args[1].ID]
|
2024-09-03 13:29:42 -07:00
|
|
|
return ft.unsignedMax(v, min(a.umax, b.umax))
|
2024-06-14 22:29:09 -07:00
|
|
|
case OpOr64, OpOr32, OpOr16, OpOr8:
|
2024-08-09 16:35:46 +02:00
|
|
|
// OR can only make the value bigger and can't flip bits proved to be zero in both inputs.
|
2024-06-14 22:29:09 -07:00
|
|
|
a := ft.limits[v.Args[0].ID]
|
|
|
|
|
b := ft.limits[v.Args[1].ID]
|
2024-08-09 16:35:46 +02:00
|
|
|
return ft.unsignedMinMax(v,
|
2024-09-03 13:29:42 -07:00
|
|
|
max(a.umin, b.umin),
|
2024-08-09 16:35:46 +02:00
|
|
|
1<<bits.Len64(a.umax|b.umax)-1)
|
2024-06-14 22:29:09 -07:00
|
|
|
case OpXor64, OpXor32, OpXor16, OpXor8:
|
2024-08-09 15:45:39 +02:00
|
|
|
// XOR can't flip bits that are proved to be zero in both inputs.
|
|
|
|
|
a := ft.limits[v.Args[0].ID]
|
|
|
|
|
b := ft.limits[v.Args[1].ID]
|
|
|
|
|
return ft.unsignedMax(v, 1<<bits.Len64(a.umax|b.umax)-1)
|
2024-08-14 22:28:00 +02:00
|
|
|
case OpCom64, OpCom32, OpCom16, OpCom8:
|
|
|
|
|
a := ft.limits[v.Args[0].ID]
|
|
|
|
|
return ft.newLimit(v, a.com(uint(v.Type.Size())*8))
|
2024-06-14 22:29:09 -07:00
|
|
|
|
|
|
|
|
// Arithmetic.
|
|
|
|
|
case OpAdd64:
|
|
|
|
|
a := ft.limits[v.Args[0].ID]
|
|
|
|
|
b := ft.limits[v.Args[1].ID]
|
|
|
|
|
return ft.newLimit(v, a.add(b, 64))
|
|
|
|
|
case OpAdd32:
|
|
|
|
|
a := ft.limits[v.Args[0].ID]
|
|
|
|
|
b := ft.limits[v.Args[1].ID]
|
|
|
|
|
return ft.newLimit(v, a.add(b, 32))
|
|
|
|
|
case OpAdd16:
|
|
|
|
|
a := ft.limits[v.Args[0].ID]
|
|
|
|
|
b := ft.limits[v.Args[1].ID]
|
|
|
|
|
return ft.newLimit(v, a.add(b, 16))
|
|
|
|
|
case OpAdd8:
|
|
|
|
|
a := ft.limits[v.Args[0].ID]
|
|
|
|
|
b := ft.limits[v.Args[1].ID]
|
|
|
|
|
return ft.newLimit(v, a.add(b, 8))
|
|
|
|
|
case OpSub64:
|
|
|
|
|
a := ft.limits[v.Args[0].ID]
|
|
|
|
|
b := ft.limits[v.Args[1].ID]
|
|
|
|
|
return ft.newLimit(v, a.sub(b, 64))
|
|
|
|
|
case OpSub32:
|
|
|
|
|
a := ft.limits[v.Args[0].ID]
|
|
|
|
|
b := ft.limits[v.Args[1].ID]
|
|
|
|
|
return ft.newLimit(v, a.sub(b, 32))
|
|
|
|
|
case OpSub16:
|
|
|
|
|
a := ft.limits[v.Args[0].ID]
|
|
|
|
|
b := ft.limits[v.Args[1].ID]
|
|
|
|
|
return ft.newLimit(v, a.sub(b, 16))
|
|
|
|
|
case OpSub8:
|
|
|
|
|
a := ft.limits[v.Args[0].ID]
|
|
|
|
|
b := ft.limits[v.Args[1].ID]
|
|
|
|
|
return ft.newLimit(v, a.sub(b, 8))
|
2024-08-15 04:54:46 +02:00
|
|
|
case OpNeg64, OpNeg32, OpNeg16, OpNeg8:
|
|
|
|
|
a := ft.limits[v.Args[0].ID]
|
|
|
|
|
bitsize := uint(v.Type.Size()) * 8
|
|
|
|
|
return ft.newLimit(v, a.com(bitsize).add(limit{min: 1, max: 1, umin: 1, umax: 1}, bitsize))
|
2024-06-27 15:53:24 -07:00
|
|
|
case OpMul64:
|
|
|
|
|
a := ft.limits[v.Args[0].ID]
|
|
|
|
|
b := ft.limits[v.Args[1].ID]
|
|
|
|
|
return ft.newLimit(v, a.mul(b, 64))
|
|
|
|
|
case OpMul32:
|
|
|
|
|
a := ft.limits[v.Args[0].ID]
|
|
|
|
|
b := ft.limits[v.Args[1].ID]
|
|
|
|
|
return ft.newLimit(v, a.mul(b, 32))
|
|
|
|
|
case OpMul16:
|
|
|
|
|
a := ft.limits[v.Args[0].ID]
|
|
|
|
|
b := ft.limits[v.Args[1].ID]
|
|
|
|
|
return ft.newLimit(v, a.mul(b, 16))
|
|
|
|
|
case OpMul8:
|
|
|
|
|
a := ft.limits[v.Args[0].ID]
|
|
|
|
|
b := ft.limits[v.Args[1].ID]
|
|
|
|
|
return ft.newLimit(v, a.mul(b, 8))
|
|
|
|
|
case OpLsh64x64, OpLsh64x32, OpLsh64x16, OpLsh64x8:
|
|
|
|
|
a := ft.limits[v.Args[0].ID]
|
|
|
|
|
b := ft.limits[v.Args[1].ID]
|
|
|
|
|
return ft.newLimit(v, a.mul(b.exp2(64), 64))
|
|
|
|
|
case OpLsh32x64, OpLsh32x32, OpLsh32x16, OpLsh32x8:
|
|
|
|
|
a := ft.limits[v.Args[0].ID]
|
|
|
|
|
b := ft.limits[v.Args[1].ID]
|
|
|
|
|
return ft.newLimit(v, a.mul(b.exp2(32), 32))
|
|
|
|
|
case OpLsh16x64, OpLsh16x32, OpLsh16x16, OpLsh16x8:
|
|
|
|
|
a := ft.limits[v.Args[0].ID]
|
|
|
|
|
b := ft.limits[v.Args[1].ID]
|
|
|
|
|
return ft.newLimit(v, a.mul(b.exp2(16), 16))
|
|
|
|
|
case OpLsh8x64, OpLsh8x32, OpLsh8x16, OpLsh8x8:
|
|
|
|
|
a := ft.limits[v.Args[0].ID]
|
|
|
|
|
b := ft.limits[v.Args[1].ID]
|
|
|
|
|
return ft.newLimit(v, a.mul(b.exp2(8), 8))
|
2024-08-13 18:40:44 +02:00
|
|
|
case OpMod64, OpMod32, OpMod16, OpMod8:
|
|
|
|
|
a := ft.limits[v.Args[0].ID]
|
|
|
|
|
b := ft.limits[v.Args[1].ID]
|
|
|
|
|
if !(a.nonnegative() && b.nonnegative()) {
|
|
|
|
|
// TODO: we could handle signed limits but I didn't bother.
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
fallthrough
|
2024-08-13 17:17:06 +02:00
|
|
|
case OpMod64u, OpMod32u, OpMod16u, OpMod8u:
|
|
|
|
|
a := ft.limits[v.Args[0].ID]
|
|
|
|
|
b := ft.limits[v.Args[1].ID]
|
|
|
|
|
// Underflow in the arithmetic below is ok, it gives to MaxUint64 which does nothing to the limit.
|
2024-09-03 13:29:42 -07:00
|
|
|
return ft.unsignedMax(v, min(a.umax, b.umax-1))
|
2024-08-13 18:40:44 +02:00
|
|
|
case OpDiv64, OpDiv32, OpDiv16, OpDiv8:
|
|
|
|
|
a := ft.limits[v.Args[0].ID]
|
|
|
|
|
b := ft.limits[v.Args[1].ID]
|
|
|
|
|
if !(a.nonnegative() && b.nonnegative()) {
|
|
|
|
|
// TODO: we could handle signed limits but I didn't bother.
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
fallthrough
|
2024-08-13 18:27:38 +02:00
|
|
|
case OpDiv64u, OpDiv32u, OpDiv16u, OpDiv8u:
|
|
|
|
|
a := ft.limits[v.Args[0].ID]
|
|
|
|
|
b := ft.limits[v.Args[1].ID]
|
|
|
|
|
lim := noLimit
|
|
|
|
|
if b.umax > 0 {
|
|
|
|
|
lim = lim.unsignedMin(a.umin / b.umax)
|
|
|
|
|
}
|
|
|
|
|
if b.umin > 0 {
|
|
|
|
|
lim = lim.unsignedMax(a.umax / b.umin)
|
|
|
|
|
}
|
|
|
|
|
return ft.newLimit(v, lim)
|
2024-06-27 15:53:24 -07:00
|
|
|
|
2024-06-14 22:29:09 -07:00
|
|
|
case OpPhi:
|
|
|
|
|
// Compute the union of all the input phis.
|
|
|
|
|
// Often this will convey no information, because the block
|
|
|
|
|
// is not dominated by its predecessors and hence the
|
|
|
|
|
// phi arguments might not have been processed yet. But if
|
|
|
|
|
// the values are declared earlier, it may help. e.g., for
|
|
|
|
|
// v = phi(c3, c5)
|
|
|
|
|
// where c3 = OpConst [3] and c5 = OpConst [5] are
|
|
|
|
|
// defined in the entry block, we can derive [3,5]
|
|
|
|
|
// as the limit for v.
|
|
|
|
|
l := ft.limits[v.Args[0].ID]
|
|
|
|
|
for _, a := range v.Args[1:] {
|
|
|
|
|
l2 := ft.limits[a.ID]
|
|
|
|
|
l.min = min(l.min, l2.min)
|
|
|
|
|
l.max = max(l.max, l2.max)
|
2024-09-03 13:29:42 -07:00
|
|
|
l.umin = min(l.umin, l2.umin)
|
|
|
|
|
l.umax = max(l.umax, l2.umax)
|
2024-06-14 22:29:09 -07:00
|
|
|
}
|
|
|
|
|
return ft.newLimit(v, l)
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
2016-03-07 18:36:16 +01:00
|
|
|
// getBranch returns the range restrictions added by p
|
|
|
|
|
// when reaching b. p is the immediate dominator of b.
|
cmd/compile: use sparse algorithm for phis in large program
This adds a sparse method for locating nearest ancestors
in a dominator tree, and checks blocks with more than one
predecessor for differences and inserts phi functions where
there are.
Uses reversed post order to cut number of passes, running
it from first def to last use ("last use" for paramout and
mem is end-of-program; last use for a phi input from a
backedge is the source of the back edge)
Includes a cutover from old algorithm to new to avoid paying
large constant factor for small programs. This keeps normal
builds running at about the same time, while not running
over-long on large machine-generated inputs.
Add "phase" flags for ssa/build -- ssa/build/stats prints
number of blocks, values (before and after linking references
and inserting phis, so expansion can be measured), and their
product; the product governs the cutover, where a good value
seems to be somewhere between 1 and 5 million.
Among the files compiled by make.bash, this is the shape of
the tail of the distribution for #blocks, #vars, and their
product:
#blocks #vars product
max 6171 28180 173,898,780
99.9% 1641 6548 10,401,878
99% 463 1909 873,721
95% 152 639 95,235
90% 84 359 30,021
The old algorithm is indeed usually fastest, for 99%ile
values of usually.
The fix to LookupVarOutgoing
( https://go-review.googlesource.com/#/c/22790/ )
deals with some of the same problems addressed by this CL,
but on at least one bug ( #15537 ) this change is still
a significant help.
With this CL:
/tmp/gopath$ rm -rf pkg bin
/tmp/gopath$ time go get -v -gcflags -memprofile=y.mprof \
github.com/gogo/protobuf/test/theproto3/combos/...
...
real 4m35.200s
user 13m16.644s
sys 0m36.712s
and pprof reports 3.4GB allocated in one of the larger profiles
With tip:
/tmp/gopath$ rm -rf pkg bin
/tmp/gopath$ time go get -v -gcflags -memprofile=y.mprof \
github.com/gogo/protobuf/test/theproto3/combos/...
...
real 10m36.569s
user 25m52.286s
sys 4m3.696s
and pprof reports 8.3GB allocated in the same larger profile
With this CL, most of the compilation time on the benchmarked
input is spent in register/stack allocation (cumulative 53%)
and in the sparse lookup algorithm itself (cumulative 20%).
Fixes #15537.
Change-Id: Ia0299dda6a291534d8b08e5f9883216ded677a00
Reviewed-on: https://go-review.googlesource.com/22342
Reviewed-by: Keith Randall <khr@golang.org>
Run-TryBot: David Chase <drchase@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
2016-04-21 13:24:58 -04:00
|
|
|
func getBranch(sdom SparseTree, p *Block, b *Block) branch {
|
2022-03-06 12:07:54 -08:00
|
|
|
if p == nil {
|
2016-03-07 18:36:16 +01:00
|
|
|
return unknown
|
2016-02-19 12:14:42 +01:00
|
|
|
}
|
2022-03-06 12:07:54 -08:00
|
|
|
switch p.Kind {
|
|
|
|
|
case BlockIf:
|
|
|
|
|
// If p and p.Succs[0] are dominators it means that every path
|
|
|
|
|
// from entry to b passes through p and p.Succs[0]. We care that
|
|
|
|
|
// no path from entry to b passes through p.Succs[1]. If p.Succs[0]
|
|
|
|
|
// has one predecessor then (apart from the degenerate case),
|
|
|
|
|
// there is no path from entry that can reach b through p.Succs[1].
|
|
|
|
|
// TODO: how about p->yes->b->yes, i.e. a loop in yes.
|
|
|
|
|
if sdom.IsAncestorEq(p.Succs[0].b, b) && len(p.Succs[0].b.Preds) == 1 {
|
|
|
|
|
return positive
|
|
|
|
|
}
|
|
|
|
|
if sdom.IsAncestorEq(p.Succs[1].b, b) && len(p.Succs[1].b.Preds) == 1 {
|
|
|
|
|
return negative
|
|
|
|
|
}
|
|
|
|
|
case BlockJumpTable:
|
|
|
|
|
// TODO: this loop can lead to quadratic behavior, as
|
|
|
|
|
// getBranch can be called len(p.Succs) times.
|
|
|
|
|
for i, e := range p.Succs {
|
|
|
|
|
if sdom.IsAncestorEq(e.b, b) && len(e.b.Preds) == 1 {
|
|
|
|
|
return jumpTable0 + branch(i)
|
|
|
|
|
}
|
|
|
|
|
}
|
2016-02-19 12:14:42 +01:00
|
|
|
}
|
2016-03-07 18:36:16 +01:00
|
|
|
return unknown
|
2016-02-19 12:14:42 +01:00
|
|
|
}
|
|
|
|
|
|
2018-04-02 01:57:49 +02:00
|
|
|
// addIndVarRestrictions updates the factsTables ft with the facts
|
|
|
|
|
// learned from the induction variable indVar which drives the loop
|
|
|
|
|
// starting in Block b.
|
|
|
|
|
func addIndVarRestrictions(ft *factsTable, b *Block, iv indVar) {
|
|
|
|
|
d := signed
|
2019-02-06 19:49:15 +00:00
|
|
|
if ft.isNonNegative(iv.min) && ft.isNonNegative(iv.max) {
|
2018-04-02 01:57:49 +02:00
|
|
|
d |= unsigned
|
|
|
|
|
}
|
|
|
|
|
|
2018-04-15 16:03:30 +02:00
|
|
|
if iv.flags&indVarMinExc == 0 {
|
|
|
|
|
addRestrictions(b, ft, d, iv.min, iv.ind, lt|eq)
|
|
|
|
|
} else {
|
|
|
|
|
addRestrictions(b, ft, d, iv.min, iv.ind, lt)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if iv.flags&indVarMaxInc == 0 {
|
|
|
|
|
addRestrictions(b, ft, d, iv.ind, iv.max, lt)
|
|
|
|
|
} else {
|
|
|
|
|
addRestrictions(b, ft, d, iv.ind, iv.max, lt|eq)
|
|
|
|
|
}
|
2018-04-02 01:57:49 +02:00
|
|
|
}
|
|
|
|
|
|
2018-04-02 01:39:03 +02:00
|
|
|
// addBranchRestrictions updates the factsTables ft with the facts learned when
|
|
|
|
|
// branching from Block b in direction br.
|
|
|
|
|
func addBranchRestrictions(ft *factsTable, b *Block, br branch) {
|
2019-08-12 20:19:58 +01:00
|
|
|
c := b.Controls[0]
|
2022-03-06 12:07:54 -08:00
|
|
|
switch {
|
|
|
|
|
case br == negative:
|
2024-07-07 14:58:47 -07:00
|
|
|
ft.booleanFalse(c)
|
2022-03-06 12:07:54 -08:00
|
|
|
case br == positive:
|
2024-07-07 14:58:47 -07:00
|
|
|
ft.booleanTrue(c)
|
2022-03-06 12:07:54 -08:00
|
|
|
case br >= jumpTable0:
|
|
|
|
|
idx := br - jumpTable0
|
|
|
|
|
val := int64(idx)
|
|
|
|
|
if v, off := isConstDelta(c); v != nil {
|
|
|
|
|
// Establish the bound on the underlying value we're switching on,
|
|
|
|
|
// not on the offset-ed value used as the jump table index.
|
|
|
|
|
c = v
|
|
|
|
|
val -= off
|
|
|
|
|
}
|
2024-07-11 12:36:32 -07:00
|
|
|
ft.newLimit(c, limit{min: val, max: val, umin: uint64(val), umax: uint64(val)})
|
2018-04-02 01:45:53 +02:00
|
|
|
default:
|
|
|
|
|
panic("unknown branch")
|
|
|
|
|
}
|
cmd/compile: make prove pass use unsatisfiability
Currently the prove pass uses implication queries. For each block, it
collects the set of branch conditions leading to that block, and
queries this fact table for whether any of these facts imply the
block's own branch condition (or its inverse). This works remarkably
well considering it doesn't do any deduction on these facts, but it
has various downsides:
1. It requires an implementation both of adding facts to the table and
determining implications. These are very nearly duals of each
other, but require separate implementations. Likewise, the process
of asserting facts of dominating branch conditions is very nearly
the dual of the process of querying implied branch conditions.
2. It leads to less effective use of derived facts. For example, the
prove pass currently derives facts about the relations between len
and cap, but can't make use of these unless a branch condition is
in the exact form of a derived fact. If one of these derived facts
contradicts another fact, it won't notice or make use of this.
This CL changes the approach of the prove pass to instead use
*contradiction* instead of implication. Rather than ever querying a
branch condition, it simply adds branch conditions to the fact table.
If this leads to a contradiction (specifically, it makes the fact set
unsatisfiable), that branch is impossible and can be cut. As a result,
1. We can eliminate the code for determining implications
(factsTable.get disappears entirely). Also, there is now a single
implementation of visiting and asserting branch conditions, since
we don't have to flip them around to treat them as facts in one
place and queries in another.
2. Derived facts can be used effectively. It doesn't matter *why* the
fact table is unsatisfiable; a contradiction in any of the facts is
enough.
3. As an added benefit, it's now quite easy to avoid traversing beyond
provably-unreachable blocks. In contrast, the current
implementation always visits all blocks.
The prove pass already has nearly all of the mechanism necessary to
compute unsatisfiability, which means this both simplifies the code
and makes it more powerful.
The only complication is that the current implication procedure has a
hack for dealing with the 0 <= Args[0] condition of OpIsInBounds and
OpIsSliceInBounds. We replace this with asserting the appropriate fact
when we process one of these conditions. This seems much cleaner
anyway, and works because we can now take advantage of derived facts.
This has no measurable effect on compiler performance.
Effectiveness:
There is exactly one condition in all of std and cmd that this fails
to prove that the old implementation could: (int64(^uint(0)>>1) < x)
in encoding/gob. This can never be true because x is an int, and it's
basically coincidence that the old code gets this. (For example, it
fails to prove the similar (x < ^int64(^uint(0)>>1)) condition that
immediately precedes it, and even though the conditions are logically
unrelated, it wouldn't get the second one if it hadn't first processed
the first!)
It does, however, prove a few dozen additional branches. These come
from facts that are added to the fact table about the relations
between len and cap. These were almost never queried directly before,
but could lead to contradictions, which the unsat-based approach is
able to use.
There are exactly two branches in std and cmd that this implementation
proves in the *other* direction. This sounds scary, but is okay
because both occur in already-unreachable blocks, so it doesn't matter
what we chose. Because the fact table logic is sound but incomplete,
it fails to prove that the block isn't reachable, even though it is
able to prove that both outgoing branches are impossible. We could
turn these blocks into BlockExit blocks, but it doesn't seem worth the
trouble of the extra proof effort for something that happens twice in
all of std and cmd.
Tests:
This CL updates test/prove.go to change the expected messages because
it can no longer give a "reason" why it proved or disproved a
condition. It also adds a new test of a branch it couldn't prove
before.
It mostly guts test/sliceopt.go, removing everything related to slice
bounds optimizations and moving a few relevant tests to test/prove.go.
Much of this test is actually unreachable. The new prove pass figures
this out and doesn't try to prove anything about the unreachable
parts. The output on the unreachable parts is already suspect because
anything can be proved at that point, so it's really just a regression
test for an algorithm the compiler no longer uses.
This is a step toward fixing #23354. That issue is quite easy to fix
once we can use derived facts effectively.
Change-Id: Ia48a1b9ee081310579fe474e4a61857424ff8ce8
Reviewed-on: https://go-review.googlesource.com/87478
Reviewed-by: Keith Randall <khr@golang.org>
2018-01-10 16:28:58 -05:00
|
|
|
}
|
|
|
|
|
|
2018-04-02 01:39:03 +02:00
|
|
|
// addRestrictions updates restrictions from the immediate
|
2018-04-02 01:45:53 +02:00
|
|
|
// dominating block (p) using r.
|
|
|
|
|
func addRestrictions(parent *Block, ft *factsTable, t domain, v, w *Value, r relation) {
|
|
|
|
|
if t == 0 {
|
|
|
|
|
// Trivial case: nothing to do.
|
2023-06-13 23:01:11 +00:00
|
|
|
// Should not happen, but just in case.
|
2016-02-19 12:14:42 +01:00
|
|
|
return
|
|
|
|
|
}
|
2016-03-07 18:36:16 +01:00
|
|
|
for i := domain(1); i <= t; i <<= 1 {
|
2016-11-27 11:43:08 -08:00
|
|
|
if t&i == 0 {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
ft.update(parent, v, w, i, r)
|
2016-02-19 12:14:42 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-14 22:29:09 -07:00
|
|
|
func addLocalFacts(ft *factsTable, b *Block) {
|
|
|
|
|
// Propagate constant ranges among values in this block.
|
|
|
|
|
// We do this before the second loop so that we have the
|
|
|
|
|
// most up-to-date constant bounds for isNonNegative calls.
|
|
|
|
|
for {
|
|
|
|
|
changed := false
|
|
|
|
|
for _, v := range b.Values {
|
|
|
|
|
changed = ft.flowLimit(v) || changed
|
cmd/compile: detect OFORUNTIL inductive facts in prove
Currently, we compile range loops into for loops with the obvious
initialization and update of the index variable. In this form, the
prove pass can see that the body is dominated by an i < len condition,
and findIndVar can detect that i is an induction variable and that
0 <= i < len.
GOEXPERIMENT=preemptibleloops compiles range loops to OFORUNTIL and
we're preparing to unconditionally switch to a variation of this for
#24543. OFORUNTIL moves the increment and condition *after* the body,
which makes the bounds on the index variable much less obvious. With
OFORUNTIL, proving anything about the index variable requires
understanding the phi that joins the index values at the top of the
loop body block.
This interferes with both prove's ability to see that i < len (this is
true on both paths that enter the body, but from two different
conditional checks) and with findIndVar's ability to detect the
induction pattern.
Fix this by teaching prove to detect that the index in the pattern
constructed by OFORUNTIL is an induction variable and add both bounds
to the facts table. Currently this is done separately from findIndVar
because it depends on prove's factsTable, while findIndVar runs before
visiting blocks and building the factsTable.
Without any GOEXPERIMENT, this has no effect on std or cmd. However,
with GOEXPERIMENT=preemptibleloops, this change becomes necessary to
prove 90 conditions in std and cmd.
Change-Id: Ic025d669f81b53426309da5a6e8010e5ccaf4f49
Reviewed-on: https://go-review.googlesource.com/102603
Run-TryBot: Austin Clements <austin@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Keith Randall <khr@golang.org>
2018-03-23 17:38:55 -04:00
|
|
|
}
|
2024-06-14 22:29:09 -07:00
|
|
|
if !changed {
|
|
|
|
|
break
|
cmd/compile: detect OFORUNTIL inductive facts in prove
Currently, we compile range loops into for loops with the obvious
initialization and update of the index variable. In this form, the
prove pass can see that the body is dominated by an i < len condition,
and findIndVar can detect that i is an induction variable and that
0 <= i < len.
GOEXPERIMENT=preemptibleloops compiles range loops to OFORUNTIL and
we're preparing to unconditionally switch to a variation of this for
#24543. OFORUNTIL moves the increment and condition *after* the body,
which makes the bounds on the index variable much less obvious. With
OFORUNTIL, proving anything about the index variable requires
understanding the phi that joins the index values at the top of the
loop body block.
This interferes with both prove's ability to see that i < len (this is
true on both paths that enter the body, but from two different
conditional checks) and with findIndVar's ability to detect the
induction pattern.
Fix this by teaching prove to detect that the index in the pattern
constructed by OFORUNTIL is an induction variable and add both bounds
to the facts table. Currently this is done separately from findIndVar
because it depends on prove's factsTable, while findIndVar runs before
visiting blocks and building the factsTable.
Without any GOEXPERIMENT, this has no effect on std or cmd. However,
with GOEXPERIMENT=preemptibleloops, this change becomes necessary to
prove 90 conditions in std and cmd.
Change-Id: Ic025d669f81b53426309da5a6e8010e5ccaf4f49
Reviewed-on: https://go-review.googlesource.com/102603
Run-TryBot: Austin Clements <austin@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Keith Randall <khr@golang.org>
2018-03-23 17:38:55 -04:00
|
|
|
}
|
2024-06-14 22:29:09 -07:00
|
|
|
}
|
cmd/compile: detect OFORUNTIL inductive facts in prove
Currently, we compile range loops into for loops with the obvious
initialization and update of the index variable. In this form, the
prove pass can see that the body is dominated by an i < len condition,
and findIndVar can detect that i is an induction variable and that
0 <= i < len.
GOEXPERIMENT=preemptibleloops compiles range loops to OFORUNTIL and
we're preparing to unconditionally switch to a variation of this for
#24543. OFORUNTIL moves the increment and condition *after* the body,
which makes the bounds on the index variable much less obvious. With
OFORUNTIL, proving anything about the index variable requires
understanding the phi that joins the index values at the top of the
loop body block.
This interferes with both prove's ability to see that i < len (this is
true on both paths that enter the body, but from two different
conditional checks) and with findIndVar's ability to detect the
induction pattern.
Fix this by teaching prove to detect that the index in the pattern
constructed by OFORUNTIL is an induction variable and add both bounds
to the facts table. Currently this is done separately from findIndVar
because it depends on prove's factsTable, while findIndVar runs before
visiting blocks and building the factsTable.
Without any GOEXPERIMENT, this has no effect on std or cmd. However,
with GOEXPERIMENT=preemptibleloops, this change becomes necessary to
prove 90 conditions in std and cmd.
Change-Id: Ic025d669f81b53426309da5a6e8010e5ccaf4f49
Reviewed-on: https://go-review.googlesource.com/102603
Run-TryBot: Austin Clements <austin@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Keith Randall <khr@golang.org>
2018-03-23 17:38:55 -04:00
|
|
|
|
2024-06-14 22:29:09 -07:00
|
|
|
// Add facts about individual operations.
|
|
|
|
|
for _, v := range b.Values {
|
2024-08-13 17:17:06 +02:00
|
|
|
// FIXME(go.dev/issue/68857): this loop only set up limits properly when b.Values is in topological order.
|
|
|
|
|
// flowLimit can also depend on limits given by this loop which right now is not handled.
|
2024-06-14 22:29:09 -07:00
|
|
|
switch v.Op {
|
|
|
|
|
case OpAnd64, OpAnd32, OpAnd16, OpAnd8:
|
|
|
|
|
ft.update(b, v, v.Args[0], unsigned, lt|eq)
|
|
|
|
|
ft.update(b, v, v.Args[1], unsigned, lt|eq)
|
|
|
|
|
if ft.isNonNegative(v.Args[0]) {
|
|
|
|
|
ft.update(b, v, v.Args[0], signed, lt|eq)
|
cmd/compile: detect OFORUNTIL inductive facts in prove
Currently, we compile range loops into for loops with the obvious
initialization and update of the index variable. In this form, the
prove pass can see that the body is dominated by an i < len condition,
and findIndVar can detect that i is an induction variable and that
0 <= i < len.
GOEXPERIMENT=preemptibleloops compiles range loops to OFORUNTIL and
we're preparing to unconditionally switch to a variation of this for
#24543. OFORUNTIL moves the increment and condition *after* the body,
which makes the bounds on the index variable much less obvious. With
OFORUNTIL, proving anything about the index variable requires
understanding the phi that joins the index values at the top of the
loop body block.
This interferes with both prove's ability to see that i < len (this is
true on both paths that enter the body, but from two different
conditional checks) and with findIndVar's ability to detect the
induction pattern.
Fix this by teaching prove to detect that the index in the pattern
constructed by OFORUNTIL is an induction variable and add both bounds
to the facts table. Currently this is done separately from findIndVar
because it depends on prove's factsTable, while findIndVar runs before
visiting blocks and building the factsTable.
Without any GOEXPERIMENT, this has no effect on std or cmd. However,
with GOEXPERIMENT=preemptibleloops, this change becomes necessary to
prove 90 conditions in std and cmd.
Change-Id: Ic025d669f81b53426309da5a6e8010e5ccaf4f49
Reviewed-on: https://go-review.googlesource.com/102603
Run-TryBot: Austin Clements <austin@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Keith Randall <khr@golang.org>
2018-03-23 17:38:55 -04:00
|
|
|
}
|
2024-06-14 22:29:09 -07:00
|
|
|
if ft.isNonNegative(v.Args[1]) {
|
|
|
|
|
ft.update(b, v, v.Args[1], signed, lt|eq)
|
cmd/compile: detect OFORUNTIL inductive facts in prove
Currently, we compile range loops into for loops with the obvious
initialization and update of the index variable. In this form, the
prove pass can see that the body is dominated by an i < len condition,
and findIndVar can detect that i is an induction variable and that
0 <= i < len.
GOEXPERIMENT=preemptibleloops compiles range loops to OFORUNTIL and
we're preparing to unconditionally switch to a variation of this for
#24543. OFORUNTIL moves the increment and condition *after* the body,
which makes the bounds on the index variable much less obvious. With
OFORUNTIL, proving anything about the index variable requires
understanding the phi that joins the index values at the top of the
loop body block.
This interferes with both prove's ability to see that i < len (this is
true on both paths that enter the body, but from two different
conditional checks) and with findIndVar's ability to detect the
induction pattern.
Fix this by teaching prove to detect that the index in the pattern
constructed by OFORUNTIL is an induction variable and add both bounds
to the facts table. Currently this is done separately from findIndVar
because it depends on prove's factsTable, while findIndVar runs before
visiting blocks and building the factsTable.
Without any GOEXPERIMENT, this has no effect on std or cmd. However,
with GOEXPERIMENT=preemptibleloops, this change becomes necessary to
prove 90 conditions in std and cmd.
Change-Id: Ic025d669f81b53426309da5a6e8010e5ccaf4f49
Reviewed-on: https://go-review.googlesource.com/102603
Run-TryBot: Austin Clements <austin@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Keith Randall <khr@golang.org>
2018-03-23 17:38:55 -04:00
|
|
|
}
|
2024-06-14 22:29:09 -07:00
|
|
|
case OpOr64, OpOr32, OpOr16, OpOr8:
|
|
|
|
|
// TODO: investigate how to always add facts without much slowdown, see issue #57959
|
|
|
|
|
//ft.update(b, v, v.Args[0], unsigned, gt|eq)
|
|
|
|
|
//ft.update(b, v, v.Args[1], unsigned, gt|eq)
|
|
|
|
|
case OpDiv64u, OpDiv32u, OpDiv16u, OpDiv8u,
|
|
|
|
|
OpRsh8Ux64, OpRsh8Ux32, OpRsh8Ux16, OpRsh8Ux8,
|
|
|
|
|
OpRsh16Ux64, OpRsh16Ux32, OpRsh16Ux16, OpRsh16Ux8,
|
|
|
|
|
OpRsh32Ux64, OpRsh32Ux32, OpRsh32Ux16, OpRsh32Ux8,
|
|
|
|
|
OpRsh64Ux64, OpRsh64Ux32, OpRsh64Ux16, OpRsh64Ux8:
|
|
|
|
|
ft.update(b, v, v.Args[0], unsigned, lt|eq)
|
|
|
|
|
case OpMod64u, OpMod32u, OpMod16u, OpMod8u:
|
|
|
|
|
ft.update(b, v, v.Args[0], unsigned, lt|eq)
|
|
|
|
|
// Note: we have to be careful that this doesn't imply
|
|
|
|
|
// that the modulus is >0, which isn't true until *after*
|
|
|
|
|
// the mod instruction executes (and thus panics if the
|
|
|
|
|
// modulus is 0). See issue 67625.
|
|
|
|
|
ft.update(b, v, v.Args[1], unsigned, lt)
|
|
|
|
|
case OpSliceLen:
|
|
|
|
|
if v.Args[0].Op == OpSliceMake {
|
|
|
|
|
ft.update(b, v, v.Args[0].Args[1], signed, eq)
|
cmd/compile: detect OFORUNTIL inductive facts in prove
Currently, we compile range loops into for loops with the obvious
initialization and update of the index variable. In this form, the
prove pass can see that the body is dominated by an i < len condition,
and findIndVar can detect that i is an induction variable and that
0 <= i < len.
GOEXPERIMENT=preemptibleloops compiles range loops to OFORUNTIL and
we're preparing to unconditionally switch to a variation of this for
#24543. OFORUNTIL moves the increment and condition *after* the body,
which makes the bounds on the index variable much less obvious. With
OFORUNTIL, proving anything about the index variable requires
understanding the phi that joins the index values at the top of the
loop body block.
This interferes with both prove's ability to see that i < len (this is
true on both paths that enter the body, but from two different
conditional checks) and with findIndVar's ability to detect the
induction pattern.
Fix this by teaching prove to detect that the index in the pattern
constructed by OFORUNTIL is an induction variable and add both bounds
to the facts table. Currently this is done separately from findIndVar
because it depends on prove's factsTable, while findIndVar runs before
visiting blocks and building the factsTable.
Without any GOEXPERIMENT, this has no effect on std or cmd. However,
with GOEXPERIMENT=preemptibleloops, this change becomes necessary to
prove 90 conditions in std and cmd.
Change-Id: Ic025d669f81b53426309da5a6e8010e5ccaf4f49
Reviewed-on: https://go-review.googlesource.com/102603
Run-TryBot: Austin Clements <austin@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Keith Randall <khr@golang.org>
2018-03-23 17:38:55 -04:00
|
|
|
}
|
2024-06-14 22:29:09 -07:00
|
|
|
case OpSliceCap:
|
|
|
|
|
if v.Args[0].Op == OpSliceMake {
|
|
|
|
|
ft.update(b, v, v.Args[0].Args[2], signed, eq)
|
cmd/compile: detect OFORUNTIL inductive facts in prove
Currently, we compile range loops into for loops with the obvious
initialization and update of the index variable. In this form, the
prove pass can see that the body is dominated by an i < len condition,
and findIndVar can detect that i is an induction variable and that
0 <= i < len.
GOEXPERIMENT=preemptibleloops compiles range loops to OFORUNTIL and
we're preparing to unconditionally switch to a variation of this for
#24543. OFORUNTIL moves the increment and condition *after* the body,
which makes the bounds on the index variable much less obvious. With
OFORUNTIL, proving anything about the index variable requires
understanding the phi that joins the index values at the top of the
loop body block.
This interferes with both prove's ability to see that i < len (this is
true on both paths that enter the body, but from two different
conditional checks) and with findIndVar's ability to detect the
induction pattern.
Fix this by teaching prove to detect that the index in the pattern
constructed by OFORUNTIL is an induction variable and add both bounds
to the facts table. Currently this is done separately from findIndVar
because it depends on prove's factsTable, while findIndVar runs before
visiting blocks and building the factsTable.
Without any GOEXPERIMENT, this has no effect on std or cmd. However,
with GOEXPERIMENT=preemptibleloops, this change becomes necessary to
prove 90 conditions in std and cmd.
Change-Id: Ic025d669f81b53426309da5a6e8010e5ccaf4f49
Reviewed-on: https://go-review.googlesource.com/102603
Run-TryBot: Austin Clements <austin@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Keith Randall <khr@golang.org>
2018-03-23 17:38:55 -04:00
|
|
|
}
|
2024-10-26 14:19:32 -07:00
|
|
|
case OpPhi:
|
|
|
|
|
addLocalFactsPhi(ft, v)
|
cmd/compile: detect OFORUNTIL inductive facts in prove
Currently, we compile range loops into for loops with the obvious
initialization and update of the index variable. In this form, the
prove pass can see that the body is dominated by an i < len condition,
and findIndVar can detect that i is an induction variable and that
0 <= i < len.
GOEXPERIMENT=preemptibleloops compiles range loops to OFORUNTIL and
we're preparing to unconditionally switch to a variation of this for
#24543. OFORUNTIL moves the increment and condition *after* the body,
which makes the bounds on the index variable much less obvious. With
OFORUNTIL, proving anything about the index variable requires
understanding the phi that joins the index values at the top of the
loop body block.
This interferes with both prove's ability to see that i < len (this is
true on both paths that enter the body, but from two different
conditional checks) and with findIndVar's ability to detect the
induction pattern.
Fix this by teaching prove to detect that the index in the pattern
constructed by OFORUNTIL is an induction variable and add both bounds
to the facts table. Currently this is done separately from findIndVar
because it depends on prove's factsTable, while findIndVar runs before
visiting blocks and building the factsTable.
Without any GOEXPERIMENT, this has no effect on std or cmd. However,
with GOEXPERIMENT=preemptibleloops, this change becomes necessary to
prove 90 conditions in std and cmd.
Change-Id: Ic025d669f81b53426309da5a6e8010e5ccaf4f49
Reviewed-on: https://go-review.googlesource.com/102603
Run-TryBot: Austin Clements <austin@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Keith Randall <khr@golang.org>
2018-03-23 17:38:55 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-26 14:19:32 -07:00
|
|
|
func addLocalFactsPhi(ft *factsTable, v *Value) {
|
|
|
|
|
// Look for phis that implement min/max.
|
|
|
|
|
// z:
|
|
|
|
|
// c = Less64 x y (or other Less/Leq operation)
|
|
|
|
|
// If c -> bx by
|
|
|
|
|
// bx: <- z
|
|
|
|
|
// -> b ...
|
|
|
|
|
// by: <- z
|
|
|
|
|
// -> b ...
|
|
|
|
|
// b: <- bx by
|
|
|
|
|
// v = Phi x y
|
|
|
|
|
// Then v is either min or max of x,y.
|
|
|
|
|
// If it is the min, then we deduce v <= x && v <= y.
|
|
|
|
|
// If it is the max, then we deduce v >= x && v >= y.
|
|
|
|
|
// The min case is useful for the copy builtin, see issue 16833.
|
|
|
|
|
if len(v.Args) != 2 {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
b := v.Block
|
|
|
|
|
x := v.Args[0]
|
|
|
|
|
y := v.Args[1]
|
|
|
|
|
bx := b.Preds[0].b
|
|
|
|
|
by := b.Preds[1].b
|
|
|
|
|
var z *Block // branch point
|
|
|
|
|
switch {
|
|
|
|
|
case bx == by: // bx == by == z case
|
|
|
|
|
z = bx
|
|
|
|
|
case by.uniquePred() == bx: // bx == z case
|
|
|
|
|
z = bx
|
|
|
|
|
case bx.uniquePred() == by: // by == z case
|
|
|
|
|
z = by
|
|
|
|
|
case bx.uniquePred() == by.uniquePred():
|
|
|
|
|
z = bx.uniquePred()
|
|
|
|
|
}
|
|
|
|
|
if z == nil || z.Kind != BlockIf {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
c := z.Controls[0]
|
|
|
|
|
if len(c.Args) != 2 {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
var isMin bool // if c, a less-than comparison, is true, phi chooses x.
|
|
|
|
|
if bx == z {
|
|
|
|
|
isMin = b.Preds[0].i == 0
|
|
|
|
|
} else {
|
|
|
|
|
isMin = bx.Preds[0].i == 0
|
|
|
|
|
}
|
|
|
|
|
if c.Args[0] == x && c.Args[1] == y {
|
|
|
|
|
// ok
|
|
|
|
|
} else if c.Args[0] == y && c.Args[1] == x {
|
|
|
|
|
// Comparison is reversed from how the values are listed in the Phi.
|
|
|
|
|
isMin = !isMin
|
|
|
|
|
} else {
|
|
|
|
|
// Not comparing x and y.
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
var dom domain
|
|
|
|
|
switch c.Op {
|
|
|
|
|
case OpLess64, OpLess32, OpLess16, OpLess8, OpLeq64, OpLeq32, OpLeq16, OpLeq8:
|
|
|
|
|
dom = signed
|
|
|
|
|
case OpLess64U, OpLess32U, OpLess16U, OpLess8U, OpLeq64U, OpLeq32U, OpLeq16U, OpLeq8U:
|
|
|
|
|
dom = unsigned
|
|
|
|
|
default:
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
var rel relation
|
|
|
|
|
if isMin {
|
|
|
|
|
rel = lt | eq
|
|
|
|
|
} else {
|
|
|
|
|
rel = gt | eq
|
|
|
|
|
}
|
|
|
|
|
ft.update(b, v, x, dom, rel)
|
|
|
|
|
ft.update(b, v, y, dom, rel)
|
|
|
|
|
}
|
|
|
|
|
|
2018-04-25 11:52:06 -07:00
|
|
|
var ctzNonZeroOp = map[Op]Op{OpCtz8: OpCtz8NonZero, OpCtz16: OpCtz16NonZero, OpCtz32: OpCtz32NonZero, OpCtz64: OpCtz64NonZero}
|
2018-08-06 19:50:38 +10:00
|
|
|
var mostNegativeDividend = map[Op]int64{
|
|
|
|
|
OpDiv16: -1 << 15,
|
|
|
|
|
OpMod16: -1 << 15,
|
|
|
|
|
OpDiv32: -1 << 31,
|
|
|
|
|
OpMod32: -1 << 31,
|
|
|
|
|
OpDiv64: -1 << 63,
|
|
|
|
|
OpMod64: -1 << 63}
|
2018-04-25 11:52:06 -07:00
|
|
|
|
cmd/compile: make prove pass use unsatisfiability
Currently the prove pass uses implication queries. For each block, it
collects the set of branch conditions leading to that block, and
queries this fact table for whether any of these facts imply the
block's own branch condition (or its inverse). This works remarkably
well considering it doesn't do any deduction on these facts, but it
has various downsides:
1. It requires an implementation both of adding facts to the table and
determining implications. These are very nearly duals of each
other, but require separate implementations. Likewise, the process
of asserting facts of dominating branch conditions is very nearly
the dual of the process of querying implied branch conditions.
2. It leads to less effective use of derived facts. For example, the
prove pass currently derives facts about the relations between len
and cap, but can't make use of these unless a branch condition is
in the exact form of a derived fact. If one of these derived facts
contradicts another fact, it won't notice or make use of this.
This CL changes the approach of the prove pass to instead use
*contradiction* instead of implication. Rather than ever querying a
branch condition, it simply adds branch conditions to the fact table.
If this leads to a contradiction (specifically, it makes the fact set
unsatisfiable), that branch is impossible and can be cut. As a result,
1. We can eliminate the code for determining implications
(factsTable.get disappears entirely). Also, there is now a single
implementation of visiting and asserting branch conditions, since
we don't have to flip them around to treat them as facts in one
place and queries in another.
2. Derived facts can be used effectively. It doesn't matter *why* the
fact table is unsatisfiable; a contradiction in any of the facts is
enough.
3. As an added benefit, it's now quite easy to avoid traversing beyond
provably-unreachable blocks. In contrast, the current
implementation always visits all blocks.
The prove pass already has nearly all of the mechanism necessary to
compute unsatisfiability, which means this both simplifies the code
and makes it more powerful.
The only complication is that the current implication procedure has a
hack for dealing with the 0 <= Args[0] condition of OpIsInBounds and
OpIsSliceInBounds. We replace this with asserting the appropriate fact
when we process one of these conditions. This seems much cleaner
anyway, and works because we can now take advantage of derived facts.
This has no measurable effect on compiler performance.
Effectiveness:
There is exactly one condition in all of std and cmd that this fails
to prove that the old implementation could: (int64(^uint(0)>>1) < x)
in encoding/gob. This can never be true because x is an int, and it's
basically coincidence that the old code gets this. (For example, it
fails to prove the similar (x < ^int64(^uint(0)>>1)) condition that
immediately precedes it, and even though the conditions are logically
unrelated, it wouldn't get the second one if it hadn't first processed
the first!)
It does, however, prove a few dozen additional branches. These come
from facts that are added to the fact table about the relations
between len and cap. These were almost never queried directly before,
but could lead to contradictions, which the unsat-based approach is
able to use.
There are exactly two branches in std and cmd that this implementation
proves in the *other* direction. This sounds scary, but is okay
because both occur in already-unreachable blocks, so it doesn't matter
what we chose. Because the fact table logic is sound but incomplete,
it fails to prove that the block isn't reachable, even though it is
able to prove that both outgoing branches are impossible. We could
turn these blocks into BlockExit blocks, but it doesn't seem worth the
trouble of the extra proof effort for something that happens twice in
all of std and cmd.
Tests:
This CL updates test/prove.go to change the expected messages because
it can no longer give a "reason" why it proved or disproved a
condition. It also adds a new test of a branch it couldn't prove
before.
It mostly guts test/sliceopt.go, removing everything related to slice
bounds optimizations and moving a few relevant tests to test/prove.go.
Much of this test is actually unreachable. The new prove pass figures
this out and doesn't try to prove anything about the unreachable
parts. The output on the unreachable parts is already suspect because
anything can be proved at that point, so it's really just a regression
test for an algorithm the compiler no longer uses.
This is a step toward fixing #23354. That issue is quite easy to fix
once we can use derived facts effectively.
Change-Id: Ia48a1b9ee081310579fe474e4a61857424ff8ce8
Reviewed-on: https://go-review.googlesource.com/87478
Reviewed-by: Keith Randall <khr@golang.org>
2018-01-10 16:28:58 -05:00
|
|
|
// simplifyBlock simplifies some constant values in b and evaluates
|
|
|
|
|
// branches to non-uniquely dominated successors of b.
|
|
|
|
|
func simplifyBlock(sdom SparseTree, ft *factsTable, b *Block) {
|
2016-10-25 15:49:52 -07:00
|
|
|
for _, v := range b.Values {
|
2018-04-25 11:52:06 -07:00
|
|
|
switch v.Op {
|
|
|
|
|
case OpSlicemask:
|
|
|
|
|
// Replace OpSlicemask operations in b with constants where possible.
|
|
|
|
|
x, delta := isConstDelta(v.Args[0])
|
|
|
|
|
if x == nil {
|
2022-05-04 17:03:13 +00:00
|
|
|
break
|
2018-04-25 11:52:06 -07:00
|
|
|
}
|
|
|
|
|
// slicemask(x + y)
|
|
|
|
|
// if x is larger than -y (y is negative), then slicemask is -1.
|
2024-05-18 08:01:27 -07:00
|
|
|
lim := ft.limits[x.ID]
|
2018-04-25 11:52:06 -07:00
|
|
|
if lim.umin > uint64(-delta) {
|
|
|
|
|
if v.Args[0].Op == OpAdd64 {
|
|
|
|
|
v.reset(OpConst64)
|
|
|
|
|
} else {
|
|
|
|
|
v.reset(OpConst32)
|
|
|
|
|
}
|
|
|
|
|
if b.Func.pass.debug > 0 {
|
|
|
|
|
b.Func.Warnl(v.Pos, "Proved slicemask not needed")
|
|
|
|
|
}
|
|
|
|
|
v.AuxInt = -1
|
|
|
|
|
}
|
|
|
|
|
case OpCtz8, OpCtz16, OpCtz32, OpCtz64:
|
|
|
|
|
// On some architectures, notably amd64, we can generate much better
|
|
|
|
|
// code for CtzNN if we know that the argument is non-zero.
|
|
|
|
|
// Capture that information here for use in arch-specific optimizations.
|
|
|
|
|
x := v.Args[0]
|
2024-05-18 08:01:27 -07:00
|
|
|
lim := ft.limits[x.ID]
|
2018-04-25 11:52:06 -07:00
|
|
|
if lim.umin > 0 || lim.min > 0 || lim.max < 0 {
|
2018-04-26 15:46:12 -07:00
|
|
|
if b.Func.pass.debug > 0 {
|
|
|
|
|
b.Func.Warnl(v.Pos, "Proved %v non-zero", v.Op)
|
|
|
|
|
}
|
2018-04-25 11:52:06 -07:00
|
|
|
v.Op = ctzNonZeroOp[v.Op]
|
2016-10-25 15:49:52 -07:00
|
|
|
}
|
2020-05-07 13:44:51 -07:00
|
|
|
case OpRsh8x8, OpRsh8x16, OpRsh8x32, OpRsh8x64,
|
|
|
|
|
OpRsh16x8, OpRsh16x16, OpRsh16x32, OpRsh16x64,
|
|
|
|
|
OpRsh32x8, OpRsh32x16, OpRsh32x32, OpRsh32x64,
|
|
|
|
|
OpRsh64x8, OpRsh64x16, OpRsh64x32, OpRsh64x64:
|
|
|
|
|
// Check whether, for a >> b, we know that a is non-negative
|
|
|
|
|
// and b is all of a's bits except the MSB. If so, a is shifted to zero.
|
2024-06-27 15:53:24 -07:00
|
|
|
bits := 8 * v.Args[0].Type.Size()
|
2020-05-07 13:44:51 -07:00
|
|
|
if v.Args[1].isGenericIntConst() && v.Args[1].AuxInt >= bits-1 && ft.isNonNegative(v.Args[0]) {
|
|
|
|
|
if b.Func.pass.debug > 0 {
|
|
|
|
|
b.Func.Warnl(v.Pos, "Proved %v shifts to zero", v.Op)
|
|
|
|
|
}
|
|
|
|
|
switch bits {
|
|
|
|
|
case 64:
|
|
|
|
|
v.reset(OpConst64)
|
|
|
|
|
case 32:
|
|
|
|
|
v.reset(OpConst32)
|
|
|
|
|
case 16:
|
|
|
|
|
v.reset(OpConst16)
|
|
|
|
|
case 8:
|
|
|
|
|
v.reset(OpConst8)
|
|
|
|
|
default:
|
|
|
|
|
panic("unexpected integer size")
|
|
|
|
|
}
|
|
|
|
|
v.AuxInt = 0
|
2022-05-04 17:03:13 +00:00
|
|
|
break // Be sure not to fallthrough - this is no longer OpRsh.
|
2020-05-07 13:44:51 -07:00
|
|
|
}
|
|
|
|
|
// If the Rsh hasn't been replaced with 0, still check if it is bounded.
|
|
|
|
|
fallthrough
|
2018-04-26 20:56:03 -07:00
|
|
|
case OpLsh8x8, OpLsh8x16, OpLsh8x32, OpLsh8x64,
|
|
|
|
|
OpLsh16x8, OpLsh16x16, OpLsh16x32, OpLsh16x64,
|
|
|
|
|
OpLsh32x8, OpLsh32x16, OpLsh32x32, OpLsh32x64,
|
|
|
|
|
OpLsh64x8, OpLsh64x16, OpLsh64x32, OpLsh64x64,
|
|
|
|
|
OpRsh8Ux8, OpRsh8Ux16, OpRsh8Ux32, OpRsh8Ux64,
|
|
|
|
|
OpRsh16Ux8, OpRsh16Ux16, OpRsh16Ux32, OpRsh16Ux64,
|
|
|
|
|
OpRsh32Ux8, OpRsh32Ux16, OpRsh32Ux32, OpRsh32Ux64,
|
|
|
|
|
OpRsh64Ux8, OpRsh64Ux16, OpRsh64Ux32, OpRsh64Ux64:
|
|
|
|
|
// Check whether, for a << b, we know that b
|
|
|
|
|
// is strictly less than the number of bits in a.
|
|
|
|
|
by := v.Args[1]
|
2024-05-18 08:01:27 -07:00
|
|
|
lim := ft.limits[by.ID]
|
2018-04-26 20:56:03 -07:00
|
|
|
bits := 8 * v.Args[0].Type.Size()
|
|
|
|
|
if lim.umax < uint64(bits) || (lim.max < bits && ft.isNonNegative(by)) {
|
2018-04-29 17:40:47 -07:00
|
|
|
v.AuxInt = 1 // see shiftIsBounded
|
2024-06-14 22:29:09 -07:00
|
|
|
if b.Func.pass.debug > 0 && !by.isGenericIntConst() {
|
2018-04-26 20:56:03 -07:00
|
|
|
b.Func.Warnl(v.Pos, "Proved %v bounded", v.Op)
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-08-06 19:50:38 +10:00
|
|
|
case OpDiv16, OpDiv32, OpDiv64, OpMod16, OpMod32, OpMod64:
|
|
|
|
|
// On amd64 and 386 fix-up code can be avoided if we know
|
|
|
|
|
// the divisor is not -1 or the dividend > MinIntNN.
|
2020-02-24 13:53:53 -08:00
|
|
|
// Don't modify AuxInt on other architectures,
|
|
|
|
|
// as that can interfere with CSE.
|
|
|
|
|
// TODO: add other architectures?
|
|
|
|
|
if b.Func.Config.arch != "386" && b.Func.Config.arch != "amd64" {
|
|
|
|
|
break
|
|
|
|
|
}
|
2018-08-06 19:50:38 +10:00
|
|
|
divr := v.Args[1]
|
2024-05-18 08:01:27 -07:00
|
|
|
divrLim := ft.limits[divr.ID]
|
2018-08-06 19:50:38 +10:00
|
|
|
divd := v.Args[0]
|
2024-05-18 08:01:27 -07:00
|
|
|
divdLim := ft.limits[divd.ID]
|
2024-07-11 19:38:33 -07:00
|
|
|
if divrLim.max < -1 || divrLim.min > -1 || divdLim.min > mostNegativeDividend[v.Op] {
|
2020-01-23 22:18:30 -08:00
|
|
|
// See DivisionNeedsFixUp in rewrite.go.
|
|
|
|
|
// v.AuxInt = 1 means we have proved both that the divisor is not -1
|
|
|
|
|
// and that the dividend is not the most negative integer,
|
|
|
|
|
// so we do not need to add fix-up code.
|
|
|
|
|
v.AuxInt = 1
|
2018-08-06 19:50:38 +10:00
|
|
|
if b.Func.pass.debug > 0 {
|
|
|
|
|
b.Func.Warnl(v.Pos, "Proved %v does not need fix-up", v.Op)
|
|
|
|
|
}
|
|
|
|
|
}
|
2016-10-25 15:49:52 -07:00
|
|
|
}
|
2022-05-04 17:03:13 +00:00
|
|
|
// Fold provable constant results.
|
|
|
|
|
// Helps in cases where we reuse a value after branching on its equality.
|
|
|
|
|
for i, arg := range v.Args {
|
2024-05-18 08:01:27 -07:00
|
|
|
lim := ft.limits[arg.ID]
|
2022-05-04 17:03:13 +00:00
|
|
|
var constValue int64
|
|
|
|
|
switch {
|
|
|
|
|
case lim.min == lim.max:
|
|
|
|
|
constValue = lim.min
|
|
|
|
|
case lim.umin == lim.umax:
|
2024-07-11 19:38:33 -07:00
|
|
|
constValue = int64(lim.umin)
|
2022-05-04 17:03:13 +00:00
|
|
|
default:
|
|
|
|
|
continue
|
|
|
|
|
}
|
2024-07-11 19:38:33 -07:00
|
|
|
switch arg.Op {
|
|
|
|
|
case OpConst64, OpConst32, OpConst16, OpConst8, OpConstBool, OpConstNil:
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
typ := arg.Type
|
2022-05-04 17:03:13 +00:00
|
|
|
f := b.Func
|
2024-07-11 19:38:33 -07:00
|
|
|
var c *Value
|
|
|
|
|
switch {
|
|
|
|
|
case typ.IsBoolean():
|
|
|
|
|
c = f.ConstBool(typ, constValue != 0)
|
|
|
|
|
case typ.IsInteger() && typ.Size() == 1:
|
2022-05-04 17:03:13 +00:00
|
|
|
c = f.ConstInt8(typ, int8(constValue))
|
2024-07-11 19:38:33 -07:00
|
|
|
case typ.IsInteger() && typ.Size() == 2:
|
|
|
|
|
c = f.ConstInt16(typ, int16(constValue))
|
|
|
|
|
case typ.IsInteger() && typ.Size() == 4:
|
|
|
|
|
c = f.ConstInt32(typ, int32(constValue))
|
|
|
|
|
case typ.IsInteger() && typ.Size() == 8:
|
|
|
|
|
c = f.ConstInt64(typ, constValue)
|
|
|
|
|
case typ.IsPtrShaped():
|
|
|
|
|
if constValue == 0 {
|
|
|
|
|
c = f.ConstNil(typ)
|
|
|
|
|
} else {
|
|
|
|
|
// Not sure how this might happen, but if it
|
|
|
|
|
// does, just skip it.
|
|
|
|
|
continue
|
|
|
|
|
}
|
2022-05-04 17:03:13 +00:00
|
|
|
default:
|
2024-07-11 19:38:33 -07:00
|
|
|
// Not sure how this might happen, but if it
|
|
|
|
|
// does, just skip it.
|
|
|
|
|
continue
|
2022-05-04 17:03:13 +00:00
|
|
|
}
|
|
|
|
|
v.SetArg(i, c)
|
2024-11-03 12:10:26 +08:00
|
|
|
ft.initLimitForNewValue(c)
|
2022-05-04 17:03:13 +00:00
|
|
|
if b.Func.pass.debug > 1 {
|
|
|
|
|
b.Func.Warnl(v.Pos, "Proved %v's arg %d (%v) is constant %d", v, i, arg, constValue)
|
|
|
|
|
}
|
|
|
|
|
}
|
2016-10-25 15:49:52 -07:00
|
|
|
}
|
|
|
|
|
|
2016-02-19 12:14:42 +01:00
|
|
|
if b.Kind != BlockIf {
|
cmd/compile: make prove pass use unsatisfiability
Currently the prove pass uses implication queries. For each block, it
collects the set of branch conditions leading to that block, and
queries this fact table for whether any of these facts imply the
block's own branch condition (or its inverse). This works remarkably
well considering it doesn't do any deduction on these facts, but it
has various downsides:
1. It requires an implementation both of adding facts to the table and
determining implications. These are very nearly duals of each
other, but require separate implementations. Likewise, the process
of asserting facts of dominating branch conditions is very nearly
the dual of the process of querying implied branch conditions.
2. It leads to less effective use of derived facts. For example, the
prove pass currently derives facts about the relations between len
and cap, but can't make use of these unless a branch condition is
in the exact form of a derived fact. If one of these derived facts
contradicts another fact, it won't notice or make use of this.
This CL changes the approach of the prove pass to instead use
*contradiction* instead of implication. Rather than ever querying a
branch condition, it simply adds branch conditions to the fact table.
If this leads to a contradiction (specifically, it makes the fact set
unsatisfiable), that branch is impossible and can be cut. As a result,
1. We can eliminate the code for determining implications
(factsTable.get disappears entirely). Also, there is now a single
implementation of visiting and asserting branch conditions, since
we don't have to flip them around to treat them as facts in one
place and queries in another.
2. Derived facts can be used effectively. It doesn't matter *why* the
fact table is unsatisfiable; a contradiction in any of the facts is
enough.
3. As an added benefit, it's now quite easy to avoid traversing beyond
provably-unreachable blocks. In contrast, the current
implementation always visits all blocks.
The prove pass already has nearly all of the mechanism necessary to
compute unsatisfiability, which means this both simplifies the code
and makes it more powerful.
The only complication is that the current implication procedure has a
hack for dealing with the 0 <= Args[0] condition of OpIsInBounds and
OpIsSliceInBounds. We replace this with asserting the appropriate fact
when we process one of these conditions. This seems much cleaner
anyway, and works because we can now take advantage of derived facts.
This has no measurable effect on compiler performance.
Effectiveness:
There is exactly one condition in all of std and cmd that this fails
to prove that the old implementation could: (int64(^uint(0)>>1) < x)
in encoding/gob. This can never be true because x is an int, and it's
basically coincidence that the old code gets this. (For example, it
fails to prove the similar (x < ^int64(^uint(0)>>1)) condition that
immediately precedes it, and even though the conditions are logically
unrelated, it wouldn't get the second one if it hadn't first processed
the first!)
It does, however, prove a few dozen additional branches. These come
from facts that are added to the fact table about the relations
between len and cap. These were almost never queried directly before,
but could lead to contradictions, which the unsat-based approach is
able to use.
There are exactly two branches in std and cmd that this implementation
proves in the *other* direction. This sounds scary, but is okay
because both occur in already-unreachable blocks, so it doesn't matter
what we chose. Because the fact table logic is sound but incomplete,
it fails to prove that the block isn't reachable, even though it is
able to prove that both outgoing branches are impossible. We could
turn these blocks into BlockExit blocks, but it doesn't seem worth the
trouble of the extra proof effort for something that happens twice in
all of std and cmd.
Tests:
This CL updates test/prove.go to change the expected messages because
it can no longer give a "reason" why it proved or disproved a
condition. It also adds a new test of a branch it couldn't prove
before.
It mostly guts test/sliceopt.go, removing everything related to slice
bounds optimizations and moving a few relevant tests to test/prove.go.
Much of this test is actually unreachable. The new prove pass figures
this out and doesn't try to prove anything about the unreachable
parts. The output on the unreachable parts is already suspect because
anything can be proved at that point, so it's really just a regression
test for an algorithm the compiler no longer uses.
This is a step toward fixing #23354. That issue is quite easy to fix
once we can use derived facts effectively.
Change-Id: Ia48a1b9ee081310579fe474e4a61857424ff8ce8
Reviewed-on: https://go-review.googlesource.com/87478
Reviewed-by: Keith Randall <khr@golang.org>
2018-01-10 16:28:58 -05:00
|
|
|
return
|
2016-02-19 12:14:42 +01:00
|
|
|
}
|
|
|
|
|
|
cmd/compile: make prove pass use unsatisfiability
Currently the prove pass uses implication queries. For each block, it
collects the set of branch conditions leading to that block, and
queries this fact table for whether any of these facts imply the
block's own branch condition (or its inverse). This works remarkably
well considering it doesn't do any deduction on these facts, but it
has various downsides:
1. It requires an implementation both of adding facts to the table and
determining implications. These are very nearly duals of each
other, but require separate implementations. Likewise, the process
of asserting facts of dominating branch conditions is very nearly
the dual of the process of querying implied branch conditions.
2. It leads to less effective use of derived facts. For example, the
prove pass currently derives facts about the relations between len
and cap, but can't make use of these unless a branch condition is
in the exact form of a derived fact. If one of these derived facts
contradicts another fact, it won't notice or make use of this.
This CL changes the approach of the prove pass to instead use
*contradiction* instead of implication. Rather than ever querying a
branch condition, it simply adds branch conditions to the fact table.
If this leads to a contradiction (specifically, it makes the fact set
unsatisfiable), that branch is impossible and can be cut. As a result,
1. We can eliminate the code for determining implications
(factsTable.get disappears entirely). Also, there is now a single
implementation of visiting and asserting branch conditions, since
we don't have to flip them around to treat them as facts in one
place and queries in another.
2. Derived facts can be used effectively. It doesn't matter *why* the
fact table is unsatisfiable; a contradiction in any of the facts is
enough.
3. As an added benefit, it's now quite easy to avoid traversing beyond
provably-unreachable blocks. In contrast, the current
implementation always visits all blocks.
The prove pass already has nearly all of the mechanism necessary to
compute unsatisfiability, which means this both simplifies the code
and makes it more powerful.
The only complication is that the current implication procedure has a
hack for dealing with the 0 <= Args[0] condition of OpIsInBounds and
OpIsSliceInBounds. We replace this with asserting the appropriate fact
when we process one of these conditions. This seems much cleaner
anyway, and works because we can now take advantage of derived facts.
This has no measurable effect on compiler performance.
Effectiveness:
There is exactly one condition in all of std and cmd that this fails
to prove that the old implementation could: (int64(^uint(0)>>1) < x)
in encoding/gob. This can never be true because x is an int, and it's
basically coincidence that the old code gets this. (For example, it
fails to prove the similar (x < ^int64(^uint(0)>>1)) condition that
immediately precedes it, and even though the conditions are logically
unrelated, it wouldn't get the second one if it hadn't first processed
the first!)
It does, however, prove a few dozen additional branches. These come
from facts that are added to the fact table about the relations
between len and cap. These were almost never queried directly before,
but could lead to contradictions, which the unsat-based approach is
able to use.
There are exactly two branches in std and cmd that this implementation
proves in the *other* direction. This sounds scary, but is okay
because both occur in already-unreachable blocks, so it doesn't matter
what we chose. Because the fact table logic is sound but incomplete,
it fails to prove that the block isn't reachable, even though it is
able to prove that both outgoing branches are impossible. We could
turn these blocks into BlockExit blocks, but it doesn't seem worth the
trouble of the extra proof effort for something that happens twice in
all of std and cmd.
Tests:
This CL updates test/prove.go to change the expected messages because
it can no longer give a "reason" why it proved or disproved a
condition. It also adds a new test of a branch it couldn't prove
before.
It mostly guts test/sliceopt.go, removing everything related to slice
bounds optimizations and moving a few relevant tests to test/prove.go.
Much of this test is actually unreachable. The new prove pass figures
this out and doesn't try to prove anything about the unreachable
parts. The output on the unreachable parts is already suspect because
anything can be proved at that point, so it's really just a regression
test for an algorithm the compiler no longer uses.
This is a step toward fixing #23354. That issue is quite easy to fix
once we can use derived facts effectively.
Change-Id: Ia48a1b9ee081310579fe474e4a61857424ff8ce8
Reviewed-on: https://go-review.googlesource.com/87478
Reviewed-by: Keith Randall <khr@golang.org>
2018-01-10 16:28:58 -05:00
|
|
|
// Consider outgoing edges from this block.
|
|
|
|
|
parent := b
|
|
|
|
|
for i, branch := range [...]branch{positive, negative} {
|
|
|
|
|
child := parent.Succs[i].b
|
|
|
|
|
if getBranch(sdom, parent, child) != unknown {
|
|
|
|
|
// For edges to uniquely dominated blocks, we
|
|
|
|
|
// already did this when we visited the child.
|
|
|
|
|
continue
|
2016-03-07 18:36:16 +01:00
|
|
|
}
|
cmd/compile: make prove pass use unsatisfiability
Currently the prove pass uses implication queries. For each block, it
collects the set of branch conditions leading to that block, and
queries this fact table for whether any of these facts imply the
block's own branch condition (or its inverse). This works remarkably
well considering it doesn't do any deduction on these facts, but it
has various downsides:
1. It requires an implementation both of adding facts to the table and
determining implications. These are very nearly duals of each
other, but require separate implementations. Likewise, the process
of asserting facts of dominating branch conditions is very nearly
the dual of the process of querying implied branch conditions.
2. It leads to less effective use of derived facts. For example, the
prove pass currently derives facts about the relations between len
and cap, but can't make use of these unless a branch condition is
in the exact form of a derived fact. If one of these derived facts
contradicts another fact, it won't notice or make use of this.
This CL changes the approach of the prove pass to instead use
*contradiction* instead of implication. Rather than ever querying a
branch condition, it simply adds branch conditions to the fact table.
If this leads to a contradiction (specifically, it makes the fact set
unsatisfiable), that branch is impossible and can be cut. As a result,
1. We can eliminate the code for determining implications
(factsTable.get disappears entirely). Also, there is now a single
implementation of visiting and asserting branch conditions, since
we don't have to flip them around to treat them as facts in one
place and queries in another.
2. Derived facts can be used effectively. It doesn't matter *why* the
fact table is unsatisfiable; a contradiction in any of the facts is
enough.
3. As an added benefit, it's now quite easy to avoid traversing beyond
provably-unreachable blocks. In contrast, the current
implementation always visits all blocks.
The prove pass already has nearly all of the mechanism necessary to
compute unsatisfiability, which means this both simplifies the code
and makes it more powerful.
The only complication is that the current implication procedure has a
hack for dealing with the 0 <= Args[0] condition of OpIsInBounds and
OpIsSliceInBounds. We replace this with asserting the appropriate fact
when we process one of these conditions. This seems much cleaner
anyway, and works because we can now take advantage of derived facts.
This has no measurable effect on compiler performance.
Effectiveness:
There is exactly one condition in all of std and cmd that this fails
to prove that the old implementation could: (int64(^uint(0)>>1) < x)
in encoding/gob. This can never be true because x is an int, and it's
basically coincidence that the old code gets this. (For example, it
fails to prove the similar (x < ^int64(^uint(0)>>1)) condition that
immediately precedes it, and even though the conditions are logically
unrelated, it wouldn't get the second one if it hadn't first processed
the first!)
It does, however, prove a few dozen additional branches. These come
from facts that are added to the fact table about the relations
between len and cap. These were almost never queried directly before,
but could lead to contradictions, which the unsat-based approach is
able to use.
There are exactly two branches in std and cmd that this implementation
proves in the *other* direction. This sounds scary, but is okay
because both occur in already-unreachable blocks, so it doesn't matter
what we chose. Because the fact table logic is sound but incomplete,
it fails to prove that the block isn't reachable, even though it is
able to prove that both outgoing branches are impossible. We could
turn these blocks into BlockExit blocks, but it doesn't seem worth the
trouble of the extra proof effort for something that happens twice in
all of std and cmd.
Tests:
This CL updates test/prove.go to change the expected messages because
it can no longer give a "reason" why it proved or disproved a
condition. It also adds a new test of a branch it couldn't prove
before.
It mostly guts test/sliceopt.go, removing everything related to slice
bounds optimizations and moving a few relevant tests to test/prove.go.
Much of this test is actually unreachable. The new prove pass figures
this out and doesn't try to prove anything about the unreachable
parts. The output on the unreachable parts is already suspect because
anything can be proved at that point, so it's really just a regression
test for an algorithm the compiler no longer uses.
This is a step toward fixing #23354. That issue is quite easy to fix
once we can use derived facts effectively.
Change-Id: Ia48a1b9ee081310579fe474e4a61857424ff8ce8
Reviewed-on: https://go-review.googlesource.com/87478
Reviewed-by: Keith Randall <khr@golang.org>
2018-01-10 16:28:58 -05:00
|
|
|
// For edges to other blocks, this can trim a branch
|
|
|
|
|
// even if we couldn't get rid of the child itself.
|
2018-04-02 01:39:03 +02:00
|
|
|
ft.checkpoint()
|
|
|
|
|
addBranchRestrictions(ft, parent, branch)
|
|
|
|
|
unsat := ft.unsat
|
|
|
|
|
ft.restore()
|
|
|
|
|
if unsat {
|
cmd/compile: make prove pass use unsatisfiability
Currently the prove pass uses implication queries. For each block, it
collects the set of branch conditions leading to that block, and
queries this fact table for whether any of these facts imply the
block's own branch condition (or its inverse). This works remarkably
well considering it doesn't do any deduction on these facts, but it
has various downsides:
1. It requires an implementation both of adding facts to the table and
determining implications. These are very nearly duals of each
other, but require separate implementations. Likewise, the process
of asserting facts of dominating branch conditions is very nearly
the dual of the process of querying implied branch conditions.
2. It leads to less effective use of derived facts. For example, the
prove pass currently derives facts about the relations between len
and cap, but can't make use of these unless a branch condition is
in the exact form of a derived fact. If one of these derived facts
contradicts another fact, it won't notice or make use of this.
This CL changes the approach of the prove pass to instead use
*contradiction* instead of implication. Rather than ever querying a
branch condition, it simply adds branch conditions to the fact table.
If this leads to a contradiction (specifically, it makes the fact set
unsatisfiable), that branch is impossible and can be cut. As a result,
1. We can eliminate the code for determining implications
(factsTable.get disappears entirely). Also, there is now a single
implementation of visiting and asserting branch conditions, since
we don't have to flip them around to treat them as facts in one
place and queries in another.
2. Derived facts can be used effectively. It doesn't matter *why* the
fact table is unsatisfiable; a contradiction in any of the facts is
enough.
3. As an added benefit, it's now quite easy to avoid traversing beyond
provably-unreachable blocks. In contrast, the current
implementation always visits all blocks.
The prove pass already has nearly all of the mechanism necessary to
compute unsatisfiability, which means this both simplifies the code
and makes it more powerful.
The only complication is that the current implication procedure has a
hack for dealing with the 0 <= Args[0] condition of OpIsInBounds and
OpIsSliceInBounds. We replace this with asserting the appropriate fact
when we process one of these conditions. This seems much cleaner
anyway, and works because we can now take advantage of derived facts.
This has no measurable effect on compiler performance.
Effectiveness:
There is exactly one condition in all of std and cmd that this fails
to prove that the old implementation could: (int64(^uint(0)>>1) < x)
in encoding/gob. This can never be true because x is an int, and it's
basically coincidence that the old code gets this. (For example, it
fails to prove the similar (x < ^int64(^uint(0)>>1)) condition that
immediately precedes it, and even though the conditions are logically
unrelated, it wouldn't get the second one if it hadn't first processed
the first!)
It does, however, prove a few dozen additional branches. These come
from facts that are added to the fact table about the relations
between len and cap. These were almost never queried directly before,
but could lead to contradictions, which the unsat-based approach is
able to use.
There are exactly two branches in std and cmd that this implementation
proves in the *other* direction. This sounds scary, but is okay
because both occur in already-unreachable blocks, so it doesn't matter
what we chose. Because the fact table logic is sound but incomplete,
it fails to prove that the block isn't reachable, even though it is
able to prove that both outgoing branches are impossible. We could
turn these blocks into BlockExit blocks, but it doesn't seem worth the
trouble of the extra proof effort for something that happens twice in
all of std and cmd.
Tests:
This CL updates test/prove.go to change the expected messages because
it can no longer give a "reason" why it proved or disproved a
condition. It also adds a new test of a branch it couldn't prove
before.
It mostly guts test/sliceopt.go, removing everything related to slice
bounds optimizations and moving a few relevant tests to test/prove.go.
Much of this test is actually unreachable. The new prove pass figures
this out and doesn't try to prove anything about the unreachable
parts. The output on the unreachable parts is already suspect because
anything can be proved at that point, so it's really just a regression
test for an algorithm the compiler no longer uses.
This is a step toward fixing #23354. That issue is quite easy to fix
once we can use derived facts effectively.
Change-Id: Ia48a1b9ee081310579fe474e4a61857424ff8ce8
Reviewed-on: https://go-review.googlesource.com/87478
Reviewed-by: Keith Randall <khr@golang.org>
2018-01-10 16:28:58 -05:00
|
|
|
// This branch is impossible, so remove it
|
|
|
|
|
// from the block.
|
|
|
|
|
removeBranch(parent, branch)
|
|
|
|
|
// No point in considering the other branch.
|
|
|
|
|
// (It *is* possible for both to be
|
|
|
|
|
// unsatisfiable since the fact table is
|
|
|
|
|
// incomplete. We could turn this into a
|
|
|
|
|
// BlockExit, but it doesn't seem worth it.)
|
|
|
|
|
break
|
2016-03-07 18:36:16 +01:00
|
|
|
}
|
2016-02-19 12:14:42 +01:00
|
|
|
}
|
cmd/compile: make prove pass use unsatisfiability
Currently the prove pass uses implication queries. For each block, it
collects the set of branch conditions leading to that block, and
queries this fact table for whether any of these facts imply the
block's own branch condition (or its inverse). This works remarkably
well considering it doesn't do any deduction on these facts, but it
has various downsides:
1. It requires an implementation both of adding facts to the table and
determining implications. These are very nearly duals of each
other, but require separate implementations. Likewise, the process
of asserting facts of dominating branch conditions is very nearly
the dual of the process of querying implied branch conditions.
2. It leads to less effective use of derived facts. For example, the
prove pass currently derives facts about the relations between len
and cap, but can't make use of these unless a branch condition is
in the exact form of a derived fact. If one of these derived facts
contradicts another fact, it won't notice or make use of this.
This CL changes the approach of the prove pass to instead use
*contradiction* instead of implication. Rather than ever querying a
branch condition, it simply adds branch conditions to the fact table.
If this leads to a contradiction (specifically, it makes the fact set
unsatisfiable), that branch is impossible and can be cut. As a result,
1. We can eliminate the code for determining implications
(factsTable.get disappears entirely). Also, there is now a single
implementation of visiting and asserting branch conditions, since
we don't have to flip them around to treat them as facts in one
place and queries in another.
2. Derived facts can be used effectively. It doesn't matter *why* the
fact table is unsatisfiable; a contradiction in any of the facts is
enough.
3. As an added benefit, it's now quite easy to avoid traversing beyond
provably-unreachable blocks. In contrast, the current
implementation always visits all blocks.
The prove pass already has nearly all of the mechanism necessary to
compute unsatisfiability, which means this both simplifies the code
and makes it more powerful.
The only complication is that the current implication procedure has a
hack for dealing with the 0 <= Args[0] condition of OpIsInBounds and
OpIsSliceInBounds. We replace this with asserting the appropriate fact
when we process one of these conditions. This seems much cleaner
anyway, and works because we can now take advantage of derived facts.
This has no measurable effect on compiler performance.
Effectiveness:
There is exactly one condition in all of std and cmd that this fails
to prove that the old implementation could: (int64(^uint(0)>>1) < x)
in encoding/gob. This can never be true because x is an int, and it's
basically coincidence that the old code gets this. (For example, it
fails to prove the similar (x < ^int64(^uint(0)>>1)) condition that
immediately precedes it, and even though the conditions are logically
unrelated, it wouldn't get the second one if it hadn't first processed
the first!)
It does, however, prove a few dozen additional branches. These come
from facts that are added to the fact table about the relations
between len and cap. These were almost never queried directly before,
but could lead to contradictions, which the unsat-based approach is
able to use.
There are exactly two branches in std and cmd that this implementation
proves in the *other* direction. This sounds scary, but is okay
because both occur in already-unreachable blocks, so it doesn't matter
what we chose. Because the fact table logic is sound but incomplete,
it fails to prove that the block isn't reachable, even though it is
able to prove that both outgoing branches are impossible. We could
turn these blocks into BlockExit blocks, but it doesn't seem worth the
trouble of the extra proof effort for something that happens twice in
all of std and cmd.
Tests:
This CL updates test/prove.go to change the expected messages because
it can no longer give a "reason" why it proved or disproved a
condition. It also adds a new test of a branch it couldn't prove
before.
It mostly guts test/sliceopt.go, removing everything related to slice
bounds optimizations and moving a few relevant tests to test/prove.go.
Much of this test is actually unreachable. The new prove pass figures
this out and doesn't try to prove anything about the unreachable
parts. The output on the unreachable parts is already suspect because
anything can be proved at that point, so it's really just a regression
test for an algorithm the compiler no longer uses.
This is a step toward fixing #23354. That issue is quite easy to fix
once we can use derived facts effectively.
Change-Id: Ia48a1b9ee081310579fe474e4a61857424ff8ce8
Reviewed-on: https://go-review.googlesource.com/87478
Reviewed-by: Keith Randall <khr@golang.org>
2018-01-10 16:28:58 -05:00
|
|
|
}
|
2016-02-19 12:14:42 +01:00
|
|
|
|
cmd/compile: make prove pass use unsatisfiability
Currently the prove pass uses implication queries. For each block, it
collects the set of branch conditions leading to that block, and
queries this fact table for whether any of these facts imply the
block's own branch condition (or its inverse). This works remarkably
well considering it doesn't do any deduction on these facts, but it
has various downsides:
1. It requires an implementation both of adding facts to the table and
determining implications. These are very nearly duals of each
other, but require separate implementations. Likewise, the process
of asserting facts of dominating branch conditions is very nearly
the dual of the process of querying implied branch conditions.
2. It leads to less effective use of derived facts. For example, the
prove pass currently derives facts about the relations between len
and cap, but can't make use of these unless a branch condition is
in the exact form of a derived fact. If one of these derived facts
contradicts another fact, it won't notice or make use of this.
This CL changes the approach of the prove pass to instead use
*contradiction* instead of implication. Rather than ever querying a
branch condition, it simply adds branch conditions to the fact table.
If this leads to a contradiction (specifically, it makes the fact set
unsatisfiable), that branch is impossible and can be cut. As a result,
1. We can eliminate the code for determining implications
(factsTable.get disappears entirely). Also, there is now a single
implementation of visiting and asserting branch conditions, since
we don't have to flip them around to treat them as facts in one
place and queries in another.
2. Derived facts can be used effectively. It doesn't matter *why* the
fact table is unsatisfiable; a contradiction in any of the facts is
enough.
3. As an added benefit, it's now quite easy to avoid traversing beyond
provably-unreachable blocks. In contrast, the current
implementation always visits all blocks.
The prove pass already has nearly all of the mechanism necessary to
compute unsatisfiability, which means this both simplifies the code
and makes it more powerful.
The only complication is that the current implication procedure has a
hack for dealing with the 0 <= Args[0] condition of OpIsInBounds and
OpIsSliceInBounds. We replace this with asserting the appropriate fact
when we process one of these conditions. This seems much cleaner
anyway, and works because we can now take advantage of derived facts.
This has no measurable effect on compiler performance.
Effectiveness:
There is exactly one condition in all of std and cmd that this fails
to prove that the old implementation could: (int64(^uint(0)>>1) < x)
in encoding/gob. This can never be true because x is an int, and it's
basically coincidence that the old code gets this. (For example, it
fails to prove the similar (x < ^int64(^uint(0)>>1)) condition that
immediately precedes it, and even though the conditions are logically
unrelated, it wouldn't get the second one if it hadn't first processed
the first!)
It does, however, prove a few dozen additional branches. These come
from facts that are added to the fact table about the relations
between len and cap. These were almost never queried directly before,
but could lead to contradictions, which the unsat-based approach is
able to use.
There are exactly two branches in std and cmd that this implementation
proves in the *other* direction. This sounds scary, but is okay
because both occur in already-unreachable blocks, so it doesn't matter
what we chose. Because the fact table logic is sound but incomplete,
it fails to prove that the block isn't reachable, even though it is
able to prove that both outgoing branches are impossible. We could
turn these blocks into BlockExit blocks, but it doesn't seem worth the
trouble of the extra proof effort for something that happens twice in
all of std and cmd.
Tests:
This CL updates test/prove.go to change the expected messages because
it can no longer give a "reason" why it proved or disproved a
condition. It also adds a new test of a branch it couldn't prove
before.
It mostly guts test/sliceopt.go, removing everything related to slice
bounds optimizations and moving a few relevant tests to test/prove.go.
Much of this test is actually unreachable. The new prove pass figures
this out and doesn't try to prove anything about the unreachable
parts. The output on the unreachable parts is already suspect because
anything can be proved at that point, so it's really just a regression
test for an algorithm the compiler no longer uses.
This is a step toward fixing #23354. That issue is quite easy to fix
once we can use derived facts effectively.
Change-Id: Ia48a1b9ee081310579fe474e4a61857424ff8ce8
Reviewed-on: https://go-review.googlesource.com/87478
Reviewed-by: Keith Randall <khr@golang.org>
2018-01-10 16:28:58 -05:00
|
|
|
func removeBranch(b *Block, branch branch) {
|
2019-10-01 11:10:22 -04:00
|
|
|
c := b.Controls[0]
|
cmd/compile: make prove pass use unsatisfiability
Currently the prove pass uses implication queries. For each block, it
collects the set of branch conditions leading to that block, and
queries this fact table for whether any of these facts imply the
block's own branch condition (or its inverse). This works remarkably
well considering it doesn't do any deduction on these facts, but it
has various downsides:
1. It requires an implementation both of adding facts to the table and
determining implications. These are very nearly duals of each
other, but require separate implementations. Likewise, the process
of asserting facts of dominating branch conditions is very nearly
the dual of the process of querying implied branch conditions.
2. It leads to less effective use of derived facts. For example, the
prove pass currently derives facts about the relations between len
and cap, but can't make use of these unless a branch condition is
in the exact form of a derived fact. If one of these derived facts
contradicts another fact, it won't notice or make use of this.
This CL changes the approach of the prove pass to instead use
*contradiction* instead of implication. Rather than ever querying a
branch condition, it simply adds branch conditions to the fact table.
If this leads to a contradiction (specifically, it makes the fact set
unsatisfiable), that branch is impossible and can be cut. As a result,
1. We can eliminate the code for determining implications
(factsTable.get disappears entirely). Also, there is now a single
implementation of visiting and asserting branch conditions, since
we don't have to flip them around to treat them as facts in one
place and queries in another.
2. Derived facts can be used effectively. It doesn't matter *why* the
fact table is unsatisfiable; a contradiction in any of the facts is
enough.
3. As an added benefit, it's now quite easy to avoid traversing beyond
provably-unreachable blocks. In contrast, the current
implementation always visits all blocks.
The prove pass already has nearly all of the mechanism necessary to
compute unsatisfiability, which means this both simplifies the code
and makes it more powerful.
The only complication is that the current implication procedure has a
hack for dealing with the 0 <= Args[0] condition of OpIsInBounds and
OpIsSliceInBounds. We replace this with asserting the appropriate fact
when we process one of these conditions. This seems much cleaner
anyway, and works because we can now take advantage of derived facts.
This has no measurable effect on compiler performance.
Effectiveness:
There is exactly one condition in all of std and cmd that this fails
to prove that the old implementation could: (int64(^uint(0)>>1) < x)
in encoding/gob. This can never be true because x is an int, and it's
basically coincidence that the old code gets this. (For example, it
fails to prove the similar (x < ^int64(^uint(0)>>1)) condition that
immediately precedes it, and even though the conditions are logically
unrelated, it wouldn't get the second one if it hadn't first processed
the first!)
It does, however, prove a few dozen additional branches. These come
from facts that are added to the fact table about the relations
between len and cap. These were almost never queried directly before,
but could lead to contradictions, which the unsat-based approach is
able to use.
There are exactly two branches in std and cmd that this implementation
proves in the *other* direction. This sounds scary, but is okay
because both occur in already-unreachable blocks, so it doesn't matter
what we chose. Because the fact table logic is sound but incomplete,
it fails to prove that the block isn't reachable, even though it is
able to prove that both outgoing branches are impossible. We could
turn these blocks into BlockExit blocks, but it doesn't seem worth the
trouble of the extra proof effort for something that happens twice in
all of std and cmd.
Tests:
This CL updates test/prove.go to change the expected messages because
it can no longer give a "reason" why it proved or disproved a
condition. It also adds a new test of a branch it couldn't prove
before.
It mostly guts test/sliceopt.go, removing everything related to slice
bounds optimizations and moving a few relevant tests to test/prove.go.
Much of this test is actually unreachable. The new prove pass figures
this out and doesn't try to prove anything about the unreachable
parts. The output on the unreachable parts is already suspect because
anything can be proved at that point, so it's really just a regression
test for an algorithm the compiler no longer uses.
This is a step toward fixing #23354. That issue is quite easy to fix
once we can use derived facts effectively.
Change-Id: Ia48a1b9ee081310579fe474e4a61857424ff8ce8
Reviewed-on: https://go-review.googlesource.com/87478
Reviewed-by: Keith Randall <khr@golang.org>
2018-01-10 16:28:58 -05:00
|
|
|
if b.Func.pass.debug > 0 {
|
|
|
|
|
verb := "Proved"
|
|
|
|
|
if branch == positive {
|
|
|
|
|
verb = "Disproved"
|
2016-02-19 12:14:42 +01:00
|
|
|
}
|
cmd/compile: make prove pass use unsatisfiability
Currently the prove pass uses implication queries. For each block, it
collects the set of branch conditions leading to that block, and
queries this fact table for whether any of these facts imply the
block's own branch condition (or its inverse). This works remarkably
well considering it doesn't do any deduction on these facts, but it
has various downsides:
1. It requires an implementation both of adding facts to the table and
determining implications. These are very nearly duals of each
other, but require separate implementations. Likewise, the process
of asserting facts of dominating branch conditions is very nearly
the dual of the process of querying implied branch conditions.
2. It leads to less effective use of derived facts. For example, the
prove pass currently derives facts about the relations between len
and cap, but can't make use of these unless a branch condition is
in the exact form of a derived fact. If one of these derived facts
contradicts another fact, it won't notice or make use of this.
This CL changes the approach of the prove pass to instead use
*contradiction* instead of implication. Rather than ever querying a
branch condition, it simply adds branch conditions to the fact table.
If this leads to a contradiction (specifically, it makes the fact set
unsatisfiable), that branch is impossible and can be cut. As a result,
1. We can eliminate the code for determining implications
(factsTable.get disappears entirely). Also, there is now a single
implementation of visiting and asserting branch conditions, since
we don't have to flip them around to treat them as facts in one
place and queries in another.
2. Derived facts can be used effectively. It doesn't matter *why* the
fact table is unsatisfiable; a contradiction in any of the facts is
enough.
3. As an added benefit, it's now quite easy to avoid traversing beyond
provably-unreachable blocks. In contrast, the current
implementation always visits all blocks.
The prove pass already has nearly all of the mechanism necessary to
compute unsatisfiability, which means this both simplifies the code
and makes it more powerful.
The only complication is that the current implication procedure has a
hack for dealing with the 0 <= Args[0] condition of OpIsInBounds and
OpIsSliceInBounds. We replace this with asserting the appropriate fact
when we process one of these conditions. This seems much cleaner
anyway, and works because we can now take advantage of derived facts.
This has no measurable effect on compiler performance.
Effectiveness:
There is exactly one condition in all of std and cmd that this fails
to prove that the old implementation could: (int64(^uint(0)>>1) < x)
in encoding/gob. This can never be true because x is an int, and it's
basically coincidence that the old code gets this. (For example, it
fails to prove the similar (x < ^int64(^uint(0)>>1)) condition that
immediately precedes it, and even though the conditions are logically
unrelated, it wouldn't get the second one if it hadn't first processed
the first!)
It does, however, prove a few dozen additional branches. These come
from facts that are added to the fact table about the relations
between len and cap. These were almost never queried directly before,
but could lead to contradictions, which the unsat-based approach is
able to use.
There are exactly two branches in std and cmd that this implementation
proves in the *other* direction. This sounds scary, but is okay
because both occur in already-unreachable blocks, so it doesn't matter
what we chose. Because the fact table logic is sound but incomplete,
it fails to prove that the block isn't reachable, even though it is
able to prove that both outgoing branches are impossible. We could
turn these blocks into BlockExit blocks, but it doesn't seem worth the
trouble of the extra proof effort for something that happens twice in
all of std and cmd.
Tests:
This CL updates test/prove.go to change the expected messages because
it can no longer give a "reason" why it proved or disproved a
condition. It also adds a new test of a branch it couldn't prove
before.
It mostly guts test/sliceopt.go, removing everything related to slice
bounds optimizations and moving a few relevant tests to test/prove.go.
Much of this test is actually unreachable. The new prove pass figures
this out and doesn't try to prove anything about the unreachable
parts. The output on the unreachable parts is already suspect because
anything can be proved at that point, so it's really just a regression
test for an algorithm the compiler no longer uses.
This is a step toward fixing #23354. That issue is quite easy to fix
once we can use derived facts effectively.
Change-Id: Ia48a1b9ee081310579fe474e4a61857424ff8ce8
Reviewed-on: https://go-review.googlesource.com/87478
Reviewed-by: Keith Randall <khr@golang.org>
2018-01-10 16:28:58 -05:00
|
|
|
if b.Func.pass.debug > 1 {
|
|
|
|
|
b.Func.Warnl(b.Pos, "%s %s (%s)", verb, c.Op, c)
|
|
|
|
|
} else {
|
|
|
|
|
b.Func.Warnl(b.Pos, "%s %s", verb, c.Op)
|
2016-02-19 12:14:42 +01:00
|
|
|
}
|
|
|
|
|
}
|
2019-10-01 11:10:22 -04:00
|
|
|
if c != nil && c.Pos.IsStmt() == src.PosIsStmt && c.Pos.SameFileAndLine(b.Pos) {
|
|
|
|
|
// attempt to preserve statement marker.
|
|
|
|
|
b.Pos = b.Pos.WithIsStmt()
|
|
|
|
|
}
|
2022-03-06 12:07:54 -08:00
|
|
|
if branch == positive || branch == negative {
|
|
|
|
|
b.Kind = BlockFirst
|
|
|
|
|
b.ResetControls()
|
|
|
|
|
if branch == positive {
|
|
|
|
|
b.swapSuccessors()
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// TODO: figure out how to remove an entry from a jump table
|
2016-02-19 12:14:42 +01:00
|
|
|
}
|
2016-03-07 18:36:16 +01:00
|
|
|
}
|
|
|
|
|
|
2018-01-11 16:32:02 -05:00
|
|
|
// isConstDelta returns non-nil if v is equivalent to w+delta (signed).
|
|
|
|
|
func isConstDelta(v *Value) (w *Value, delta int64) {
|
|
|
|
|
cop := OpConst64
|
|
|
|
|
switch v.Op {
|
|
|
|
|
case OpAdd32, OpSub32:
|
|
|
|
|
cop = OpConst32
|
2022-09-16 12:16:00 +02:00
|
|
|
case OpAdd16, OpSub16:
|
|
|
|
|
cop = OpConst16
|
|
|
|
|
case OpAdd8, OpSub8:
|
|
|
|
|
cop = OpConst8
|
2018-01-11 16:32:02 -05:00
|
|
|
}
|
|
|
|
|
switch v.Op {
|
2022-09-16 12:16:00 +02:00
|
|
|
case OpAdd64, OpAdd32, OpAdd16, OpAdd8:
|
2018-01-11 16:32:02 -05:00
|
|
|
if v.Args[0].Op == cop {
|
|
|
|
|
return v.Args[1], v.Args[0].AuxInt
|
|
|
|
|
}
|
|
|
|
|
if v.Args[1].Op == cop {
|
|
|
|
|
return v.Args[0], v.Args[1].AuxInt
|
|
|
|
|
}
|
2022-09-16 12:16:00 +02:00
|
|
|
case OpSub64, OpSub32, OpSub16, OpSub8:
|
2018-01-11 16:32:02 -05:00
|
|
|
if v.Args[1].Op == cop {
|
|
|
|
|
aux := v.Args[1].AuxInt
|
|
|
|
|
if aux != -aux { // Overflow; too bad
|
|
|
|
|
return v.Args[0], -aux
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil, 0
|
|
|
|
|
}
|
cmd/compile: handle sign/zero extensions in prove, via update method
Array accesses with index types smaller than the machine word size may
involve a sign or zero extension of the index value before bounds
checking. Currently, this defeats prove because the facts about the
original index value don't flow through the sign/zero extension.
This CL fixes this by looking back through value-preserving sign/zero
extensions when adding facts via Update and, where appropriate, applying
the same facts using the pre-extension value. This fix is enhanced by
also looking back through value-preserving extensions within
ft.isNonNegative to infer whether the extended value is known to be
non-negative. Without this additional isNonNegative enhancement, this
logic is rendered significantly less effective by the limitation
discussed in the next paragraph.
In Update, the application of facts to pre-extension values is limited
to cases where the domain of the new fact is consistent with the type of
the pre-extension value. There may be cases where this cross-domain
passing of facts is valid, but distinguishing them from the invalid
cases is difficult for me to reason about and to implement.
Assessing which cases to allow requires details about the context and
inferences behind the fact being applied which are not available
within Update. Additional difficulty arises from the fact that the SSA
does not curently differentiate extensions added by the compiler for
indexing operations, extensions added by the compiler for implicit
conversions, or explicit extensions from the source.
Examples of some cases that would need to be filtered correctly for
cross-domain facts:
(1) A uint8 is zero-extended to int for indexing (a value-preserving
zeroExt). When, if ever, can signed domain facts learned about the int be
applied to the uint8?
(2) An int8 is sign-extended to int16 (value-preserving) for an equality
comparison. Equality comparison facts are currently always learned in both
the signed and unsigned domains. When, if ever, can the unsigned facts
learned about the int16, from the int16 != int16 comparison, be applied
to the original int8?
This is an alternative to CL 122695 and CL 174309. Compared to CL 122695,
this CL differs in that the facts added about the pre-extension value will
pass through the Update method, where additional inferences are processed
(e.g. fence-post implications, see #29964). CL 174309 is limited to bounds
checks, so is narrower in application, and makes the code harder to read.
Fixes #26292.
Fixes #29964.
Fixes #15074
Removes 238 bounds checks from std/cmd.
Change-Id: I1f87c32ee672bfb8be397b27eab7a4c2f304893f
Reviewed-on: https://go-review.googlesource.com/c/go/+/174704
Run-TryBot: Zach Jones <zachj1@gmail.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Giovanni Bajo <rasky@develer.com>
2019-04-09 23:19:43 +01:00
|
|
|
|
|
|
|
|
// isCleanExt reports whether v is the result of a value-preserving
|
2022-11-11 19:22:35 +08:00
|
|
|
// sign or zero extension.
|
cmd/compile: handle sign/zero extensions in prove, via update method
Array accesses with index types smaller than the machine word size may
involve a sign or zero extension of the index value before bounds
checking. Currently, this defeats prove because the facts about the
original index value don't flow through the sign/zero extension.
This CL fixes this by looking back through value-preserving sign/zero
extensions when adding facts via Update and, where appropriate, applying
the same facts using the pre-extension value. This fix is enhanced by
also looking back through value-preserving extensions within
ft.isNonNegative to infer whether the extended value is known to be
non-negative. Without this additional isNonNegative enhancement, this
logic is rendered significantly less effective by the limitation
discussed in the next paragraph.
In Update, the application of facts to pre-extension values is limited
to cases where the domain of the new fact is consistent with the type of
the pre-extension value. There may be cases where this cross-domain
passing of facts is valid, but distinguishing them from the invalid
cases is difficult for me to reason about and to implement.
Assessing which cases to allow requires details about the context and
inferences behind the fact being applied which are not available
within Update. Additional difficulty arises from the fact that the SSA
does not curently differentiate extensions added by the compiler for
indexing operations, extensions added by the compiler for implicit
conversions, or explicit extensions from the source.
Examples of some cases that would need to be filtered correctly for
cross-domain facts:
(1) A uint8 is zero-extended to int for indexing (a value-preserving
zeroExt). When, if ever, can signed domain facts learned about the int be
applied to the uint8?
(2) An int8 is sign-extended to int16 (value-preserving) for an equality
comparison. Equality comparison facts are currently always learned in both
the signed and unsigned domains. When, if ever, can the unsigned facts
learned about the int16, from the int16 != int16 comparison, be applied
to the original int8?
This is an alternative to CL 122695 and CL 174309. Compared to CL 122695,
this CL differs in that the facts added about the pre-extension value will
pass through the Update method, where additional inferences are processed
(e.g. fence-post implications, see #29964). CL 174309 is limited to bounds
checks, so is narrower in application, and makes the code harder to read.
Fixes #26292.
Fixes #29964.
Fixes #15074
Removes 238 bounds checks from std/cmd.
Change-Id: I1f87c32ee672bfb8be397b27eab7a4c2f304893f
Reviewed-on: https://go-review.googlesource.com/c/go/+/174704
Run-TryBot: Zach Jones <zachj1@gmail.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Giovanni Bajo <rasky@develer.com>
2019-04-09 23:19:43 +01:00
|
|
|
func isCleanExt(v *Value) bool {
|
|
|
|
|
switch v.Op {
|
|
|
|
|
case OpSignExt8to16, OpSignExt8to32, OpSignExt8to64,
|
|
|
|
|
OpSignExt16to32, OpSignExt16to64, OpSignExt32to64:
|
|
|
|
|
// signed -> signed is the only value-preserving sign extension
|
|
|
|
|
return v.Args[0].Type.IsSigned() && v.Type.IsSigned()
|
|
|
|
|
|
|
|
|
|
case OpZeroExt8to16, OpZeroExt8to32, OpZeroExt8to64,
|
|
|
|
|
OpZeroExt16to32, OpZeroExt16to64, OpZeroExt32to64:
|
|
|
|
|
// unsigned -> signed/unsigned are value-preserving zero extensions
|
|
|
|
|
return !v.Args[0].Type.IsSigned()
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|