Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
# p99.Rust CHANGES <!-- omit in toc -->


## 0.0.2 - 12th July 2026

#### Changes

* Added opt-in crate feature **`binary-scaling`** that replaces integer division with $2^{32}$ fixed-point binary scaling for all integer-based percentile queries (`value_at_p90()`, `value_at_p95()`, `value_at_p99()`, etc.), achieving a ~1.5x to 2x speedup with a small loss of accuracy;
* Added **`null-feature`** -- a no-op feature that has no effect but simplifies driver scripts that conditionally pass features;


## 0.0.1 - 26th June 2026

FIRST PUBLIC RELEASE
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 17 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ license = "BSD-3-Clause"
name = "p99"
readme = "README.md"
repository = "https://github.com/synesissoftware/p99.Rust"
version = "0.0.1"
version = "0.0.2"


# ##########################################################
Expand All @@ -47,7 +47,22 @@ path = "examples/build_histogram.rs"

[features]

default = []
default = [
]

# General features:
#
# - "null-feature" - a feature that has no effect (and, thus, is useful for simplifying driver scripts);

null-feature = []

# Crate-specific features:
#
# - "binary-scaling" - uses binary scaling to calculate target rank, achieving higher performance with a small loss of accuracy;


binary-scaling = [
]


# ##########################################################
Expand Down
48 changes: 47 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ Low-cost generation of performance percentiles (p50, p90, p99, p99.9, etc.).

* **Logarithmic Precision**: To achieve zero allocation and constant-time operations, `Histogram` sacrifices exact precision. It does not store individual event times. Instead, values are grouped into logarithmic buckets.
* **Approximation**: Percentile values are approximated using linear interpolation within the bucket boundaries. For very large values, the bucket width is wider, which leads to a wider approximation range. However, for low-latency performance measurements where precision is needed most (the lower nanosecond ranges), the buckets are extremely narrow (e.g., 1ns, 2ns, 4ns wide), providing exceptional resolution.
* **`binary-scaling` Accuracy**: When the `binary-scaling` feature is enabled, the percentile target rank is computed using a $2^{32}$ fixed-point approximation. The pre-encoded multiplier for each percentile (e.g., `3_865_470_566 >> 32` ≈ `0.9000` for p90) differs from the true decimal value by less than $10^{-9}$, which is far below the approximation error introduced by the logarithmic bucketing itself. In practice this has no measurable impact on percentile accuracy.


## Installation
Expand All @@ -80,6 +81,12 @@ Reference in **Cargo.toml** in the usual way:
p99 = { version = "0" }
```

To enable the optional binary-scaling optimization:

```toml
p99 = { version = "0", features = ["binary-scaling"] }
```


## Components

Expand All @@ -95,7 +102,43 @@ No public enumerations are defined at this time.

### Features

No public crate-specific features are defined at this time.
The following crate features are available:

* **`binary-scaling`** *(opt-in)*: Replaces integer division in the integer-based percentile methods (`value_at_p90()`, `value_at_p95()`, `value_at_p99()`, etc.) with $2^{32}$ fixed-point binary scaling. Each percentile multiplier (e.g., `0.90` for p90) is pre-encoded as a `u32` constant and the target rank is computed via a single multiplication and a 32-bit right-shift, avoiding the cost of integer division entirely. This yields a **~1.5x to 2x speedup** for percentile queries with a negligible loss of accuracy (the scaled multiplier differs from the true value by less than $10^{-9}$). The generic `value_at_percentile(f64)` method is unaffected by this feature.

* **`null-feature`** *(opt-in)*: A no-op feature that has no effect on the compiled library. It exists to simplify driver scripts and CI pipelines that conditionally pass `--features` flags, allowing a feature list to always be present even when no real features are needed.

#### Enabling `binary-scaling`

Add the feature in your **Cargo.toml**:

```toml
[dependencies]
p99 = { version = "0", features = ["binary-scaling"] }
```

Or, when building from the command line:

```bash
# Default (standard integer division)
cargo run --example build_histogram

# With binary scaling enabled
cargo run --example build_histogram --features binary-scaling
```

#### Benchmark Results

Measured with [**criterion**](https://github.com/bheisler/criterion.rs) on 100k events (Apple M-series, release profile). Only the integer-based percentile methods are affected; the generic `value_at_percentile(f64)` method is unchanged.

| Method | Default | `binary-scaling` | Improvement |
|---|---:|---:|---:|
| `value_at_p90()` | 23.25 ns | 21.63 ns | **-7.0%** |
| `value_at_p99()` (dense) | 16.52 ns | 14.88 ns | **-10.4%** |
| `value_at_p99()` (wide) | 23.39 ns | 21.55 ns | **-7.9%** |
| `value_at_p99_99()` | 23.32 ns | 21.64 ns | **-7.2%** |

Methods using simple fractional multipliers (p50 = 1/2, p75 = 3/4) already compile to bit-shifts without this feature, so they show no change.


### Functions
Expand Down Expand Up @@ -184,6 +227,9 @@ cargo run --example build_histogram

# Run with 1000 tries
P99_TRIES=1000 cargo run --example build_histogram

# Run with binary-scaling enabled (faster percentile queries)
cargo run --example build_histogram --features binary-scaling
```


Expand Down
2 changes: 1 addition & 1 deletion TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@
## TODOs

- [x] ~~~`Debug` form~~~;
- [ ] binary scaling;
- [x] ~~~binary scaling~~~;

