Hi all,

Two recent datasketches-cpp PRs from Rusty Conover, #558 (KLL) and #561
(t-digest), change those sketches to allocate their item storage on
first use, not in the constructor. The motivation is real, but the
choice affects a number of sketches in every language, so I'd like us to
agree on one convention before merging either PR.


THE PROBLEM
-----------

  Several sketches allocate their full working storage when they are
  constructed, before any item arrives:

    * KLL: k items (1.6 KB for the default k = 200 with doubles)
    * t-digest: about 10 KB for k = 100 with doubles
    * REQ: the first compactor's buffer

  That is wasted when an application creates many sketches that stay
  empty or tiny. Query engines do this all the time: one aggregate state
  per group in a GROUP BY, or short-lived partial states that are only
  merged into. Rusty measured a KLL aggregate over 2M groups in the
  DuckDB DataSketches extension using tens of GB, for this reason alone.

  The pattern is the same in Java (KllHeapDoublesSketch, ReqSketch,
  TDigestDouble), and Python inherits it from C++. Classic quantiles
  already does the right thing in both C++ and Java: it starts with a
  small buffer and grows.


PROPOSAL
--------

  1. In-memory (heap) sketches allocate item storage on first use,
     meaning the first update or the first non-empty merge, not in the
     constructor. Storage then grows as it does today.

  2. Users who want the storage up front call an explicit method right
     after construction:
       C++:  reserve()   (after std::vector::reserve)
       Java: reserve()   (same name, for parity; ensureCapacity() is
             the alternative if people prefer the Java collections name)
             (I prefer ensureCapacity(), but open to other views)
     Other languages pick the closest idiomatic name.
     This keeps the choice with the user without adding constructor
     overloads (C++ constructors already take comparator and allocator
     arguments).

  3. Off-heap / direct sketches (Java MemorySegment) are unchanged. The
     caller supplies and sizes that memory, so there is nothing to defer.

  4. Behavior that changes, and must be documented in each release:
       * An allocation failure moves from the constructor to the first
         update or merge (std::bad_alloc in C++, OutOfMemoryError in
         Java). In C++, those calls must keep the basic exception
         guarantee: if the call fails, the sketch is still valid and
         nothing leaks. Tests should check this with an allocator that
         fails on purpose.
       * Results, the serialized format and cross-language compatibility
         are unchanged. Languages can adopt this in different releases.
       * Allocating on first use does have a small performance cost in
         that every update must check for sufficient capacity.

  5. The sketches in scope, as far as I know today:
       C++:  KLL (#558), t-digest (#561), REQ
       Java: KLL (heap), t-digest, REQ
       Go, Rust: to be checked by their maintainers
     Corrections and additions welcome.


WHO DOES THE WORK
-----------------

  If this passes, I will make the changes for C++ and Java, starting from
  Rusty's #558 and #561, reworked to the convention if needed. Python
  follows C++ and needs only bindings for reserve(). For Go and Rust,
  volunteers are very welcome. The change is self-contained for each
  sketch.

  Until this resolves, ds-cpp PRs #558 and #561 are on hold.


This is a lazy consensus. If there are no objections by
Nov 6, 5 PM, PST or Nov 7, 1 AM UTC, one week from sending>, I'll take
it as accepted.
Comments on the API names, or on the default (lazy, versus eager with
an opt-in), are especially welcome.

Thanks,
Lee

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

Reply via email to