gortiz commented on code in PR #19806:
URL: https://github.com/apache/pinot/pull/19806#discussion_r4258594580


##########
pinot-core/src/main/java/org/apache/pinot/core/common/ObjectSerDeUtils.java:
##########
@@ -510,11 +522,11 @@ public DoubleArrayList deserialize(byte[] bytes) {
     @Override
     public DoubleArrayList deserialize(ByteBuffer byteBuffer) {
       int numValues = byteBuffer.getInt();
-      DoubleArrayList doubleArrayList = new DoubleArrayList(numValues);
-      for (int i = 0; i < numValues; i++) {
-        doubleArrayList.add(byteBuffer.getDouble());
-      }
-      return doubleArrayList;
+      checkRemaining(byteBuffer, numValues, Double.BYTES);
+      double[] values = new double[numValues];
+      byteBuffer.asDoubleBuffer().get(values);

Review Comment:
   Good catch, thanks. Fixed in 75c1df0da1. Lists with fewer than 64 values 
(`PrimitiveArrayOutput.MIN_BULK_VALUES`) now go through a per-value loop. This 
applies to the four list deserializers, the PERCENTILE merge and 
`PagedPinotOutputStream.write{Ints,Longs,Doubles}`. The chunked write over a 
plain `DataOutput` uses a lower cutoff of 16. The short-list path reads 
straight into the array instead of calling `add()`, so it is a bit faster than 
master too.
   
   I picked 64 from these runs (JDK 25, ns per list). The x86 numbers compare 
bulk with the old per-value code, and the M3 numbers compare the new code with 
master:
   
   | Values | x86 deserialize DOUBLE, per-value → bulk | x86 PERCENTILE merge | 
M3 deserialize DOUBLE, master → now |
   |---|---|---|---|
   | 16 | 25.7 → 31.8 | 25.5 → 34.4 | 13.4 → 11.2 |
   | 32 | 40.3 → 39.4 | 34.6 → 41.1 | 21.0 → 15.8 |
   | 63 | | | 35.1 → 24.2 |
   | 64 | 74.7 → 66.2 | 55.6 → 56.8 | 39.2 → 49.7 |
   | 128 | 145.3 → 129.5 | 122.4 → 91.8 | 70.9 → 64.2 |
   | 1024 | 1980 → 905 | 1397 → 444 | |
   
   On x86 the JNI swap call costs less (about 10-16 ns), and bulk already 
breaks even around 32-48 values. On the M3 the crossover is closer to 128, so a 
list of 64-127 values still loses about 10-16 ns there. I kept 64 because 
servers run on x86. The tests cover both sides of each cutoff.
   



##########
pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/PercentileAggregationFunction.java:
##########
@@ -206,10 +211,70 @@ public ColumnDataType getIntermediateResultColumnType() {
     return ColumnDataType.OBJECT;
   }
 
+  @Override
+  public DoubleArrayList mergeSerializedIntermediateResult(@Nullable 
DoubleArrayList intermediateResult,
+      CustomObject serialized) {
+    if (intermediateResult == null) {
+      return deserializeIntermediateResult(serialized);
+    }
+    // Reads the values in bulk straight into the merged list, in the format of
+    // ObjectSerDeUtils.DOUBLE_ARRAY_LIST_SER_DE: an int count, then the 
doubles, in the buffer's byte order. Same
+    // growth as addAll(), without the incoming list.
+    ByteBuffer buffer = serialized.getBuffer();

Review Comment:
   Agreed, this is real. It sits in the mailbox receive path 
(`CompoundDataBuffer` + `copyOrView`) and in how `BlockSplitter` sizes blocks, 
not in the SerDe this PR changes. I'd prefer to handle it in a separate 
follow-up so this PR stays about the SerDe format and merge. The merge here 
already avoids the second full-size copy. The follow-up removes the first one 
for values that cross a gRPC message boundary.
   



##########
pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/PercentileAggregationFunction.java:
##########
@@ -206,10 +211,70 @@ public ColumnDataType getIntermediateResultColumnType() {
     return ColumnDataType.OBJECT;
   }
 
+  @Override
+  public DoubleArrayList mergeSerializedIntermediateResult(@Nullable 
DoubleArrayList intermediateResult,
+      CustomObject serialized) {
+    if (intermediateResult == null) {
+      return deserializeIntermediateResult(serialized);
+    }
+    // Reads the values in bulk straight into the merged list, in the format of
+    // ObjectSerDeUtils.DOUBLE_ARRAY_LIST_SER_DE: an int count, then the 
doubles, in the buffer's byte order. Same
+    // growth as addAll(), without the incoming list.
+    ByteBuffer buffer = serialized.getBuffer();
+    int numValues = buffer.getInt();
+    if (numValues < 0) {
+      throw new IllegalArgumentException("Negative number of values: " + 
numValues);
+    }
+    if (numValues > buffer.remaining() / Double.BYTES) {
+      throw new BufferUnderflowException();
+    }
+    int size = intermediateResult.size();
+    int capacity = intermediateResult.elements().length;
+    if (size + numValues > capacity) {
+      intermediateResult.ensureCapacity(
+          (int) Math.min(Math.max((long) size + numValues, (long) capacity + 
(capacity >> 1)), Integer.MAX_VALUE - 8));
+    }
+    // size() zero-fills the new range, so it must run before the values are 
copied in.
+    intermediateResult.size(size + numValues);

Review Comment:
   I left this one out on purpose. Returning `DoubleArrayList.wrap(elements, 
size + numValues)` makes the old and the new list share one array. Nothing 
breaks today because both callers use the return value, but a future caller 
that keeps the old reference would see its contents change. The zero fill is a 
sequential memset, and the gain (262 → 144 µs for 8 MB) only shows on very 
large lists. The PR already brings that case down from 777 µs. If profiles show 
the fill matters, I'll do it as a follow-up with a comment that pins the 
aliasing contract.
   



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