andygrove commented on code in PR #26277:
URL: https://github.com/apache/datafusion/pull/26277#discussion_r4298359773


##########
datafusion/physical-plan/src/joins/sort_merge_join/materializing_stream.rs:
##########
@@ -117,44 +117,112 @@ impl StreamedBatch {
         self.num_output_rows
     }
 
-    /// Appends new pair consisting of current streamed index and 
`buffered_idx`
-    /// index of buffered batch with `buffered_batch_idx` index.
-    fn append_output_pair(
+    /// Appends a pair of the current streamed index and a null buffered 
index, for a
+    /// streamed row that no buffered row matches.
+    fn append_null_joined_pair(
         &mut self,
         buffered_batch_idx: Option<usize>,
-        buffered_idx: Option<usize>,
         batch_size: usize,
     ) {
-        // If no current chunk exists or current chunk is not for current 
buffered batch,
-        // create a new chunk
-        if self.output_indices.is_empty() || self.buffered_batch_idx != 
buffered_batch_idx
-        {
-            // Compute capacity only when creating a new chunk (infrequent 
operation).
-            // The capacity is the remaining space to reach batch_size.
-            // This should always be >= 1 since we only call this when 
num_output_rows < batch_size.
-            debug_assert!(
-                batch_size > self.num_output_rows,
-                "batch_size ({batch_size}) must be > num_output_rows ({})",
-                self.num_output_rows
-            );
-            let capacity = batch_size - self.num_output_rows;
-            self.output_indices.push(StreamedJoinedChunk {
+        let streamed_idx = self.idx as u64;
+        let current_chunk = self.chunk_for(buffered_batch_idx, batch_size);
+        current_chunk.streamed_indices.append_value(streamed_idx);
+        current_chunk.buffered_indices.append_null();
+        self.num_output_rows += 1;
+    }
+
+    /// Appends one pair for each row in `buffered_range` of the buffered 
batch with
+    /// `buffered_batch_idx` index, all with the current streamed index. The 
indices
+    /// are appended in bulk, which matters when a streamed row matches many 
buffered
+    /// rows.
+    fn append_output_pairs(
+        &mut self,
+        buffered_batch_idx: usize,
+        buffered_range: Range<usize>,
+        batch_size: usize,
+    ) {
+        let len = buffered_range.len();
+        debug_assert!(
+            len > 0 && self.num_output_rows + len <= batch_size,
+            "appending {len} pairs to {} with batch_size {batch_size}",
+            self.num_output_rows
+        );
+        if len > 1 {
+            return self.append_output_run(
                 buffered_batch_idx,
-                streamed_indices: UInt64Builder::with_capacity(capacity),
-                buffered_indices: UInt64Builder::with_capacity(capacity),
-            });
-            self.buffered_batch_idx = buffered_batch_idx;
+                buffered_range,
+                batch_size,
+            );
         }
-        let current_chunk = self.output_indices.last_mut().unwrap();
+        // A single match, as in a one-to-one join, is cheaper to append 
directly.
+        let streamed_idx = self.idx as u64;
+        let current_chunk = self.chunk_for(Some(buffered_batch_idx), 
batch_size);
+        current_chunk.streamed_indices.append_value(streamed_idx);
+        current_chunk
+            .buffered_indices
+            .append_value(buffered_range.start as u64);
+        self.num_output_rows += 1;
+    }
 
-        // Append index of streamed batch and index of buffered batch into 
current chunk
-        current_chunk.streamed_indices.append_value(self.idx as u64);
-        if let Some(idx) = buffered_idx {
-            current_chunk.buffered_indices.append_value(idx as u64);
-        } else {
-            current_chunk.buffered_indices.append_null();
+    /// The bulk half of [`Self::append_output_pairs`], kept out of line so 
that the
+    /// one-row case, which one-to-one joins take for every row, does not pay 
for its
+    /// setup.
+    #[inline(never)]
+    fn append_output_run(
+        &mut self,
+        buffered_batch_idx: usize,
+        buffered_range: Range<usize>,
+        batch_size: usize,
+    ) {
+        let len = buffered_range.len();
+        let streamed_idx = self.idx as u64;
+        let current_chunk = self.chunk_for(Some(buffered_batch_idx), 
batch_size);
+        current_chunk
+            .streamed_indices
+            .append_value_n(streamed_idx, len);
+        // SAFETY: a `Range<u64>` reports its exact length in `size_hint`.
+        unsafe {

Review Comment:
   I measured both, as the median of ten interleaved runs of each binary on an 
M3 Max:
   
   | Case | `append_trusted_len_iter` | `append_value` loop | Change |
   | --- | --- | --- | --- |
   | `inner_1to100` | 19.39 ms | 20.27 ms | +4.6% |
   | `inner_1to10` | 10.62 ms | 11.21 ms | +5.5% |
   | `left_1to100_filtered` | 50.28 ms | 51.13 ms | +1.7% |
   | `inner_group_spans_buffered_batches/512` | 3.42 ms | 3.54 ms | +3.5% |
   | `inner_1to1`, which never reaches this code | 1.51 ms | 1.55 ms | +2.2% |
   | `left_semi_1to10`, which doesn't use this stream | 0.649 ms | 0.643 ms | 
−0.9% |
   
   An earlier run of seven rounds gave +5.6% for `inner_1to100`. So the 
`unsafe` is worth about 5% at 100 matches per row, where `main` takes 27 ms. 
That's small next to the bulk path's own gain, so I switched to the loop, and 
the module has no `unsafe` again. The chunk's builders are created with 
capacity for the room left in the output batch, so the loop needs no `reserve`. 
The safe `extend_from_iter_option` was no faster than the loop.
   



##########
datafusion/physical-plan/benches/sort_merge_join.rs:
##########
@@ -168,6 +168,22 @@ fn bench_smj(c: &mut Criterion) {
         });
     }
 
+    // 1:100 Inner Join — 20K left, 20K right, 200 distinct keys

Review Comment:
   Added `left_1to100_filtered`. It joins the `inner_1to100` inputs as a left 
join with the filter `left.data < right.data`, which keeps about half of the 
pairs. The last streamed row of each key fails it for every buffered row, so 
the deferred filtering also produces null-joined rows.
   
   Against `main`, as the median of seven interleaved runs, it goes from 58.57 
ms to 52.86 ms (−9.8%). The bulk path saves about as much as in `inner_1to100` 
(5.7 ms, against 6.7 ms there), so the smaller relative gain comes from the 
cost of the filtering, which this PR doesn't change.
   



##########
datafusion/physical-plan/src/joins/sort_merge_join/materializing_stream.rs:
##########
@@ -725,13 +793,16 @@ impl MaterializingSortMergeJoinStream {
         while !self.buffered_data.scanning_finished()
             && self.num_unfrozen_pairs() < self.batch_size
         {
-            let scanning_idx = self.buffered_data.scanning_idx();
-            self.streamed_batch.append_output_pair(
-                Some(self.buffered_data.scanning_batch_idx),
-                Some(scanning_idx),
+            // Pair the streamed row with the rest of the scanning buffered 
batch at
+            // once, up to the room left in the output batch.
+            let rows = self.buffered_data.scanning_remaining();
+            let len = rows.len().min(self.batch_size - 
self.num_unfrozen_pairs());

Review Comment:
   Done, thanks:
   
   - `pair_streamed_row_with_group` computes `room` once and counts it down, 
and passes `rows.start` and `len` down, so neither `append_output_pairs` nor 
the bulk half takes a `Range` now.
   - `append_output_pairs` matches on `len`, with the bulk path as the `_` arm, 
so a zero `len` in a release build would append no pair. The `debug_assert!` 
stays.
   - The bulk half is now `append_output_range`.
   - The comment above `scanning_remaining()` is gone, and the "cheaper to 
append directly" one is on the `1` arm.
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to