mbutrovich commented on code in PR #11879:
URL: https://github.com/apache/arrow-rs/pull/11879#discussion_r4516242565


##########
arrow-buffer/src/builder/boolean.rs:
##########
@@ -186,18 +200,17 @@ impl BooleanBufferBuilder {
     #[inline]
     pub fn resize(&mut self, len: usize) {
         match len.checked_sub(self.len) {
-            Some(delta) => self.advance(delta),
+            Some(delta) => self.advance_zeroed(delta),
             None => self.truncate(len),
         }
     }
 
     /// Appends a boolean `v` into the buffer
     #[inline]
     pub fn append(&mut self, v: bool) {
-        self.advance(1);
-        if v {
-            unsafe { bit_util::set_bit_raw(self.buffer.as_mut_ptr(), self.len 
- 1) };
-        }
+        let index = self.len;
+        self.advance_zeroed(1);
+        self.buffer.as_slice_mut()[index / 8] |= u8::from(v) << (index & 7);
     }

Review Comment:
   `append` now reads and writes the byte twice for every value, including 
`false`, which `main` never writes. Cross-compiled for x86_64, the loop does 
this for each value:
   
   ```asm
   cmp   rdi, r13
   jae   .LBB0_12                ; bounds check
   and   byte ptr [rax + rdi], dl  ; clear the padding
   mov   rsi, qword ptr [r15 + 24] ; reload the buffer length
   cmp   rdi, rsi
   jae   .LBB0_18                ; second bounds check
   mov   rax, qword ptr [r15 + 16] ; reload the buffer pointer
   or    byte ptr [rax + rdi], r14b ; set the bit
   ```
   
   The next `append` reads the same byte, so each value waits for the previous 
one's store to reach the next load. I measured a version with a single 
read-modify-write per value in #12137, and on the bot's CPUs it cost a lot 
whenever the values were predictable:
   
   | Case | Xeon Platinum 8581C | Neoverse-V2 |
   |---|---|---|
   | `append`, all false | 0.37x | 0.67x |
   | `append`, all true | 0.88x | 0.86x |
   | `append`, random | 1.95x | 2.25x |
   
   Speedup is `main` time over the branch's time. The numbers come from 
https://github.com/apache/arrow-rs/pull/12137#issuecomment-6745382116 and 
https://github.com/apache/arrow-rs/pull/12137#issuecomment-6745307055. This PR 
does two writes per value, so I'd expect it to do no better. On an Apple M5 Max 
with thin LTO, this PR measured 0.70x on all false and 0.72x on all true (the 
"Local benchmarks" section of #12137). Apple silicon didn't predict the bot's 
results, though, so a bot run on this PR would settle it.
   
   Could we keep `main`'s `append` as it is and clear the padding in `advance` 
only when something may have dirtied it? See the next comment.



##########
arrow-buffer/src/builder/boolean.rs:
##########
@@ -147,6 +147,20 @@ impl BooleanBufferBuilder {
         self.len = new_len;
     }
 
+    /// Advances the buffer with zeroed bits, clearing the padding at the old 
end.
+    #[inline]
+    fn advance_zeroed(&mut self, additional: usize) {
+        if additional == 0 {
+            return;
+        }
+
+        let old_len = self.len;
+        self.advance(additional);
+
+        let keep_mask = (1_u8 << (old_len & 7)) - 1;
+        self.buffer.as_slice_mut()[old_len / 8] &= keep_mask;
+    }

Review Comment:
   `advance_zeroed` adds a write and a bounds check to every call, which also 
costs `append_slice`. On the M5 Max, many calls of 1 to 7 values measured 0.61x 
to 0.64x, and calls of 8 values measured 0.78x to 0.81x (same section of 
#12137).
   
   Only `set_bit` and `as_slice_mut` can set the padding bits. A flag lets 
every append skip the clear until one of them has run:
   
   - Add a `padding_dirty: bool` field. `set_bit` does `self.padding_dirty |= 
index >= self.len`, and `as_slice_mut` sets it to `true`.
   - `advance` calls the clear only when the flag is set, and the clear resets 
it. `append`, `append_slice`, `append_n(_, false)`, `resize`, and `append_word` 
all go through `advance`. `append_word` would need to call `advance` instead of 
resizing on its own. That fixes the public `advance` too. Its docs don't 
promise zeroed bits, but `BooleanBuilder::append_null` and `append_nulls` call 
it 
([`boolean_builder.rs#L83-L95`](https://github.com/apache/arrow-rs/blob/30c5c3287cd5b99ca9aa0e46c18e0d8e09c4e2a2/arrow-array/src/builder/boolean_builder.rs#L83-L95)).
   - Mark the clear `#[cold]` and `#[inline(never)]`. `#[cold]` puts the clean 
case on the fall-through path. `#[inline(never)]` keeps `advance` small. With 
only `#[cold]`, LLVM still inlined the clear, and under thin LTO with 16 
codegen units `append_slice` stopped inlining into its callers. Short 
`append_slice` calls then measured 0.86x to 0.91x on the Xeon.
   
   
https://github.com/apache/arrow-rs/blob/32fbb333e4c577ad0b03ab4ddb3c7e936cd6ec7b/arrow-buffer/src/builder/boolean.rs#L136-L180
 has that version. On the Xeon 
(https://github.com/apache/arrow-rs/pull/12137#issuecomment-6747325134), short 
and 8-value `append_slice` calls measured 0.97x to 1.00x. Per-value `append` 
measured 1.01x on all true, 0.83x on all false, 0.91x on mostly true, and 0.82x 
on random. All false came out at 0.86x and 0.83x in two runs, and that's the 
cost of loading and testing the flag on every value. The other per-value cases 
moved by up to 10% between runs whose loops differed only in out-of-line code, 
so I'd read them as layout noise until an aligned run says otherwise.
   
   If you go this way, could you add `advance` to the test's operations? The 
test in #12137 covers it, along with `append_packed_range` and 
`extend_trusted_len`.



-- 
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]

Reply via email to