[ 
https://issues.apache.org/jira/browse/CASSANDRA-20333?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18116779#comment-18116779
 ] 

Dmitry Konstantinov commented on CASSANDRA-20333:
-------------------------------------------------

Hi, I have prepared a patch with a performance improvement for 
DecayingEstimatedHistogramReservoir: 
https://github.com/apache/cassandra/pull/5185
It is based on [~benedict]'s idea + include a set of other optimizations.

The main idea: we accumulate reservoir updates in a thread-local buffer (using 
CassandraThread). The updates can be encoded into int values (reservoir id, 
bucket id, temestamp delta). So, we have have just an int[] buffer. The buffer 
is common for all reservoirs. It has a single writer and multiple readers.

During a flush of this buffer we count identical values using a simple 
open-addressing hashtable, it allows to reduce number of atomic updates per 
bucket. Taking in account that in reality we have a lot of mostly constant 
value histograms (like written rows count) and durations usually have 
multi-modal distribution then saving from such aggregation can be huge.
Also we want to group buckets for the same reservoir together, to re-use 
calculated exponential decay factors + for better memory locality.
The original way to do it was sorting but I found it not fast enough, so I 
introduced a logic to do it using another open-addressing hashtable for 
reservoir id, so we can count how many entries per reservoir we have, identify 
require ranges per reservoir and do a compaction which puts all of entries 
together using few additional data structures. All flushing data structures are 
re-used, so there is no allocation there.
For metric read operations a time limiter is added for buffer flushing, to 
reduce threads iteration overhead here.

Additionally:
- findIndex logic is optimized a bit by avoiding float computation for log2 
calculation using short[] lookup table.
- AtomicLongArray offset is added for 1st value to avoid false sharing with 
array object header for length read
- clock.nowInSec is introduced for approximated clocks to avoid a frequent unit 
conversion (division)


In total such change give the following performance results:
1) microbenhmark, DecayingEstimatedHistogramBench.update — Mode.Throughput, 
ops/ms, 12 threads, 16-cores server: Intel Xeon 6975P-C (Granite Rapids)
{code:java}
┌────────────┬──────────┬──────────┬───────────┬──────────┐
│ histograms │ maxValue │  before  │   after   │ speed-up │
├────────────┼──────────┼──────────┼───────────┼──────────┤
│    (count) │  (value) │ (ops/ms) │  (ops/ms) │      (×) │
├────────────┼──────────┼──────────┼───────────┼──────────┤
│          1 │       10 │   47,151 │ 1,136,306 │   24.10× │
├────────────┼──────────┼──────────┼───────────┼──────────┤
│          1 │      500 │   75,396 │   995,328 │   13.20× │
├────────────┼──────────┼──────────┼───────────┼──────────┤
│          1 │  100,000 │   81,608 │ 1,084,287 │   13.29× │
├────────────┼──────────┼──────────┼───────────┼──────────┤
│         16 │       10 │  124,317 │   641,430 │    5.16× │
├────────────┼──────────┼──────────┼───────────┼──────────┤
│         16 │      500 │  124,746 │   507,754 │    4.07× │
├────────────┼──────────┼──────────┼───────────┼──────────┤
│         16 │  100,000 │  134,469 │   484,406 │    3.60× │
└────────────┴──────────┴──────────┴───────────┴──────────┘{code}
2) e2e test, write single row (easy-stress keyvalue write-only workload)
CPU is spent in Reservoir logic according to async-profiler cpu flamegraph:
before : 8.05% [^histogram_buffer_final_before_cpu.html]
after  : 1.85% [^histogram_buffer_final_after_cpu.html]

 

 

> Reduce DecayingEstimatedHistogramReservoir update cost
> ------------------------------------------------------
>
>                 Key: CASSANDRA-20333
>                 URL: https://issues.apache.org/jira/browse/CASSANDRA-20333
>             Project: Apache Cassandra
>          Issue Type: Improvement
>          Components: Observability/Metrics
>            Reporter: Dmitry Konstantinov
>            Assignee: Maxim Muzafarov
>            Priority: High
>             Fix For: 7.x
>
>         Attachments: Cumulative Occurrences - MutationStage.png, Cumulative 
> Occurrences - ReadStage.png, Cumulative occurrences - 
> Native-Transport-Requests.png, 
> Flamegraph-getBucketsThreadLocal-Contirbution.png, 
> Perf-Conribution-Histogram-Update.png, 
> flame-jmh-20333-optlock-210425-nowl-1-volatile.html, 
> flamegraph-20333-optlock-210425-warmed-nowl-1-volatile.html, 
> flamegraph-trunk-final-warmed.html, histogram_buffer_final_after_cpu.html, 
> histogram_buffer_final_before_cpu.html
>
>          Time Spent: 1h 10m
>  Remaining Estimate: 0h
>
> Based on the discussions in CASSANDRA-20250
> [~benedict]:
> {quote}We can probably improve our reservoir performance if we want to, 
> perhaps in a follow-up patch? For instance, we could have a small 
> thread-local buffer of (time, latency) pairs that we periodically flush 
> together, so that we amortise the memory latency costs. Or we could explore 
> maintaining a per-thread HdrHistogram, that we periodically flush. This would 
> be a good time to explore fully migrating to HdrHistogram, as it has built-in 
> merge semantics iirc. I am not sure what the decayed version would look like 
> there, but I am certain we could maintain a separate decayed HdrHistogram.
> Having a thread-local buffer of updates we intend to flush to the histograms 
> would amortise the latency penalties without fundamentally redesigning 
> anything (as well as reducing contention).
> Other possibilities might include e.g. changing the bucket distribution so we 
> don't need a LUT for computing lg2, although the above would gracefully 
> handle any contribution this has as well.
> {quote}
>  
> Other ideas about squeezing extra bits from the current design:
>  * bucket id can be calculated once (currently we do it 2 times for decaying 
> and current buckets), like:
> {code:java}
> int stripe = (int) (Thread.currentThread().getId() & (nStripes - 1));
> int bucket = stripedIndex(index, stripe);
> rescaledDecayingBuckets.update(bucket, now);
> updateBucket(buckets, bucket, 1); {code}
>  * for histograms on highly loaded paths we can use another number of stripes 
> (by default it is 2, we can set for example 4 for them)
>  * I noticed some variation in performance for a micro-benchmark (existing 
> one: DecayingEstimatedHistogramBench) depending on what exact value for 
> distributionPrime is used (but I need to double check it)
>  * forwardDecayWeight function depends on SampledClock value, so we can try 
> to recalculate the weight only when time is changed



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

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

Reply via email to