Latte is a single-header C++ telemetry library designed for high‑frequency trading, game engines, and real‑time systems where measurement overhead must be measured in nanoseconds rather than microseconds. Latte measures CPU cycles using x86_64 timestamp counters (RDTSC / RDTSCP) and stores samples in per‑thread fixed‑size ring buffers for later reporting.
Zero allocations in steady state. The first
Start(id)per thread allocates a ring buffer entry in the per-thread map; all subsequent calls to the same ID are allocation-free and lock-free.
Latte is header-only. Include the file in your project. It requires a C++17 compliant compiler and an x86_64 architecture.
#include "Latte.hpp"Use string literals directly as identifiers. No pre-registration is required.
void ProcessOrder() {
Latte::Fast::Start(__func__);
// Core logic execution here
Latte::Fast::Stop(__func__);
}Latte provides three levels of timing precision. They differ in ordering guarantees and overhead, choose the right one for your measurement context.
| Mode | Intrinsic(s) | Serialization | Best for |
|---|---|---|---|
Fast |
__rdtsc() |
None | Coarse, high‑frequency polling where every cycle matters (e.g., Hot Path). |
Mid |
__rdtscp() |
Partial barrier (waits on prior instructions only) | Default for function‑level profiling, balanced accuracy and overhead. |
Hard |
lfence + __rdtscp() and __rdtscp() + lfence |
Full (LFENCE + serialize) | Measuring tiny snippets (few dozen cycles) or when out‑of‑order execution could distort deltas. |
The framework supports up to 64 active overlapping slots per thread.
Latte::Fast::Start("Frame_Total");
Latte::Mid::Start("Physics_Engine");
// Core logic execution
Latte::Mid::Stop("Physics_Engine");
Latte::Fast::Stop("Frame_Total");LATTE_PULSE("ID") records the cycle delta between successive calls on the same thread.
Implementation details:
- The macro uses static
thread_localpointers to aRingBufferand alasttimestamp, avoiding repeated map lookups after the first call. - First call per thread: initialises the buffer pointer and stores
RDTSC()aslast– no sample is pushed. - Subsequent calls: compute
now - last, push the delta into the ring buffer (with a fixed calibration keyInternal::CALIB_KEY_PULSE), and updatelast.
The recorded delta represents the time span between two consecutive
LATTE_PULSEinvocations, which can be used to measure loop iteration time or polling frequency.
for (;;) {
// ... poll / process ...
LATTE_PULSE("Toroidal_Record");
}Snapshot("ID") returns raw cycle samples collected so far for a given ID, aggregated across threads.
std::vector<Latte::Cycles> samples = Latte::Snapshot("Physics_Engine"); // vec<uint64_t>Send output to any std::ostream at the conclusion of the execution period.
Latte::DumpToStream(std::ostream& os,
Latte::Parameter::Unit unit,
Latte::Parameter::Data data_mode);Defaults:
unit = Latte::Parameter::Cycledata_mode = Latte::Parameter::Raw
Common usage:
// Raw cycles (built-in defaults)
Latte::DumpToStream(std::cout);
// Calibrated time (ns/us/ms formatting) with overhead correction
Latte::DumpToStream(std::cout,
Latte::Parameter::Time,
Latte::Parameter::Calibrated);Calibration and overhead:
- Time formatting uses an internal
cycles_per_ns. - When calibration is active,
DumpToStream()subtracts measured instrumentation overhead from each sample before computing statistics (conceptually:v' = v - overhead). - When calibration is active,
DumpToStream()prints a secondary table labeledOVERHEAD H[Start] x W[Stop]with measured overhead for each Start/Stop mode permutation.
Mixed-mode calibration:
- The per-thread stack stores the capture Mode (Fast/Mid/Hard) alongside the timestamp.
- On
Stop(), calibration/overhead selection is keyed by the(start_mode, stop_mode)pair (e.g., Fast×Fast, Fast×Mid, Hard×Mid).
Latte provides insights into the distribution of latency, focusing on long-tail behavior:
- Average
- Median
- Standard Deviation
- Skewness
- Min
- Max
- Range (Δ)
- OUTLIER number of filtered samples classified as outlier from IQR
Before computing report statistics, DumpToStream() runs Internal::CleanData over the collected samples for each (thread, id). The cleaning pass:
- Buckets samples into groups of 1 000, records the maximum of each bucket.
- Calculates an IQR (interquartile range) on the bucket maxima to determine a cutoff.
- Samples above the cutoff are counted as
OUTLIERand excluded from statistics. - Remaining samples are sorted and used for median, mean, stddev, skew, min, max, and range.
This bucket‑max IQR method is more robust against long‑tail outliers than a raw IQR.
#==============================================================================================================#
| LATTE TELEMETRY [TIME][CAL] |
#==============================================================================================================#
| SELF-OFFSET H[Start] x W[Stop] |
| F M H |
| F 0.21 ns 10.02 ns 10.02 ns |
| M 0.21 ns 10.02 ns 10.02 ns |
| H 0.21 ns 10.02 ns 10.02 ns |
| PULSE 10.02 ns |
|--------------------------------------------------------------------------------------------------------------|
| COMPONENT SAMPLES AVG MEDIAN STD DEV SKEW MIN MAX RANGE OUTLIER |
|--------------------------------------------------------------------------------------------------------------|
| DP_Build_Total 1 38.07 s 38.07 s 0.00 ns 0.00 38.07 s 38.07 s 0.00 ns 0 |
| DP_StateLoop 65536 31.15 us 30.96 us 1.42 us 12.13 29.32 us 95.10 us 65.78 us 0 |
| Sim_Tick_Total 4997 226.79 ns 220.42 ns 76.04 ns 1.40 110.21 ns 821.55 ns 711.34 ns 3 |
| Sim_OrderFlow 4997 43.62 ns 29.63 ns 36.61 ns 2.02 9.59 ns 380.51 ns 370.91 ns 3 |
| Sim_AskLoop 1216 1.03 us 711.34 ns 1.06 us 2.14 0.00 ns 8.30 us 8.30 us 0 |
| Sim_BidLoop 1247 1.12 us 751.42 ns 4.21 us 32.56 0.00 ns 145.96 us 145.96 us 0 |
| Sim_RiskPnL 5000 6.24 ns 9.59 ns 4.67 ns -0.57 0.00 ns 19.82 ns 19.82 ns 0 |
#==============================================================================================================#
The
SELF-OFFSETtable shows what Latte measures whenStartandStopare called back-to-back with no work between them (function call overhead only). Row = Start mode, Column = Stop mode. These values are automatically subtracted when usingParameter::Calibrated.
Each (thread, id) owns a fixed‑size ring buffer (alignas(64) for cache‑line isolation).
- Write:
push()stores a newCyclesvalue at the currentheadand advanceshead = (head+1) & BUFFER_MASK. No zeroing is performed – old values are silently overwritten. - Read:
ExtractRaw()andDumpToStream()consider any positive value as a valid sample. (Zero is the initial state and is ignored.) - Because overwrites happen unconditionally, the buffer always contains the most recent
MAX_SAMPLESsamples (some of which may be zero only if fewer thanMAX_SAMPLEShave ever been written). - Default capacity:
MAX_SAMPLES = 65536(BUFFER_PWR = 16). Must stay a power of two for the bitmask wrap.
The default capacity is 65,536 samples per ID per thread:
BUFFER_PWR = 16MAX_SAMPLES = 1 << BUFFER_PWR→65536- wrapping uses a bitmask (
MAX_SAMPLESmust remain a power of two)
Latte uses a per-thread stack to support nesting:
- Maximum depth: 64 (
MAX_ACTIVE_SLOTS)- If the stack is full, extra
Start()calls are ignored (no sample recorded for that scope).
- If the stack is full, extra
Stop()pops the most recentStart()(LIFO).- The
idargument toStop(id)is not validated against the top-of-stack ID.
- The
Stop()on an empty stack returns without recording.
Best practice: always pair Start/Stop in strict LIFO order and pass the same ID for readability.
Sampling (Start/Stop and LATTE_PULSE) is designed for low contention by using per-thread storage.
However DumpToStream and Snapshot are NOT safe to call concurrently with active sampling threads.
- The manager’s mutex only protects the global list of
ThreadStoragepointers – it does not synchronise access to the ring buffers or the per‑threadhistorymap. - Writes (from
Start/Stop/LATTE_PULSE) are performed without locks or atomics for maximum speed. - Concurrent reads while writes happen on the same thread or another thread cause data races (undefined behaviour).
Recommended usage: Call DumpToStream / Snapshot only after all worker threads have stopped instrumenting (e.g., after joining threads, or after a barrier that guarantees no Start/Stop is in flight).
Latte is engineered to solve the "Observer Effect," ensuring that the act of measurement does not significantly distort the performance of the system being observed.
Standard profiling tools often utilize global mutexes or atomic counters that cause cache line contention between CPU cores. Latte utilizes thread_local storage. Each thread records data into its own private buffers, ensuring that measurement never forces one thread to stall for another.
Latte uses const char* IDs so identification is based on the pointer value (address), avoiding string hashing and string comparisons. IDs are used as keys (e.g., std::map<const char*, RingBuffer>); comparisons are pointer comparisons, and lookup cost scales as O(log N) with the number of distinct IDs on the thread.
Important: IDs are compared by pointer value, not by string contents. Only use:
- string literals:
"MyComponent" - stable static storage:
static const char name[] = "MyComponent";
Do not pass temporary std::string::c_str() pointers or stack buffers.
To support deep nesting without linear search overhead, Latte utilizes a per-thread SoA stack.
Start()pushes the ID, timestamp, and capture Mode (Fast/Mid/Hard) to the stack index.Stop()pops the top of the stack, calculates the cycle delta, and carries the start/stop Modes into calibration selection.
This keeps Start/Stop overhead stable with nesting (up to the fixed maximum depth).
Each thread owns a ThreadStorage that keeps per-ID history in an ordered map:
std::map<const char*, RingBuffer> history
On Stop(), Latte looks up the ring buffer for the ID in that map (O(log N) pointer comparisons) and pushes the measured cycle delta into the buffer.
Notes:
- Keys are compared by address (
const char*), so there is no string hashing and nostrcmp. - The container is still a tree (
std::map), so lookup scales asO(log N)with the number of distinct IDs on the thread.
To prevent "False Sharing" and maximize CPU pre-fetcher efficiency, internal buffers are aligned to 64-byte boundaries (alignas(64)). The use of Structure of Arrays instead of Arrays of Structs ensures that only relevant timing data is pulled into the L1 cache, preventing unnecessary memory bandwidth usage.
Latte provides three levels of precision by wrapping x86 intrinsics directly:
- Fast (RDTSC): Lowest overhead. Non-serializing; suitable for general logic.
- Mid (RDTSCP): More ordered than RDTSC.
- Hard (LFENCE + RDTSCP): More serialized; forces stronger ordering.
- Architecture: x86_64 required for
__rdtsc/__rdtscp. - C++ standard: C++17.
- ID Persistence: Use string literals or stable static pointers (
const char*). - Memory Footprint: Reserves space for 65,536 samples per identifier per thread by default (fixed-size ring buffer, overwriting on wrap). Configurable by changing
BUFFER_PWR/MAX_SAMPLES(must stay power-of-two for mask wrap).
Latte.hpp contains GCC/Clang‑specific constructs. To compile under MSVC, you must wrap them with preprocessor conditionals. Example:
#if defined(__GNUC__) || defined(__clang__)
#pragma GCC optimize ("O3")
#define ALWAYS_INLINE __attribute__((always_inline))
#define LIKELY(expr) __builtin_expect(!!(expr), 1)
#elif defined(_MSC_VER)
#define ALWAYS_INLINE __forceinline
#define LIKELY(expr) (expr)
#endifThe intrinsics headers are already handled: <intrin.h> for MSVC, <x86intrin.h> for GCC/Clang.
The library is not portable to non‑x86_64 architectures because it relies on
__rdtsc/__rdtscp.
| Function | Avg (cycles) | Median (cycles) | StdDev (cycles) | Min (cycles) | Max (cycles) | Δ Min-Max (cycles) |
|---|---|---|---|---|---|---|
| __rdtsc | 30.1 | 29.9 | 0.4 | 29.7 | 31.2 | 1.5 |
| __rdtscp | 57.7 | 57.5 | 0.9 | 57.3 | 62.6 | 5.2 |
| _LFENCE | 14.7 | 14.7 | 0.1 | 14.7 | 15.3 | 0.6 |
| Function | Avg (cycles) | Median (cycles) | StdDev (cycles) | Min (cycles) | Max (cycles) | Δ Min-Max (cycles) |
|---|---|---|---|---|---|---|
| Fast::Start+Stop | 60.1 | 60.0 | 0.1 | 59.9 | 60.4 | 0.5 |
| Mid::Start+Stop | 119.8 | 119.7 | 0.4 | 119.9 | 122.7 | 2.8 |
| Hard::Start+Stop | 175.8 | 175.4 | 0.9 | 175.4 | 179.3 | 3.9 |
| LATTE_PULSE | 29.9 | 29.8 | 0.1 | 29.7 | 30.3 | 0.6 |
| Function | Avg (cycles) | Median (cycles) | StdDev (cycles) | Min (cycles) | Max (cycles) | Δ Min-Max (cycles) |
|---|---|---|---|---|---|---|
| std::chrono::now | 153.9 | 153.4 | 0.4 | 153.1 | 156.9 | 3.3 |
Measurements were computed using:
- Batch size: 100 000 iterations per trial
- Trials: 100 independent batch
- Warm-up: 1 independent batch
- Core: Pinned
- Optimization: g++ -O3 -march=native
- CPU: AMD Ryzen 5 7600X 6-Core 4.7 GHz (Boost Clock: 5.3 GHz)
Each function’s latency was measured in CPU cycles using a high-accuracy timer and batched calls, with initial warm-ups to stabilize branch predictors and caches.
For example:
- On a 3.5 GHz core (≈ 3.5 billion cycles/sec), 1 cycle ≈ 0.286 ns.
- On a 4.0 GHz core, 1 cycle ≈ 0.250 ns.
- On a 4.7 GHz core, 1 cycle ≈ 0.213 ns.
These conversions come from the fact that clock rate (in hertz) is the number of cycles per second:
time per cycle = 1 / frequency(in seconds).