tisonkun opened a new pull request, #313:
URL: https://github.com/apache/datasketches-rust/pull/313

   Public operations, storage management, query helpers, and serialization are 
interleaved in `sketch.rs`, making the implementation difficult to follow. 
Group the public APIs first, keep complete algorithms together, and name query 
helpers around interpolation intervals and cumulative weights. 
Digest-independent interpolation becomes a module-level function; construction 
remains associated with the type.
   
   Keep codec implementation and format constants together at the end of the 
same module to preserve its compilation boundary. Public APIs, query 
arithmetic, validation, and serialization formats are unchanged.
   
   ## Performance methodology
   
   Compared baseline `8bb74d7fe77d769ff9c20d2a3b8e8296928239ea` with 
`354d742cfffcdfc886d805a08f794d3b3661e655` on an Apple M4 Max, macOS 26.6.2, 
Rust 1.99.1 (`58cf8f9c3`, LLVM 23.1.1), using default release settings and the 
`tdigest` feature.
   
   - Use the same standalone harness and existing 
`benchmarks/tdigest/support.rs` fixtures for both builds. Timing uses `Instant` 
and `std::hint::black_box`, with the system allocator and allocation profiling 
disabled. Builds have no custom Rust flags. This is a local desktop run, 
without CPU affinity or an isolated host.
   - Calibrate repetitions **only on the baseline**, targeting a whole batch of 
approximately **300 ms**. Freeze that count for the baseline, candidate, and 
control; do not independently scale each implementation or retune counts after 
warmup. This differs from setting Divan's overall minimum runtime to 300 ms.
   - Run two warmup rounds at one fifth of the fixed count, then seven measured 
rounds at the full count. Each case runs in a fresh process. Reverse 
baseline/control/candidate order on alternating rounds.
   - Generate inputs and prepare prebuilt digests before timing. The general 
query fixture uses `k=200` and 100,000 deterministic values. CDF/PMF split 
points and sorted quantile ranks are evenly spaced in `(0, 1)`. Output 
allocation and destruction are included.
   - The merge-only case clones input pools of up to 256 compressed digests 
outside timing; merging and destroying consumed states are inside timing. Its 
control includes cloning inside timing. Each source digest represents 100,000 
values.
   - Serialized aggregation starts with a prepared eight-value image: decode, 
owned collection, serialize, decode, and query the 0.9 quantile. Building that 
fixture is excluded. The build and build-plus-CDF rows separately include 
ingestion of 100,000 values and subsequent destruction, with pending 
compression included in the CDF lifecycle.
   - Include an **A/A control copied byte-for-byte from the baseline 
executable**, measured at the same fixed count. Source and executable hashes 
were checked against the measured revision.
   
   ## Results
   
   Times below are **whole-batch medians**, not single-call latencies. `Paired 
change = median_i(100 * (PR_i / main_i - 1))`; A/A applies the same formula to 
the identical baseline copy. Positive means slower. A median of paired ratios 
need not equal the ratio of the displayed median times. Counts were fixed after 
baseline calibration, so measured baseline totals can drift from the 300 ms 
target.
   
   | Workload per repetition | Repetitions per sample | Main (ms) | PR (ms) | 
Paired change | A/A change |
   | --- | ---: | ---: | ---: | ---: | ---: |
   | Frozen Rank | 12,241,214 | 303.1 | 304.1 | +0.68% | +1.05% |
   | Mutable Rank, compressed state | 10,859,058 | 282.4 | 278.3 | -0.53% | 