72 changes: 56 additions & 16 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -295,8 +295,13 @@ impl Histogram {
/// events; otherwise, returns `None`.
#[inline(always)]
pub fn value_at_p90(&self) -> Option<u64> {
let target_rank = (self.event_count as u128 * 90) / 100;
let r = self.value_at_target_rank_impl(target_rank as u64);
#[cfg(feature = "binary-scaling")]
let target_rank = ((self.event_count as u128 * 3_865_470_566) >> 32) as u64; // multiplier: 0.8999999999068677

#[cfg(not(feature = "binary-scaling"))]
let target_rank = ((self.event_count as u128 * 90) / 100) as u64;

let r = self.value_at_target_rank_impl(target_rank);

r
}
Expand All @@ -309,8 +314,13 @@ impl Histogram {
/// events; otherwise, returns `None`.
#[inline(always)]
pub fn value_at_p95(&self) -> Option<u64> {
let target_rank = (self.event_count as u128 * 95) / 100;
let r = self.value_at_target_rank_impl(target_rank as u64);
#[cfg(feature = "binary-scaling")]
let target_rank = ((self.event_count as u128 * 4_080_218_931) >> 32) as u64; // multiplier: 0.9499999999534339

#[cfg(not(feature = "binary-scaling"))]
let target_rank = ((self.event_count as u128 * 95) / 100) as u64;

let r = self.value_at_target_rank_impl(target_rank);

r
}
Expand All @@ -323,8 +333,13 @@ impl Histogram {
/// events; otherwise, returns `None`.
#[inline(always)]
pub fn value_at_p99(&self) -> Option<u64> {
let target_rank = (self.event_count as u128 * 99) / 100;
let r = self.value_at_target_rank_impl(target_rank as u64);
#[cfg(feature = "binary-scaling")]
let target_rank = ((self.event_count as u128 * 4_252_017_623) >> 32) as u64; // multiplier: 0.9899999999906868

#[cfg(not(feature = "binary-scaling"))]
let target_rank = ((self.event_count as u128 * 99) / 100) as u64;

let r = self.value_at_target_rank_impl(target_rank);

r
}
Expand All @@ -337,8 +352,13 @@ impl Histogram {
/// events; otherwise, returns `None`.
#[inline(always)]
pub fn value_at_p99_5(&self) -> Option<u64> {
let target_rank = (self.event_count as u128 * 995) / 1_000;
let r = self.value_at_target_rank_impl(target_rank as u64);
#[cfg(feature = "binary-scaling")]
let target_rank = ((self.event_count as u128 * 4_273_492_460) >> 32) as u64; // multiplier: 0.9950000001117587

#[cfg(not(feature = "binary-scaling"))]
let target_rank = ((self.event_count as u128 * 995) / 1_000) as u64;

let r = self.value_at_target_rank_impl(target_rank);

r
}
Expand All @@ -351,8 +371,13 @@ impl Histogram {
/// events; otherwise, returns `None`.
#[inline(always)]
pub fn value_at_p99_9(&self) -> Option<u64> {
let target_rank = (self.event_count as u128 * 999) / 1_000;
let r = self.value_at_target_rank_impl(target_rank as u64);
#[cfg(feature = "binary-scaling")]
let target_rank = ((self.event_count as u128 * 4_290_672_329) >> 32) as u64; // multiplier: 0.9990000000689179

#[cfg(not(feature = "binary-scaling"))]
let target_rank = ((self.event_count as u128 * 999) / 1_000) as u64;

let r = self.value_at_target_rank_impl(target_rank);

r
}
Expand All @@ -365,8 +390,13 @@ impl Histogram {
/// events; otherwise, returns `None`.
#[inline(always)]
pub fn value_at_p99_99(&self) -> Option<u64> {
let target_rank = (self.event_count as u128 * 9_999) / 10_000;
let r = self.value_at_target_rank_impl(target_rank as u64);
#[cfg(feature = "binary-scaling")]
let target_rank = ((self.event_count as u128 * 4_294_537_799) >> 32) as u64; // multiplier: 0.9998999999370426

#[cfg(not(feature = "binary-scaling"))]
let target_rank = ((self.event_count as u128 * 9_999) / 10_000) as u64;

let r = self.value_at_target_rank_impl(target_rank);

r
}
Expand All @@ -379,8 +409,13 @@ impl Histogram {
/// events; otherwise, returns `None`.
#[inline(always)]
pub fn value_at_p99_999(&self) -> Option<u64> {
let target_rank = (self.event_count as u128 * 99_999) / 100_000;
let r = self.value_at_target_rank_impl(target_rank as u64);
#[cfg(feature = "binary-scaling")]
let target_rank = ((self.event_count as u128 * 4_294_924_346) >> 32) as u64; // multiplier: 0.9999899999238551

#[cfg(not(feature = "binary-scaling"))]
let target_rank = ((self.event_count as u128 * 99_999) / 100_000) as u64;

let r = self.value_at_target_rank_impl(target_rank);

r
}
Expand All @@ -393,8 +428,13 @@ impl Histogram {
/// events; otherwise, returns `None`.
#[inline(always)]
pub fn value_at_p99_999_9(&self) -> Option<u64> {
let target_rank = (self.event_count as u128 * 999_999) / 1_000_000;
let r = self.value_at_target_rank_impl(target_rank as u64);
#[cfg(feature = "binary-scaling")]
let target_rank = ((self.event_count as u128 * 4_294_963_001) >> 32) as u64; // multiplier: 0.9999989999923855

#[cfg(not(feature = "binary-scaling"))]
let target_rank = ((self.event_count as u128 * 999_999) / 1_000_000) as u64;

let r = self.value_at_target_rank_impl(target_rank);

r
}
Expand Down
Loading