Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions internal/loopir/lower.go
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,12 @@ func (l *lowerer) bound(e ast.Expr) (bound, bool) {
}
b := bound{text: types.ExprString(e)}
b.value, b.isConst = l.constant(e)
if b.isConst {
// A named constant or a constant expression (s0, 1 - 1) is spelled as its
// value, so the checks that read a constant start or limit see it and the
// emitted code never indexes at a negative constant they would have caught.
b.text = strconv.FormatInt(b.value, 10)
}
if call, ok := ast.Unparen(e).(*ast.CallExpr); ok {
b.slice, _ = l.lenOf(call)
}
Expand Down Expand Up @@ -532,6 +538,25 @@ func (l *lowerer) body(node ast.Stmt, block *ast.BlockStmt, trip Trip, step int6
}
}

// A constant offset that puts an index below zero makes the scalar loop panic
// when it runs: on its first iteration when the start is a constant and the
// first index is negative, or on every iteration when the limit is a constant
// and even the last index is. It is left alone, since the vector loop would
// index or slice at a negative constant, which does not compile.
start, errStart := strconv.ParseInt(trip.Start, 10, 64)
limit, errLimit := strconv.ParseInt(trip.Limit, 10, 64)
negative := func(offset string) bool {
off, err := strconv.ParseInt(offset, 10, 64)
return err == nil && (errStart == nil && start+off < 0 || errLimit == nil && limit+off <= 0)
}
for _, stmt := range body {
for _, leaf := range Leaves(stmtValue(stmt)) {
if load, ok := leaf.(*Load); ok && negative(load.Off) {
return nil, ReasonNegativeIndex
}
}
}

loop := &Loop{
Node: node,
Ind: Induction{Var: l.iv, Step: step, Trip: trip},
Expand Down
6 changes: 6 additions & 0 deletions internal/loopir/reason.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ const (
// iteration writes (a[i] = a[i-1] + x): iteration i reads what iteration i-1
// wrote, so the iterations cannot run as a vector.
ReasonCarriedDependence Reason = "an iteration reads an element an earlier iteration wrote"
// ReasonNegativeIndex covers a slice read at a constant offset that puts an
// index below zero: the first index, with a constant start (a[i-1] in a loop
// from 0), or every index, with a constant limit (a[i-20] in a loop that stops
// before 16). The scalar loop panics when it runs, and the vector loop would
// index or slice at a negative constant, which does not compile.
ReasonNegativeIndex Reason = "a constant offset puts an index below zero"
// ReasonUnsupportedIf covers an if that is not a select: a condition that is
// not a comparison of loop values (joined by && and ||), an init statement, or
// a branch that is not one assignment to an element of the same slice.
Expand Down
90 changes: 90 additions & 0 deletions testdata/offset_negative_constant.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# A read at a negative constant offset is rewritten while every index it reads can
# be zero or more. When an index is a negative constant, the first one in a loop
# with a constant start (a[i-1] from 0) or every one in a loop with a constant limit
# (a[i-16] in a loop that stops before 16), the scalar loop panics when it runs and
# the vector loop would index at a negative constant, which does not compile, so the
# loop is left alone, whether the start and limit are written as literals, named
# constants or constant expressions. The rewritten package must build.

loopvec -json .
cmpenv stdout stdout.want

loopvec -split .
cmpenv stderr stderr.want
env GOEXPERIMENT=simd
exec go build .

-- go.mod --
module example.com/test

go 1.27.1
-- ops.go --
package test

func FirstIndexZeroConstant(dst, a []int32) {
for i := 15; i < 31; i++ {
dst[i] = a[i-15] + 1
}
}

func EveryIndexNegative(dst, a []int32) {
for i := 0; i < 16; i++ {
dst[i] = a[i-16] + 1
}
}

func FarBelow(dst, a []float32) {
for i := 0; i < 16; i++ {
dst[i] = a[i-20] * a[i]
}
}

func FirstIndexNegative(dst, a []int32, n int) {
for i := 0; i < n; i++ {
dst[i] = a[i-1] * 3
}
}

func FirstIndexZero(dst, a []int32, n int) {
for i := 1; i < n; i++ {
dst[i] = a[i-1] * 3
}
}

const s0, lim = 0, 16

func NamedLimit(dst, a []int32, k int) {
for i := k; i < lim; i++ {
dst[i] = a[i-20] + 1
}
}

func NamedStart(dst, a []int32, n int) {
for i := s0; i < n; i++ {
dst[i] = a[i-1] * 3
}
}

func ExpressionStart(dst, a []int32, n int) {
for i := 1 - 1; i < n; i++ {
dst[i] = a[i-1] * 3
}
}

func NamedStartCopy(dst, a []int32, n int) {
for i := s0; i < n; i++ {
dst[i] = a[i-1]
}
}
-- stdout.want --
{"file":"$WORK/ops.go","line":4,"func":"FirstIndexZeroConstant","vectorized":true}
{"file":"$WORK/ops.go","line":10,"func":"EveryIndexNegative","vectorized":false,"reason":"a constant offset puts an index below zero","stage":"lower"}
{"file":"$WORK/ops.go","line":16,"func":"FarBelow","vectorized":false,"reason":"a constant offset puts an index below zero","stage":"lower"}
{"file":"$WORK/ops.go","line":22,"func":"FirstIndexNegative","vectorized":false,"reason":"a constant offset puts an index below zero","stage":"lower"}
{"file":"$WORK/ops.go","line":28,"func":"FirstIndexZero","vectorized":true}
{"file":"$WORK/ops.go","line":36,"func":"NamedLimit","vectorized":false,"reason":"a constant offset puts an index below zero","stage":"lower"}
{"file":"$WORK/ops.go","line":42,"func":"NamedStart","vectorized":false,"reason":"a constant offset puts an index below zero","stage":"lower"}
{"file":"$WORK/ops.go","line":48,"func":"ExpressionStart","vectorized":false,"reason":"a constant offset puts an index below zero","stage":"lower"}
{"file":"$WORK/ops.go","line":54,"func":"NamedStartCopy","vectorized":false,"reason":"a constant offset puts an index below zero","stage":"lower"}
-- stderr.want --
loopvec: rewrote 2 loop(s) in $WORK/ops.go