Skip to content

Commit

Permalink
Don't use explicit 'full' field in Envelope
Browse files Browse the repository at this point in the history
Empty envelopes are now indicated by the "logical" value of Min X being
NaN. MinX is no longer stored directly as an envelope field, but instead
its bit patterned XORed with NaN is stored. This is so that the zero
value of an envelope has the logical value of Min X being NaN,
representing an empty envelope. This is in an effort to bring Envelope
back down to 32 bytes to see if that fixes the performance regression.
  • Loading branch information
peterstace committed Oct 7, 2021
1 parent 3d837b7 commit 031c1d9
Show file tree
Hide file tree
Showing 3 changed files with 99 additions and 53 deletions.
2 changes: 1 addition & 1 deletion geom/line.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ type line struct {
func (ln line) uncheckedEnvelope() Envelope {
ln.a.X, ln.b.X = sortFloat64Pair(ln.a.X, ln.b.X)
ln.a.Y, ln.b.Y = sortFloat64Pair(ln.a.Y, ln.b.Y)
return Envelope{true, ln.a, ln.b}
return newUncheckedEnvelope(ln.a, ln.b)
}

func (ln line) box() rtree.Box {
Expand Down
148 changes: 97 additions & 51 deletions geom/type_envelope.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,27 @@ import (
// The Envelope zero value is the empty envelope. Envelopes are immutable after
// creation.
type Envelope struct {
full bool
min XY
max XY
// nanXORMinX is the bit pattern of "min X" XORed with the bit pattern of
// NaN. This is so that when Envelope has its zero value, the logical value
// of "min X" is NaN. The logical value of "min X" being NaN is used to
// signify that the Envelope is empty.
nanXORMinX uint64

minY float64
maxX float64
maxY float64
}

const nan = 0x7FF8000000000001

// encodeFloat64WithNaN encodes a float64 by XORing it with NaN.
func encodeFloat64WithNaN(f float64) uint64 {
return math.Float64bits(f) ^ nan
}

// minX decodes the logical value ("min X") of nanXORMinX.
func (e Envelope) minX() float64 {
return math.Float64frombits(e.nanXORMinX ^ nan)
}

// NewEnvelope returns the smallest envelope that contains all provided XYs.
Expand All @@ -40,26 +58,43 @@ func NewEnvelope(xys []XY) (Envelope, error) {
return env, nil
}

func newUncheckedEnvelope(min, max XY) Envelope {
return Envelope{
nanXORMinX: encodeFloat64WithNaN(min.X),
minY: min.Y,
maxX: max.X,
maxY: max.Y,
}
}

func (e Envelope) min() XY {
return XY{e.minX(), e.minY}
}

func (e Envelope) max() XY {
return XY{e.maxX, e.maxY}
}

// IsEmpty returns true if and only if this envelope is empty.
func (e Envelope) IsEmpty() bool {
return !e.full
return math.IsNaN(e.minX())
}

// IsPoint returns true if and only if this envelope represents a single point.
func (e Envelope) IsPoint() bool {
return e.full && e.min == e.max
return !e.IsEmpty() && e.min() == e.max()
}

// IsLine returns true if and only if this envelope represents a single line
// (which must be either vertical or horizontal).
func (e Envelope) IsLine() bool {
return e.full && (e.min.X == e.max.X) != (e.min.Y == e.max.Y)
return !e.IsEmpty() && (e.minX() == e.maxX) != (e.minY == e.maxY)
}

// IsRectangle returns true if and only if this envelope represents a
// non-degenerate rectangle with some area.
func (e Envelope) IsRectangle() bool {
return e.full && e.min.X != e.max.X && e.min.Y != e.max.Y
return !e.IsEmpty() && e.minX() != e.maxX && e.minY != e.maxY
}

// AsGeometry returns the envelope as a Geometry. In the regular case where the
Expand All @@ -73,18 +108,19 @@ func (e Envelope) AsGeometry() Geometry {
case e.IsEmpty():
return Geometry{}
case e.IsPoint():
return e.min.asUncheckedPoint().AsGeometry()
return e.min().asUncheckedPoint().AsGeometry()
case e.IsLine():
ln := line{e.min, e.max}
ln := line{e.min(), e.max()}
return ln.asLineString().AsGeometry()
}

minX := e.minX()
floats := [...]float64{
e.min.X, e.min.Y,
e.min.X, e.max.Y,
e.max.X, e.max.Y,
e.max.X, e.min.Y,
e.min.X, e.min.Y,
minX, e.minY,
minX, e.maxY,
e.maxX, e.maxY,
e.maxX, e.minY,
minX, e.minY,
}
seq := NewSequence(floats[:], DimXY)
ls, err := NewLineString(seq)
Expand All @@ -100,18 +136,18 @@ func (e Envelope) AsGeometry() Geometry {

// Min returns the point in the envelope with the minimum X and Y values.
func (e Envelope) Min() Point {
if !e.full {
if e.IsEmpty() {
return Point{}
}
return e.min.asUncheckedPoint()
return e.min().asUncheckedPoint()
}

// Max returns the point in the envelope with the maximum X and Y values.
func (e Envelope) Max() Point {
if !e.full {
if e.IsEmpty() {
return Point{}
}
return e.max.asUncheckedPoint()
return e.max().asUncheckedPoint()
}

// ExtendToIncludeXY returns the smallest envelope that contains all of the
Expand All @@ -129,13 +165,12 @@ func (e Envelope) ExtendToIncludeXY(xy XY) (Envelope, error) {
// when the XY doesn't come directly from user input.
func (e Envelope) uncheckedExtend(xy XY) Envelope {
if e.IsEmpty() {
return Envelope{full: true, min: xy, max: xy}
}
return Envelope{
full: true,
min: XY{fastMin(e.min.X, xy.X), fastMin(e.min.Y, xy.Y)},
max: XY{fastMax(e.max.X, xy.X), fastMax(e.max.Y, xy.Y)},
return newUncheckedEnvelope(xy, xy)
}
return newUncheckedEnvelope(
XY{fastMin(e.minX(), xy.X), fastMin(e.minY, xy.Y)},
XY{fastMax(e.maxX, xy.X), fastMax(e.maxY, xy.Y)},
)
}

// ExpandToIncludeEnvelope returns the smallest envelope that contains all of
Expand All @@ -147,64 +182,75 @@ func (e Envelope) ExpandToIncludeEnvelope(o Envelope) Envelope {
if o.IsEmpty() {
return e
}
return Envelope{
full: true,
min: XY{fastMin(e.min.X, o.min.X), fastMin(e.min.Y, o.min.Y)},
max: XY{fastMax(e.max.X, o.max.X), fastMax(e.max.Y, o.max.Y)},
}
return newUncheckedEnvelope(
XY{fastMin(e.minX(), o.minX()), fastMin(e.minY, o.minY)},
XY{fastMax(e.maxX, o.maxX), fastMax(e.maxY, o.maxY)},
)
}

// Contains returns true if and only if this envelope contains the given XY. It
// always returns false in the case where the XY contains NaN or +/- Infinity
// coordinates.
func (e Envelope) Contains(p XY) bool {
return e.full &&
return !e.IsEmpty() &&
p.validate() == nil &&
p.X >= e.min.X && p.X <= e.max.X &&
p.Y >= e.min.Y && p.Y <= e.max.Y
p.X >= e.minX() && p.X <= e.maxX &&
p.Y >= e.minY && p.Y <= e.maxY
}

// Intersects returns true if and only if this envelope has any points in
// common with another envelope.
func (e Envelope) Intersects(o Envelope) bool {
return e.full && o.full &&
(e.min.X <= o.max.X) && (e.max.X >= o.min.X) &&
(e.min.Y <= o.max.Y) && (e.max.Y >= o.min.Y)
return !e.IsEmpty() && !o.IsEmpty() &&
(e.minX() <= o.maxX) && (e.maxX >= o.minX()) &&
(e.minY <= o.maxY) && (e.maxY >= o.minY)
}

// Center returns the center point of the envelope.
func (e Envelope) Center() Point {
if !e.full {
if e.IsEmpty() {
return Point{}
}
return e.min.Add(e.max).Scale(0.5).asUncheckedPoint()
return e.min().
Add(e.max()).
Scale(0.5).
asUncheckedPoint()
}

// Covers returns true if and only if this envelope entirely covers another
// envelope (i.e. every point in the other envelope is contained within this
// envelope). An envelope can only cover another if it is non-empty.
// Furthermore, an envelope can only be covered if it is non-empty.
func (e Envelope) Covers(o Envelope) bool {
return e.full && o.full &&
e.min.X <= o.min.X && e.min.Y <= o.min.Y &&
e.max.X >= o.max.X && e.max.Y >= o.max.Y
return !e.IsEmpty() && !o.IsEmpty() &&
e.minX() <= o.minX() && e.minY <= o.minY &&
e.maxX >= o.maxX && e.maxY >= o.maxY
}

// Width returns the difference between the maximum and minimum X coordinates
// of the envelope.
func (e Envelope) Width() float64 {
return e.max.X - e.min.X
if e.IsEmpty() {
return 0
}
return e.maxX - e.minX()
}

// Height returns the difference between the maximum and minimum X coordinates
// of the envelope.
func (e Envelope) Height() float64 {
return e.max.Y - e.min.Y
if e.IsEmpty() {
return 0
}
return e.maxY - e.minY
}

// Area returns the area covered by the envelope.
func (e Envelope) Area() float64 {
return (e.max.X - e.min.X) * (e.max.Y - e.min.Y)
if e.IsEmpty() {
return 0
}
return (e.maxX - e.minX()) * (e.maxY - e.minY)
}

// Distance calculates the shortest distance between this envelope and another
Expand All @@ -213,19 +259,19 @@ func (e Envelope) Area() float64 {
// non-empty and intersect with each other, the distance between them is still
// well-defined, but zero.
func (e Envelope) Distance(o Envelope) (float64, bool) {
if !e.full || !o.full {
if e.IsEmpty() || o.IsEmpty() {
return 0, false
}
dx := fastMax(0, fastMax(o.min.X-e.max.X, e.min.X-o.max.X))
dy := fastMax(0, fastMax(o.min.Y-e.max.Y, e.min.Y-o.max.Y))
dx := fastMax(0, fastMax(o.minX()-e.maxX, e.minX()-o.maxX))
dy := fastMax(0, fastMax(o.minY-e.maxY, e.minY-o.maxY))
return math.Sqrt(dx*dx + dy*dy), true
}

func (e Envelope) box() (rtree.Box, bool) {
return rtree.Box{
MinX: e.min.X,
MinY: e.min.Y,
MaxX: e.max.X,
MaxY: e.max.Y,
}, e.full
MinX: e.minX(),
MinY: e.minY,
MaxX: e.maxX,
MaxY: e.maxY,
}, !e.IsEmpty()
}
2 changes: 1 addition & 1 deletion geom/xy.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ func (w XY) asUncheckedPoint() Point {
// XY value doesn't come directly from outline the library without first being
// validated.
func (w XY) uncheckedEnvelope() Envelope {
return Envelope{true, w, w}
return newUncheckedEnvelope(w, w)
}

// Sub returns the result of subtracting the other XY from this XY (in the same
Expand Down

0 comments on commit 031c1d9

Please sign in to comment.