-1.16% |
   | 256 mixed Rank queries | 43,990 | 302.7 | 311.3 | +1.95% | -0.99% |
   | Rank on six values | 50,228,751 | 292.9 | 285.9 | -2.43% | +1.16% |
   | Rank on repeated means | 13,595,031 | 300.7 | 300.4 | +0.00% | -0.77% |
   | CDF, 1 split point | 6,172,792 | 301.0 | 301.1 | -0.47% | -0.53% |
   | CDF, 6 split points | 2,233,848 | 291.0 | 292.7 | +0.01% | +0.84% |
   | CDF, 100 split points | 691,840 | 311.5 | 294.6 | -3.95% | -3.28% |
   | CDF, 512 split points | 148,389 | 299.4 | 292.1 | -1.81% | -0.38% |
   | PMF, 1 split point | 6,142,334 | 299.6 | 301.0 | +0.19% | -0.39% |
   | PMF, 6 split points | 2,198,425 | 296.7 | 295.1 | -0.33% | +0.67% |
   | PMF, 100 split points | 656,161 | 296.1 | 287.5 | -2.94% | +1.35% |
   | PMF, 512 split points | 143,748 | 296.8 | 291.7 | -2.16% | -1.93% |
   | 100 sorted quantiles | 974,627 | 294.3 | 299.0 | +1.62% | -0.68% |
   | Merge, input cloning excluded | 149,126 | 303.1 | 306.9 | +1.28% | -0.09% |
   | Clone + merge | 145,630 | 300.3 | 301.6 | +0.71% | +1.78% |
   | Decode 512 states of 64 values | 3,666 | 300.3 | 301.0 | +0.24% | -0.33% |
   | Decode one 64-value state | 1,693,951 | 246.5 | 250.6 | +0.88% | -0.02% |
   | Serialize a prebuilt digest | 1,209,004 | 304.4 | 307.0 | -0.09% | -0.57% |
   | Serialized eight-value aggregation | 2,160,388 | 294.7 | 295.0 | -0.26% | 
+0.37% |
   | Build from 100,000 values | 111 | 299.7 | 296.1 | -1.40% | -1.56% |
   | Build 100,000 values + CDF100 | 109 | 296.5 | 296.8 | -0.02% | -0.69% |
   
   The largest paired median increase in this run is **1.95% for mixed Rank**, 
with all seven paired increases between **0.62% and 2.88%**. The same 
workload's A/A paired range is **−2.31% to +4.32%**; across workloads, A/A 
paired medians range from **−3.28% to +1.78%**. The control demonstrates 
measurement variability, but does not prove that every candidate difference is 
noise or exclude a small performance effect. No speedup or universal 
zero-regression guarantee is claimed.
   
   As a separate code-generation check, the CDF kernel, pairwise merge, 
compression loop, centroid merge, and decoder have matching normalized 
instruction sequences after resolving external call targets and excluding 
relocation addresses and alignment padding. The decoder contains 1,666 
instructions in each build. This checks the named kernels, not every inlined 
caller or workload.
   
   ## Validation
   
   - `cargo x check`
   - `cargo x test`: 778 passing tests, including doctests and serialization 
compatibility tests
   - `cargo x lint`
   
   <details>
   <summary>Standalone fixed-work benchmark harness and runner</summary>
   
   These temporary benchmark sources are provided here for reproducing the 
