HappenLee commented on code in PR #68498:
URL: https://github.com/apache/doris/pull/68498#discussion_r4130215920


##########
be/src/core/value/quantile_state.cpp:
##########
@@ -28,7 +30,50 @@
 #include "util/tdigest.h"
 #include "util/unaligned.h"
 
+#ifdef BE_TEST
+#include "cpp/sync_point.h"
+#endif
+
 namespace doris {
+
+// Shares a digest across QuantileState copies and detaches it before sample
+// writes. Readers use shared locks; compression uses an exclusive lock.
+struct QuantileState::TDigestHolder {
+    explicit TDigestHolder(float compression) : digest(compression) {}
+    TDigestHolder(const TDigestHolder& other) : digest(other.digest) {}
+
+    std::shared_lock<std::shared_mutex> lock_processed_digest() {
+        std::shared_lock read_lock(mutex);
+        if (digest.have_unprocessed()) {
+            read_lock.unlock();
+            {
+                std::unique_lock write_lock(mutex);
+                if (digest.have_unprocessed()) {
+                    digest.compress();
+                }
+            }
+            read_lock.lock();
+        }
+        return read_lock;
+    }
+
+    TDigest digest;
+    std::shared_mutex mutex;
+};
+
+TDigest& QuantileState::_mutable_tdigest() {
+    std::shared_ptr<TDigestHolder> detached;
+    {
+        std::unique_lock lock(_tdigest_ptr->mutex);
+        if (_tdigest_ptr.use_count() == 1) {
+            return _tdigest_ptr->digest;
+        }
+        detached = std::make_shared<TDigestHolder>(*_tdigest_ptr);

Review Comment:
   Suggested minimal change for this finding: replace the capacity-preserving 
TDigest copy constructor with `TDigest(const TDigest&) = default`, or an 
equivalent member-wise copy that copies the vectors' live elements without 
explicitly reserving the source capacities or the maximum write-buffer sizes. 
Keep `_max_processed` and `_max_unprocessed` unchanged: they are algorithm 
thresholds, not allocation sizes.
   
   The current `_unprocessed.reserve(max(other.capacity(), _max_unprocessed + 
1))` allocates at least 80001 centroids at compression 10000 even when the 
source's unprocessed vector is empty. A growing `quantile_union(q)` window 
saves the accumulator after each row; the following merge detaches, so 
successive result holders retain these buffers. About 1000 such buffers account 
for 610 MiB of allocation capacity, excluding the other vectors. This is a 
code-derived capacity estimate, not a measured RSS result.
   
   Please update `CopyPreservesValuesAndWriteCapacity` to check 
values/isolation and avoidance of unnecessary spare capacity, and add a long 
high-compression window capacity test. Member-wise copying removes this 
unconditional reservation, but subsequent growth and existing saved-holder 
capacity still need checking. The existing omission of internal buffers from 
`ColumnComplexType::allocated_bytes()` also makes block-level accounting 
underestimate this cost.



##########
be/src/core/value/quantile_state.cpp:
##########
@@ -28,7 +30,50 @@
 #include "util/tdigest.h"
 #include "util/unaligned.h"
 
+#ifdef BE_TEST
+#include "cpp/sync_point.h"
+#endif
+
 namespace doris {
+
+// Shares a digest across QuantileState copies and detaches it before sample
+// writes. Readers use shared locks; compression uses an exclusive lock.
+struct QuantileState::TDigestHolder {
+    explicit TDigestHolder(float compression) : digest(compression) {}
+    TDigestHolder(const TDigestHolder& other) : digest(other.digest) {}
+
+    std::shared_lock<std::shared_mutex> lock_processed_digest() {
+        std::shared_lock read_lock(mutex);
+        if (digest.have_unprocessed()) {
+            read_lock.unlock();
+            {
+                std::unique_lock write_lock(mutex);
+                if (digest.have_unprocessed()) {
+                    digest.compress();
+                }
+            }
+            read_lock.lock();
+        }
+        return read_lock;
+    }
+
+    TDigest digest;
+    std::shared_mutex mutex;
+};
+
+TDigest& QuantileState::_mutable_tdigest() {
+    std::shared_ptr<TDigestHolder> detached;
+    {
+        std::unique_lock lock(_tdigest_ptr->mutex);

Review Comment:
   Performance suggestion: could the ownership check and source cloning use a 
`std::shared_lock` here instead of a `std::unique_lock`? The detached-holder 
construction only reads the source digest, but currently holds the exclusive 
lock across allocation and all vector copies. Consequently, independent copies 
detaching from the same holder serialize their cloning and block otherwise 
read-only percentile queries and source merges.
   
   A shared lock would still exclude `lock_processed_digest()`'s exclusive 
compression phase while allowing source readers and other clones to overlap. 
This needs to preserve the existing contract that mutation of the same outer 
QuantileState requires exclusive access; sample writes must still happen only 
after obtaining an exclusively owned digest, and replacement of the old holder 
must remain outside the lock's lifetime.
   
   Please evaluate this with a synchronized concurrent-detach/read test and a 
shared-source benchmark. This is an optimization opportunity identified from 
the lock scope, not a separately demonstrated correctness bug or a measured 
performance regression.



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