Skip to content

Commit fa894dd

Browse files
committed
perf(rust/sedona-geo): fast-path point-to-point ST_Distance
Inspect each operand's WKB header once via the new WkbPointLayout primitive and, when both are Points, compute the Euclidean distance directly from the coordinate offsets, skipping a full geometry parse and the generic distance dispatch. A per-batch check routes a non-Point scalar operand to the parse-once general path so it is never re-parsed per row.
1 parent 9f73cba commit fa894dd

3 files changed

Lines changed: 181 additions & 59 deletions

File tree

rust/sedona-geo/benches/geo-functions.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,11 @@ fn criterion_benchmark(c: &mut Criterion) {
6060
ArrayScalar(Point, Polygon(500)),
6161
);
6262

63+
benchmark::scalar(c, &f, "geo", "st_distance", ArrayArray(Point, Point));
64+
benchmark::scalar(c, &f, "geo", "st_distance", ArrayScalar(Point, Point));
65+
benchmark::scalar(c, &f, "geo", "st_distance", ScalarArray(Point, Point));
6366
benchmark::scalar(c, &f, "geo", "st_distance", ArrayScalar(Point, Polygon(10)));
67+
benchmark::scalar(c, &f, "geo", "st_distance", ArrayArray(Point, Polygon(10)));
6468
benchmark::scalar(
6569
c,
6670
&f,

rust/sedona-geo/src/st_distance.rs

Lines changed: 88 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,18 @@ use std::sync::Arc;
1818

1919
use arrow_array::builder::Float64Builder;
2020
use arrow_schema::DataType;
21-
use datafusion_common::error::Result;
21+
use datafusion_common::error::{DataFusionError, Result};
22+
use datafusion_common::scalar::ScalarValue;
2223
use datafusion_expr::ColumnarValue;
2324
use sedona_expr::{
2425
item_crs::ItemCrsKernel,
2526
scalar_udf::{ScalarKernelRef, SedonaScalarKernel},
2627
};
27-
use sedona_functions::executor::WkbExecutor;
28+
use sedona_functions::executor::{WkbBytesExecutor, WkbExecutor};
2829
use sedona_geo_generic_alg::line_measures::DistanceExt;
30+
use sedona_geometry::wkb_header::WkbPointLayout;
2931
use sedona_schema::{datatypes::SedonaType, matchers::ArgMatcher};
30-
use wkb::reader::Wkb;
32+
use wkb::reader::read_wkb;
3133

3234
/// ST_Distance() implementation using [DistanceExt]
3335
pub fn st_distance_impl() -> Vec<ScalarKernelRef> {
@@ -52,12 +54,21 @@ impl SedonaScalarKernel for STDistance {
5254
arg_types: &[SedonaType],
5355
args: &[ColumnarValue],
5456
) -> Result<ColumnarValue> {
55-
let executor = WkbExecutor::new(arg_types, args);
57+
// The point-to-point fast path can only fire when neither operand is a
58+
// non-Point scalar: every row pairs against that scalar, so reading raw
59+
// bytes would re-parse it on every row for no benefit. Detect that case
60+
// up front (one cheap header read per scalar) and take the general path,
61+
// which parses each scalar exactly once.
62+
if scalar_is_non_point(&args[0])? || scalar_is_non_point(&args[1])? {
63+
return invoke_general(arg_types, args);
64+
}
65+
66+
let executor = WkbBytesExecutor::new(arg_types, args);
5667
let mut builder = Float64Builder::with_capacity(executor.num_iterations());
57-
executor.execute_wkb_wkb_void(|maybe_wkb0, maybe_wkb1| {
58-
match (maybe_wkb0, maybe_wkb1) {
59-
(Some(wkb0), Some(wkb1)) => {
60-
builder.append_value(invoke_scalar(wkb0, wkb1)?);
68+
executor.execute_wkb_wkb_void(|maybe_bytes0, maybe_bytes1| {
69+
match (maybe_bytes0, maybe_bytes1) {
70+
(Some(bytes0), Some(bytes1)) => {
71+
builder.append_value(invoke_scalar_bytes(bytes0, bytes1)?);
6172
}
6273
_ => builder.append_null(),
6374
}
@@ -69,8 +80,75 @@ impl SedonaScalarKernel for STDistance {
6980
}
7081
}
7182

72-
fn invoke_scalar(wkb_a: &Wkb, wkb_b: &Wkb) -> Result<f64> {
73-
Ok(wkb_a.distance_ext(wkb_b))
83+
/// Distance between two operands given their raw WKB bytes.
84+
fn invoke_scalar_bytes(bytes_a: &[u8], bytes_b: &[u8]) -> Result<f64> {
85+
// Point-to-point fast path: inspect each operand's header once. When both
86+
// are Points, read their (x, y) from the offsets just discovered and compute
87+
// the Euclidean distance directly, skipping a full geometry parse and the
88+
// generic distance dispatch. Inspecting both headers before reading any
89+
// coordinate means a non-Point operand never triggers a wasted coordinate
90+
// read, and the matched Point coordinates are read without re-parsing the
91+
// header.
92+
if let (Ok(Some(layout_a)), Ok(Some(layout_b))) = (
93+
WkbPointLayout::try_from_wkb(bytes_a),
94+
WkbPointLayout::try_from_wkb(bytes_b),
95+
) {
96+
if let (Ok(Some((ax, ay))), Ok(Some((bx, by)))) =
97+
(layout_a.read_xy(bytes_a), layout_b.read_xy(bytes_b))
98+
{
99+
let dx = ax - bx;
100+
let dy = ay - by;
101+
return Ok((dx * dx + dy * dy).sqrt());
102+
}
103+
}
104+
105+
// Any non-Point, POINT EMPTY, or malformed input falls back to the general
106+
// parser, which preserves the exact semantics (and surfaces the same error
107+
// on truly malformed bytes).
108+
let wkb_a = read_wkb(bytes_a).map_err(|e| DataFusionError::External(Box::new(e)))?;
109+
let wkb_b = read_wkb(bytes_b).map_err(|e| DataFusionError::External(Box::new(e)))?;
110+
Ok(wkb_a.distance_ext(&wkb_b))
111+
}
112+
113+
/// General path: parse both operands (parsing any scalar operand exactly once)
114+
/// and use the generic distance implementation.
115+
fn invoke_general(arg_types: &[SedonaType], args: &[ColumnarValue]) -> Result<ColumnarValue> {
116+
let executor = WkbExecutor::new(arg_types, args);
117+
let mut builder = Float64Builder::with_capacity(executor.num_iterations());
118+
executor.execute_wkb_wkb_void(|maybe_wkb0, maybe_wkb1| {
119+
match (maybe_wkb0, maybe_wkb1) {
120+
(Some(wkb0), Some(wkb1)) => {
121+
builder.append_value(wkb0.distance_ext(wkb1));
122+
}
123+
_ => builder.append_null(),
124+
}
125+
126+
Ok(())
127+
})?;
128+
129+
executor.finish(Arc::new(builder.finish()))
130+
}
131+
132+
/// Returns `true` only when `arg` is a scalar WKB value that is definitively not
133+
/// a Point. Array arguments, nulls, and Point scalars return `false` (the fast
134+
/// path may still apply per row). Reads only the WKB header.
135+
fn scalar_is_non_point(arg: &ColumnarValue) -> Result<bool> {
136+
if let ColumnarValue::Scalar(scalar) = arg {
137+
if let Some(bytes) = scalar_wkb_bytes(scalar) {
138+
return Ok(matches!(WkbPointLayout::try_from_wkb(bytes), Ok(None)));
139+
}
140+
}
141+
Ok(false)
142+
}
143+
144+
/// Extracts the raw WKB bytes from a scalar geometry value, if present.
145+
fn scalar_wkb_bytes(scalar: &ScalarValue) -> Option<&[u8]> {
146+
match scalar {
147+
ScalarValue::Binary(maybe)
148+
| ScalarValue::BinaryView(maybe)
149+
| ScalarValue::LargeBinary(maybe) => maybe.as_deref(),
150+
_ => None,
151+
}
74152
}
75153

76154
#[cfg(test)]

rust/sedona-geometry/src/wkb_header.rs

Lines changed: 89 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -152,74 +152,114 @@ impl WkbHeader {
152152
}
153153
}
154154

155-
/// Read the `(x, y)` of a WKB Point without parsing the geometry generally.
155+
/// The coordinate layout of a WKB Point, discovered by reading only its header.
156156
///
157-
/// This is a fast path for the common case of sampling/indexing by a single
158-
/// point: a Point WKB has a fixed layout (`byte order`, `type code`, optional
159-
/// SRID, then the coordinate), so the coordinate is at a known offset and can
160-
/// be read with a couple of fixed-offset loads. It deliberately handles only
161-
/// Points — any other geometry is an error.
157+
/// This is the fast-path primitive for sampling/indexing by Point. A Point WKB
158+
/// has a fixed layout (`byte order`, `type code`, optional SRID, then the
159+
/// coordinate), so [`try_from_wkb`](Self::try_from_wkb) can cheaply decide
160+
/// whether a buffer is a Point — reading only the first few header bytes, never
161+
/// a coordinate — and a later [`read_xy`](Self::read_xy) reads the coordinate
162+
/// from the offset it already found.
162163
///
163-
/// Returns `Ok(None)` for `POINT EMPTY` (encoded as `NaN`/`NaN`), `Ok(Some((x, y)))`
164-
/// for a finite point, and an error if the bytes are not a Point or are truncated.
165-
/// Accepts both ISO (with Z/M dimension flags) and EWKB (with Z/M/SRID flags)
166-
/// encodings; only the first two ordinates are read.
167-
pub fn read_point_xy(buf: &[u8]) -> Result<Option<(f64, f64)>, SedonaGeometryError> {
168-
let little_endian = match buf.first() {
169-
Some(1) => true,
170-
Some(0) => false,
171-
_ => {
172-
return Err(SedonaGeometryError::Invalid(
173-
"WKB: missing or invalid byte order".to_string(),
174-
))
175-
}
176-
};
164+
/// Splitting the header inspection from the coordinate read lets a binary
165+
/// function (e.g. distance) test *both* operands first and read coordinates only
166+
/// when both are Points, without re-parsing the header or wasting a coordinate
167+
/// read on rows that fall back to the general parser.
168+
#[derive(Debug, Clone, Copy)]
169+
pub struct WkbPointLayout {
170+
little_endian: bool,
171+
coord_offset: usize,
172+
}
177173

178-
let read_u32 = |offset: usize| -> Result<u32, SedonaGeometryError> {
179-
let bytes: [u8; 4] = buf
180-
.get(offset..offset + 4)
174+
impl WkbPointLayout {
175+
/// Inspects a WKB header, returning the Point layout or `Ok(None)` if `buf`
176+
/// is not a Point. Reads only the byte order and type code (recognizing the
177+
/// EWKB SRID prefix) — never a coordinate. Errors only on a missing/invalid
178+
/// byte order or a truncated type code. Accepts both ISO (with Z/M dimension
179+
/// flags) and EWKB (with Z/M/SRID flags) encodings.
180+
#[inline]
181+
pub fn try_from_wkb(buf: &[u8]) -> Result<Option<Self>, SedonaGeometryError> {
182+
let little_endian = match buf.first() {
183+
Some(1) => true,
184+
Some(0) => false,
185+
_ => {
186+
return Err(SedonaGeometryError::Invalid(
187+
"WKB: missing or invalid byte order".to_string(),
188+
))
189+
}
190+
};
191+
192+
let type_bytes: [u8; 4] = buf
193+
.get(1..5)
181194
.ok_or_else(|| SedonaGeometryError::Invalid("WKB: truncated header".to_string()))?
182195
.try_into()
183196
.unwrap();
184-
Ok(if little_endian {
185-
u32::from_le_bytes(bytes)
197+
let type_code = if little_endian {
198+
u32::from_le_bytes(type_bytes)
186199
} else {
187-
u32::from_be_bytes(bytes)
188-
})
189-
};
200+
u32::from_be_bytes(type_bytes)
201+
};
202+
203+
// The low three bits of the base type identify a Point (1) regardless of
204+
// the ISO dimension digits or EWKB high-bit flags.
205+
if type_code & 0x7 != 1 {
206+
return Ok(None);
207+
}
208+
209+
// EWKB carries a 4-byte SRID between the type code and the coordinate.
210+
let has_srid = type_code & SRID_FLAG_BIT != 0;
211+
let coord_offset = if has_srid { 9 } else { 5 };
190212

191-
let read_f64 = |offset: usize| -> Result<f64, SedonaGeometryError> {
213+
Ok(Some(Self {
214+
little_endian,
215+
coord_offset,
216+
}))
217+
}
218+
219+
/// Reads the `(x, y)` from `buf`, which must be the same buffer passed to
220+
/// [`try_from_wkb`](Self::try_from_wkb). Returns `Ok(None)` for `POINT EMPTY`
221+
/// (encoded as `NaN`/`NaN`); errors if the coordinate is truncated.
222+
#[inline]
223+
pub fn read_xy(&self, buf: &[u8]) -> Result<Option<(f64, f64)>, SedonaGeometryError> {
224+
let x = self.read_f64(buf, self.coord_offset)?;
225+
let y = self.read_f64(buf, self.coord_offset + 8)?;
226+
if x.is_nan() && y.is_nan() {
227+
Ok(None)
228+
} else {
229+
Ok(Some((x, y)))
230+
}
231+
}
232+
233+
#[inline]
234+
fn read_f64(&self, buf: &[u8], offset: usize) -> Result<f64, SedonaGeometryError> {
192235
let bytes: [u8; 8] = buf
193236
.get(offset..offset + 8)
194237
.ok_or_else(|| SedonaGeometryError::Invalid("WKB: truncated coordinate".to_string()))?
195238
.try_into()
196239
.unwrap();
197-
Ok(if little_endian {
240+
Ok(if self.little_endian {
198241
f64::from_le_bytes(bytes)
199242
} else {
200243
f64::from_be_bytes(bytes)
201244
})
202-
};
203-
204-
let type_code = read_u32(1)?;
205-
// The low three bits of the base type identify a Point (1) regardless of the
206-
// ISO dimension digits or EWKB high-bit flags.
207-
if type_code & 0x7 != 1 {
208-
return Err(SedonaGeometryError::Invalid(format!(
209-
"WKB: expected a Point (type code {type_code})"
210-
)));
211245
}
246+
}
212247

213-
// EWKB carries a 4-byte SRID between the type code and the coordinate.
214-
let has_srid = type_code & SRID_FLAG_BIT != 0;
215-
let coord_offset = if has_srid { 9 } else { 5 };
216-
217-
let x = read_f64(coord_offset)?;
218-
let y = read_f64(coord_offset + 8)?;
219-
if x.is_nan() && y.is_nan() {
220-
Ok(None)
221-
} else {
222-
Ok(Some((x, y)))
248+
/// Read the `(x, y)` of a WKB Point without parsing the geometry generally.
249+
///
250+
/// Convenience wrapper over [`WkbPointLayout`] for the single-Point case: it
251+
/// deliberately handles only Points — any other geometry is an error.
252+
///
253+
/// Returns `Ok(None)` for `POINT EMPTY` (encoded as `NaN`/`NaN`), `Ok(Some((x, y)))`
254+
/// for a finite point, and an error if the bytes are not a Point or are truncated.
255+
/// Accepts both ISO (with Z/M dimension flags) and EWKB (with Z/M/SRID flags)
256+
/// encodings; only the first two ordinates are read.
257+
pub fn read_point_xy(buf: &[u8]) -> Result<Option<(f64, f64)>, SedonaGeometryError> {
258+
match WkbPointLayout::try_from_wkb(buf)? {
259+
Some(layout) => layout.read_xy(buf),
260+
None => Err(SedonaGeometryError::Invalid(
261+
"WKB: expected a Point".to_string(),
262+
)),
223263
}
224264
}
225265

0 commit comments

Comments
 (0)