measurement protocol; they are not part of the library diff. Create an 
independent Cargo package named `tdigest-probe` (version `0.0.0`, edition 
`2024`) with a path dependency on the checkout's `datasketches` package, 
enabling only `tdigest`. The measured manifest also declares optional `divan = 
"=0.1.21"` and `profile = ["dep:divan"]`; leave that feature disabled. Copy the 
checkout's `benchmarks/tdigest/support.rs` unchanged to `src/support.rs`, and 
use the following as `src/main.rs`.
   
   Build the exact same harness against each revision with `cargo +1.99.1 build 
--release`. Keep the executables immutable as `bin/baseline` and 
`bin/candidate`, then copy `bin/baseline` to `bin/control`. Run the Python 
driver beside `bin/` as `python3 run.py baseline,control,candidate results`. 
The QoS call in this driver matches the macOS measurement host; input 
preparation and process startup are outside the Rust timer.
   
   ```rust
   use std::hint::black_box;
   use std::time::{Duration, Instant};
   use datasketches::tdigest::{TDigest, TDigestMut};
   #[allow(dead_code)]
   mod support;
   #[cfg(feature = "profile")]
   #[global_allocator]
   static ALLOC: divan::AllocProfiler = divan::AllocProfiler::system();
   fn measure<T>(count: usize, mut op: impl FnMut() -> T) -> Duration {
       let start = Instant::now();
       for _ in 0..count { black_box(op()); }
       start.elapsed()
   }
   fn digest(xs: &[f64]) -> TDigest { support::build_digest(xs) }
   fn main() {
       let args: Vec<_> = std::env::args().collect();
       let case = &args[1];
       let count: usize = args[2].parse().unwrap();
       let elapsed = match case.as_str() {
           "rank" => { let d=support::prepared_digest(); measure(count, || 
black_box(&d).rank(black_box(0.53125)).unwrap()) }
           "rank_mut" => { let mut d=support::prepared_digest().unfreeze(); 
measure(count, || black_box(&mut d).rank(black_box(0.53125)).unwrap()) }
           "rank_mixed" => { let d=support::prepared_digest(); let 
xs=support::values(256); measure(count, || { for &x in black_box(&xs) 
{black_box(black_box(&d).rank(black_box(x)).unwrap());} }) }
           "rank_small" => { let d=digest(&[0.,1.,2.,3.,4.,5.]); measure(count, 
|| black_box(&d).rank(black_box(2.5)).unwrap()) }
           "rank_repeated" => { let xs=(0..100_000).map(|i|(i%8) as 
f64).collect::<Vec<_>>(); let d=digest(&xs); measure(count, || 
black_box(&d).rank(black_box(3.5)).unwrap()) }
           "quantiles_100" => { let d=support::prepared_digest(); let 
qs=(1..=100).map(|i|i as f64/101.).collect::<Vec<_>>(); measure(count, || 
black_box(&d).quantiles(black_box(&qs)).unwrap()) }
           "merge" | "merge_clone" => {
               let xs=support::values(200_000);
               let mut left=support::build_mut_digest(&xs[..100_000]); let mut 
right=support::build_mut_digest(&xs[100_000..]);
               black_box(left.rank(0.5).unwrap()); 
black_box(right.rank(0.5).unwrap());
               if case=="merge_clone" { measure(count, || {let mut 
d=black_box(&left).clone(); d.merge(black_box(&right)); d}) }
               else {
                   let mut elapsed=Duration::ZERO; let mut done=0;
                   while done<count {
                       let batch=(count-done).min(256);
                       let 
inputs=(0..batch).map(|_|left.clone()).collect::<Vec<_>>();
                       let start=Instant::now();
                       for mut d in inputs {d.merge(black_box(&right)); 
black_box(d);}
                       elapsed+=start.elapsed(); done+=batch;
                   }
                   elapsed
               }
           }
           "decode_groups" => {
               let bytes=support::serialized_partial_digests_with(200,512,64);
               measure(count, || 
black_box(&bytes).iter().map(|b|TDigestMut::deserialize(black_box(b)).unwrap()).collect::<Vec<_>>())
           }
           "decode_one" => {
               let 
bytes=support::serialized_partial_digests_with(200,1,64).pop().unwrap();
               measure(count, || 
TDigestMut::deserialize(black_box(&bytes)).unwrap())
           }
           "serialize" => { let d=support::prepared_digest(); measure(count, || 
black_box(&d).serialize()) }
           "aggregate_8" => {
               let xs=support::values(8); let 
bytes=support::build_mut_digest(&xs).serialize();
               measure(count, || {
                   let mut 
merged=std::iter::once(black_box(&bytes)).map(|b|TDigestMut::deserialize(b)).collect::<Result<TDigestMut,_>>().unwrap();
                   let result=merged.serialize();
                   
TDigestMut::deserialize(black_box(&result)).unwrap().quantile(black_box(0.9)).unwrap()
               })
           }
           "update" => { let xs=support::values(100_000); measure(count, || 
support::build_mut_digest(black_box(&xs))) }
           "lifecycle" => { let xs=support::values(100_000); let 
qs=(1..=100).map(|i|i as f64/101.).collect::<Vec<_>>(); measure(count, || 
support::build_mut_digest(black_box(&xs)).cdf(black_box(&qs)).unwrap()) }
           other if other.starts_with("cdf_") || other.starts_with("pmf_") => {
               let d=support::prepared_digest(); let 
len:usize=other[4..].parse().unwrap();
               let qs=(1..=len).map(|i|i as f64/(len+1) as 
f64).collect::<Vec<_>>();
               if other.starts_with("cdf_") {measure(count, || 
black_box(&d).cdf(black_box(&qs)).unwrap())}
               else {measure(count, || 
black_box(&d).pmf(black_box(&qs)).unwrap())}
           }
           _ => panic!("unknown case"),
       };
       println!("{}", elapsed.as_nanos());
   }
   ```
   
   ```python
   from pathlib import Path
   import subprocess,json,statistics,random,ctypes,sys,hashlib
   root=Path(__file__).parent
   assert ctypes.CDLL(None).pthread_set_qos_class_self_np(0x19,0)==0
   variants=sys.argv[1].split(',') if len(sys.argv)>1 else 
['baseline-system','initial-system']
   tag=sys.argv[2] if len(sys.argv)>2 else 'system'
   
cases=['rank','rank_mut','rank_mixed','rank_small','rank_repeated','cdf_1','cdf_6','cdf_100','cdf_512','pmf_1','pmf_6','pmf_100','pmf_512','quantiles_100','merge','merge_clone','decode_groups','decode_one','serialize','aggregate_8','update','lifecycle']
   if len(sys.argv)>3:cases=sys.argv[3].split(',')
   def measure(name,case,n):return 
int(subprocess.check_output([str(root/'bin'/name),case,str(n)],text=True))
   counts={}
   for c in cases:
    n=1
    while True:
     ns=measure(variants[0],c,n)
     if ns>=30_000_000:break
     n=max(n+1,min(n*10,round(n*50_000_000/max(ns,1))))
    n=max(1,round(n*300_000_000/ns))
    ns=measure(variants[0],c,n)
    n=max(1,round(n*300_000_000/ns))
    counts[c]=n
    print('calibrated',c,n,flush=True)
   (root/(tag+'-counts.json')).write_text(json.dumps(counts,indent=2))
   rows=[]
   for i in range(-2,7):
    row={}
    for c in cases:
     order=variants.copy()
     if i%2:order.reverse()
     for name in order:
      ns=measure(name,c,counts[c] if i>=0 else max(1,counts[c]//5))
      row.setdefault(name,{})[c]=ns
    if i>=0:
     
rows.append(row);(root/(tag+'-rounds.json')).write_text(json.dumps(rows,indent=2))
    print('round',i,'complete',flush=True)
   results=[]
   for c in cases:
    base=variants[0]
    med={n:statistics.median(r[n][c] for r in rows)/1e6 for n in variants}
    changes={n:statistics.median((r[n][c]/r[base][c]-1)*100 for r in rows) for 
n in variants}
    ranges={n:[min((r[n][c]/r[base][c]-1)*100 for r in 
rows),max((r[n][c]/r[base][c]-1)*100 for r in rows)] for n in variants}
    
results.append({'case':c,'count':counts[c],'median_ms':med,'changes_percent':changes,'paired_range_percent':ranges})
    print(c,counts[c],{n:round(v,3) for n,v in med.items()},{n:round(v,2) for 
n,v in changes.items()},flush=True)
   (root/(tag+'-results.json')).write_text(json.dumps(results,indent=2))
   ```
   
   </details>
   


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