Project
Loading...
Searching...
No Matches
RepresentationBench.h
Go to the documentation of this file.
1// Copyright 2019-2026 CERN and copyright holders of ALICE O2.
2// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders.
3// All rights not expressly granted are reserved.
4//
5// This software is distributed under the terms of the GNU General Public
6// License v3 (GPL Version 3), copied verbatim in the file "COPYING".
7//
8// In applying this license CERN does not waive the privileges and immunities
9// granted to it by virtue of its status as an Intergovernmental Organization
10// or submit itself to any jurisdiction.
13
41
42#ifndef ALICEO2_BASE_REPRESENTATIONBENCH_H_
43#define ALICEO2_BASE_REPRESENTATIONBENCH_H_
44
46
47#include "TGeoBBox.h"
48#include "TGeoBoolNode.h"
49#include "TGeoCompositeShape.h"
50#include "TGeoMatrix.h"
51#include "TGeoShape.h"
52#include "TGeoTube.h"
53
54#include <algorithm>
55#include <array>
56#include <chrono>
57#include <cmath>
58#include <cstdint>
59#include <cstdio>
60#include <cstring>
61#include <fstream>
62#include <string>
63#include <vector>
64
65#ifdef __linux__
66#include <malloc.h>
67#include <unistd.h>
68#endif
69
70namespace o2
71{
72namespace cad
73{
74namespace bench
75{
76
79
80// ------------------------------------------------------------------------------------------
81// 1. Timing: several passes, a robust statistic, and the spread
82// ------------------------------------------------------------------------------------------
83
89struct TimingStat {
90 long long callsPerPass = 0;
91 int passes = 0;
92 double medianNsPerCall = 0.;
93 double minNsPerCall = 0.;
94 double maxNsPerCall = 0.;
97 double spread = 0.;
98 uint64_t checksum = 0;
102 double hitFraction = -1.;
103};
104
105namespace detail
106{
109inline uint64_t mix(uint64_t acc, double value)
110{
111 uint64_t bits = 0;
112 std::memcpy(&bits, &value, sizeof(bits));
113 acc ^= bits + 0x9e3779b97f4a7c15ULL + (acc << 6) + (acc >> 2);
114 return acc;
115}
116} // namespace detail
117
125template <typename Pass>
126TimingStat timePasses(long long callsPerPass, int warmupPasses, int passes, Pass&& pass)
127{
128 TimingStat stat;
129 stat.callsPerPass = callsPerPass;
130 if (callsPerPass <= 0 || passes <= 0) {
131 return stat;
132 }
133 for (int i = 0; i < warmupPasses; ++i) {
134 stat.checksum = detail::mix(stat.checksum, static_cast<double>(pass()));
135 }
136 std::vector<double> perPass;
137 perPass.reserve(passes);
138 for (int i = 0; i < passes; ++i) {
139 const auto t0 = std::chrono::steady_clock::now();
140 const uint64_t sum = pass();
141 const auto t1 = std::chrono::steady_clock::now();
142 stat.checksum = detail::mix(stat.checksum, static_cast<double>(sum));
143 perPass.push_back(std::chrono::duration<double, std::nano>(t1 - t0).count() /
144 static_cast<double>(callsPerPass));
145 }
146 std::sort(perPass.begin(), perPass.end());
147 stat.passes = passes;
148 stat.minNsPerCall = perPass.front();
149 stat.maxNsPerCall = perPass.back();
150 stat.medianNsPerCall = perPass[perPass.size() / 2];
151 stat.spread = stat.medianNsPerCall > 0.
152 ? (stat.maxNsPerCall - stat.minNsPerCall) / stat.medianNsPerCall
153 : 0.;
154 return stat;
155}
156
157// ------------------------------------------------------------------------------------------
158// 2. Memory: one exact number and one measured number
159// ------------------------------------------------------------------------------------------
160
176 long long residentBytes = 0;
177 long long heapInUseBytes = 0;
178};
179
181{
182 MemorySnapshot out;
183#ifdef __linux__
184 std::ifstream statm("/proc/self/statm");
185 if (statm) {
186 long long totalPages = 0;
187 long long residentPages = 0;
188 statm >> totalPages >> residentPages;
189 out.residentBytes = residentPages * static_cast<long long>(::sysconf(_SC_PAGESIZE));
190 }
191#if defined(__GLIBC__) && (__GLIBC__ > 2 || (__GLIBC__ == 2 && __GLIBC_MINOR__ >= 33))
192 const struct mallinfo2 info = ::mallinfo2();
193 out.heapInUseBytes = static_cast<long long>(info.uordblks) + static_cast<long long>(info.hblkhd);
194#endif
195#endif
196 return out;
197}
198
200{
201 return {a.residentBytes - b.residentBytes, a.heapInUseBytes - b.heapInUseBytes};
202}
203
210 long long primitives = 0;
211 long long bytes = 0;
212 long long sidecarBytes = 0;
213 std::string formula;
214};
215
218inline long long fileBytes(const std::string& path)
219{
220 std::ifstream in(path, std::ios::binary | std::ios::ate);
221 return in ? static_cast<long long>(in.tellg()) : 0;
222}
223
224// ------------------------------------------------------------------------------------------
225// 3. The sample sets -- built once per part, handed unchanged to every representation
226// ------------------------------------------------------------------------------------------
227
234 std::string partitionedBy;
235 std::vector<Point3D> points;
236 std::vector<char> pointIsInside;
237 std::vector<Ray> outsideRays;
238 std::vector<Ray> insideRays;
239 long long insidePoints = 0;
240};
241
242namespace detail
243{
247struct Lcg {
248 uint64_t state = 88172645463325252ULL;
249 double next()
250 {
251 state = state * 6364136223846793005ULL + 1442695040888963407ULL;
252 return static_cast<double>((state >> 11) & ((1ULL << 53) - 1)) / static_cast<double>(1ULL << 53);
253 }
254};
255} // namespace detail
256
266inline QuerySamples buildQuerySamples(const TGeoShape* reference, const std::string& referenceName,
267 const Point3D& bboxMin, const Point3D& bboxMax, int nPoints,
268 int nRays, uint64_t seed = 20260802ULL, double inflate = 0.12)
269{
270 QuerySamples out;
271 out.partitionedBy = referenceName;
272 detail::Lcg rng{seed};
273 Point3D lo{};
274 Point3D hi{};
275 Point3D centre{};
276 for (int k = 0; k < 3; ++k) {
277 const double half = 0.5 * (bboxMax[k] - bboxMin[k]);
278 centre[k] = 0.5 * (bboxMax[k] + bboxMin[k]);
279 lo[k] = centre[k] - half * (1. + inflate);
280 hi[k] = centre[k] + half * (1. + inflate);
281 }
282 auto drawPoint = [&]() {
283 Point3D p{};
284 for (int k = 0; k < 3; ++k) {
285 p[k] = lo[k] + (hi[k] - lo[k]) * rng.next();
286 }
287 return p;
288 };
289 out.points.reserve(nPoints);
290 out.pointIsInside.reserve(nPoints);
291 for (int i = 0; i < nPoints; ++i) {
292 const Point3D p = drawPoint();
293 const bool in = reference->Contains(p.data());
294 out.points.push_back(p);
295 out.pointIsInside.push_back(in ? 1 : 0);
296 out.insidePoints += in ? 1 : 0;
297 }
298 const int budget = 400 * std::max(1, nRays);
299 int attempts = 0;
300 while (static_cast<int>(out.outsideRays.size()) < nRays && attempts < budget) {
301 ++attempts;
302 const Point3D p = drawPoint();
303 if (reference->Contains(p.data())) {
304 continue;
305 }
306 // Aim at a random point of the (uninflated) bounding box: a thin part is missed by an
307 // isotropic direction almost always, and a DistFromOutside timing dominated by misses prices
308 // the early-out rather than the kernel.
309 Point3D target{};
310 for (int k = 0; k < 3; ++k) {
311 target[k] = bboxMin[k] + (bboxMax[k] - bboxMin[k]) * rng.next();
312 }
313 Point3D d{target[0] - p[0], target[1] - p[1], target[2] - p[2]};
314 const double norm = std::sqrt(d[0] * d[0] + d[1] * d[1] + d[2] * d[2]);
315 if (!(norm > 0.)) {
316 continue;
317 }
318 for (int k = 0; k < 3; ++k) {
319 d[k] /= norm;
320 }
321 out.outsideRays.push_back({p, d});
322 }
323 attempts = 0;
324 while (static_cast<int>(out.insideRays.size()) < nRays && attempts < budget) {
325 ++attempts;
326 const Point3D p = drawPoint();
327 if (!reference->Contains(p.data())) {
328 continue;
329 }
330 const double z = 2. * rng.next() - 1.;
331 const double phi = 2. * 3.14159265358979323846 * rng.next();
332 const double r = std::sqrt(std::max(0., 1. - z * z));
333 out.insideRays.push_back({p, {r * std::cos(phi), r * std::sin(phi), z}});
334 }
335 return out;
336}
337
338// ------------------------------------------------------------------------------------------
339// 4. The four kernel passes
340// ------------------------------------------------------------------------------------------
341//
342// Each is a closure over the shared sample set that performs exactly `callsPerPass` virtual calls
343// and returns a checksum. They exist as named functions rather than as lambdas at the call site
344// so that the unit tests time the same loop bodies the benchmark does.
345
346inline TimingStat timeContainsPass(const TGeoShape* shape, const QuerySamples& s, int warmup, int passes)
347{
348 return timePasses(static_cast<long long>(s.points.size()), warmup, passes, [&]() {
349 uint64_t acc = 0;
350 for (const auto& p : s.points) {
351 acc = detail::mix(acc, shape->Contains(p.data()) ? 1. : 0.);
352 }
353 return acc;
354 });
355}
356
362inline TimingStat timeSafetyPass(const TGeoShape* shape, const QuerySamples& s, int warmup, int passes)
363{
364 return timePasses(static_cast<long long>(s.points.size()), warmup, passes, [&]() {
365 uint64_t acc = 0;
366 for (size_t i = 0; i < s.points.size(); ++i) {
367 acc = detail::mix(acc, shape->Safety(s.points[i].data(), s.pointIsInside[i] ? kTRUE : kFALSE));
368 }
369 return acc;
370 });
371}
372
373inline TimingStat timeDistOutPass(const TGeoShape* shape, const QuerySamples& s, int warmup, int passes)
374{
375 long long hits = 0;
376 long long calls = 0;
377 TimingStat stat = timePasses(static_cast<long long>(s.outsideRays.size()), warmup, passes, [&]() {
378 uint64_t acc = 0;
379 for (const auto& ray : s.outsideRays) {
380 const double d = shape->DistFromOutside(ray.origin.data(), ray.dir.data(), 3, TGeoShape::Big(), nullptr);
381 hits += (d < TGeoShape::Big()) ? 1 : 0;
382 ++calls;
383 acc = detail::mix(acc, d);
384 }
385 return acc;
386 });
387 stat.hitFraction = calls > 0 ? static_cast<double>(hits) / static_cast<double>(calls) : -1.;
388 return stat;
389}
390
391inline TimingStat timeDistInPass(const TGeoShape* shape, const QuerySamples& s, int warmup, int passes)
392{
393 long long hits = 0;
394 long long calls = 0;
395 TimingStat stat = timePasses(static_cast<long long>(s.insideRays.size()), warmup, passes, [&]() {
396 uint64_t acc = 0;
397 for (const auto& ray : s.insideRays) {
398 const double d = shape->DistFromInside(ray.origin.data(), ray.dir.data(), 3, TGeoShape::Big(), nullptr);
399 hits += (d < TGeoShape::Big()) ? 1 : 0;
400 ++calls;
401 acc = detail::mix(acc, d);
402 }
403 return acc;
404 });
405 stat.hitFraction = calls > 0 ? static_cast<double>(hits) / static_cast<double>(calls) : -1.;
406 return stat;
407}
408
409// ------------------------------------------------------------------------------------------
410// 5. The synthetic boolean ladder
411// ------------------------------------------------------------------------------------------
412//
413// Every genuine boolean in the corpus today is a 2-leaf union of two TGeoTubes, so the corpus
414// cannot say how a composite scales with leaf count
415// and no amount of running it harder will make it. This builds the missing fixture: unions of
416// 2, 4, 8, ... TGeoTubes, in the two tree shapes an emitter can plausibly produce.
417//
418// The two shapes are the point of the experiment.
419// * CHAIN -- (((t0 + t1) + t2) + t3) ... : depth K-1, the natural output of a fold over a
420// list of leaves. Every query descends the whole spine.
421// * BALANCED-- a complete binary tree of depth ceil(log2 K). This is what a BVH over primitives
422// would give you for free, minus the bounding-box rejection.
423// If the two scale the same way, tree shape is not where the cost is and a BVH-over-primitives
424// CSG solid has nothing to win from restructuring alone. If they separate, the gap IS the prize.
425
426enum class LadderShape { Chain,
427 Balanced };
428
437inline TGeoShape* buildBooleanLadder(int leaves, LadderShape shape, const std::string& tag)
438{
439 if (leaves < 1) {
440 return nullptr;
441 }
442 const double rMin = 0.2;
443 const double rMax = 0.5;
444 const double dz = 1.0;
445 const double pitch = 0.8;
446 auto leafName = [&](int i) { return tag + "_leaf" + std::to_string(i); };
447 std::vector<TGeoShape*> nodes;
448 std::vector<TGeoMatrix*> offsets;
449 for (int i = 0; i < leaves; ++i) {
450 auto* tube = new TGeoTube(leafName(i).c_str(), rMin, rMax, dz);
451 nodes.push_back(tube);
452 auto* m = new TGeoTranslation((i - 0.5 * (leaves - 1)) * pitch, 0., 0.);
453 m->SetName((leafName(i) + "_m").c_str());
454 m->RegisterYourself();
455 offsets.push_back(m);
456 }
457 if (leaves == 1) {
458 return nodes.front();
459 }
460 int serial = 0;
461 auto join = [&](TGeoShape* a, TGeoMatrix* ma, TGeoShape* b, TGeoMatrix* mb) -> TGeoShape* {
462 auto* node = new TGeoUnion(a, b, ma, mb);
463 auto* composite = new TGeoCompositeShape((tag + "_u" + std::to_string(serial++)).c_str(), node);
464 return composite;
465 };
466 if (shape == LadderShape::Chain) {
467 TGeoShape* acc = nodes[0];
468 TGeoMatrix* accMatrix = offsets[0];
469 for (int i = 1; i < leaves; ++i) {
470 acc = join(acc, accMatrix, nodes[i], offsets[i]);
471 accMatrix = nullptr; // the accumulated composite is already in the common frame
472 }
473 return acc;
474 }
475 std::vector<TGeoShape*> level = nodes;
476 std::vector<TGeoMatrix*> levelMatrix = offsets;
477 while (level.size() > 1) {
478 std::vector<TGeoShape*> next;
479 std::vector<TGeoMatrix*> nextMatrix;
480 for (size_t i = 0; i < level.size(); i += 2) {
481 if (i + 1 < level.size()) {
482 next.push_back(join(level[i], levelMatrix[i], level[i + 1], levelMatrix[i + 1]));
483 nextMatrix.push_back(nullptr);
484 } else {
485 next.push_back(level[i]);
486 nextMatrix.push_back(levelMatrix[i]);
487 }
488 }
489 level.swap(next);
490 levelMatrix.swap(nextMatrix);
491 }
492 return level.front();
493}
494
500 long long leaves = 0;
501 long long nodes = 0;
502 int depth = 0;
503};
504
505inline BooleanTreeStats booleanTreeStats(const TGeoShape* shape)
506{
508 const auto* composite = dynamic_cast<const TGeoCompositeShape*>(shape);
509 if (composite == nullptr || composite->GetBoolNode() == nullptr) {
510 out.leaves = 1;
511 out.depth = 1;
512 return out;
513 }
514 const TGeoBoolNode* node = composite->GetBoolNode();
515 const BooleanTreeStats left = booleanTreeStats(node->GetLeftShape());
516 const BooleanTreeStats right = booleanTreeStats(node->GetRightShape());
517 out.leaves = left.leaves + right.leaves;
518 out.nodes = left.nodes + right.nodes + 1;
519 out.depth = 1 + std::max(left.depth, right.depth);
520 return out;
521}
522
523// ------------------------------------------------------------------------------------------
524// 6. The negative control for the timing harness itself
525// ------------------------------------------------------------------------------------------
526
534class BallastShape : public TGeoBBox
535{
536 public:
537 BallastShape(const char* name, double dx, double dy, double dz, int burn)
538 : TGeoBBox(name, dx, dy, dz), mBurn(burn) {}
539
540 double ballast(const double* point) const
541 {
542 double acc = 1.;
543 for (int i = 0; i < mBurn; ++i) {
544 acc = std::sqrt(acc * acc + point[i % 3] * point[i % 3] + 1.);
545 }
546 return acc;
547 }
548
549 bool Contains(const double* point) const override
550 {
551 return TGeoBBox::Contains(point) && ballast(point) > 0.;
552 }
553 double Safety(const double* point, bool in = kTRUE) const override
554 {
555 return TGeoBBox::Safety(point, in) + 0. * ballast(point);
556 }
557 double DistFromOutside(const double* point, const double* dir, int iact = 1,
558 double step = TGeoShape::Big(), double* safe = nullptr) const override
559 {
560 return TGeoBBox::DistFromOutside(point, dir, iact, step, safe) + 0. * ballast(point);
561 }
562 double DistFromInside(const double* point, const double* dir, int iact = 1,
563 double step = TGeoShape::Big(), double* safe = nullptr) const override
564 {
565 return TGeoBBox::DistFromInside(point, dir, iact, step, safe) + 0. * ballast(point);
566 }
567
568 private:
569 int mBurn = 0;
570};
571
572} // namespace bench
573} // namespace cad
574} // namespace o2
575
576#endif
std::unique_ptr< expressions::Node > node
int32_t i
Validation and timing harness for TGeoShape navigation, typed on plain TGeoShape*.
bool Contains(const double *point) const override
BallastShape(const char *name, double dx, double dy, double dz, int burn)
double ballast(const double *point) const
double DistFromInside(const double *point, const double *dir, int iact=1, double step=TGeoShape::Big(), double *safe=nullptr) const override
double Safety(const double *point, bool in=kTRUE) const override
double DistFromOutside(const double *point, const double *dir, int iact=1, double step=TGeoShape::Big(), double *safe=nullptr) const override
float sum(float s, o2::dcs::DataPointValue v)
Definition dcs-ccdb.cxx:39
const GLfloat * m
Definition glcorearb.h:4066
GLint GLsizei count
Definition glcorearb.h:399
GLuint GLsizei const GLuint const GLintptr * offsets
Definition glcorearb.h:2595
GLuint const GLchar * name
Definition glcorearb.h:781
GLdouble GLdouble right
Definition glcorearb.h:4077
GLboolean GLboolean GLboolean b
Definition glcorearb.h:1233
GLsizei const GLfloat * value
Definition glcorearb.h:819
GLenum target
Definition glcorearb.h:1641
GLenum GLint GLenum GLsizei GLsizei GLsizei GLint GLsizei const void * bits
Definition glcorearb.h:4150
GLint GLint GLsizei GLsizei GLsizei depth
Definition glcorearb.h:470
GLsizei const GLchar *const * path
Definition glcorearb.h:3591
GLboolean r
Definition glcorearb.h:1233
GLuint GLfloat GLfloat GLfloat GLfloat GLfloat GLfloat GLfloat t0
Definition glcorearb.h:5034
GLboolean GLboolean GLboolean GLboolean a
Definition glcorearb.h:1233
GLdouble GLdouble GLdouble z
Definition glcorearb.h:843
GLuint GLfloat GLfloat GLfloat GLfloat GLfloat GLfloat GLfloat GLfloat GLfloat t1
Definition glcorearb.h:5034
uint64_t mix(uint64_t acc, double value)
TimingStat timeSafetyPass(const TGeoShape *shape, const QuerySamples &s, int warmup, int passes)
long long fileBytes(const std::string &path)
QuerySamples buildQuerySamples(const TGeoShape *reference, const std::string &referenceName, const Point3D &bboxMin, const Point3D &bboxMax, int nPoints, int nRays, uint64_t seed=20260802ULL, double inflate=0.12)
TGeoShape * buildBooleanLadder(int leaves, LadderShape shape, const std::string &tag)
TimingStat timeContainsPass(const TGeoShape *shape, const QuerySamples &s, int warmup, int passes)
MemorySnapshot readMemory()
MemorySnapshot operator-(const MemorySnapshot &a, const MemorySnapshot &b)
TimingStat timePasses(long long callsPerPass, int warmupPasses, int passes, Pass &&pass)
TimingStat timeDistInPass(const TGeoShape *shape, const QuerySamples &s, int warmup, int passes)
TimingStat timeDistOutPass(const TGeoShape *shape, const QuerySamples &s, int warmup, int passes)
BooleanTreeStats booleanTreeStats(const TGeoShape *shape)
std::array< double, 3 > Point3D
a couple of static helper functions to create timestamp values for CCDB queries or override obsolete ...
std::string to_string(gsl::span< T, Size > span)
Definition common.h:52
long long nodes
TGeoCompositeShape / TGeoBoolNode pairs.
std::vector< char > pointIsInside
the reference's Contains() for each, as a fixed label
std::vector< Ray > insideRays
origin inside per the reference, isotropic direction
std::vector< Ray > outsideRays
origin outside per the reference, aimed into the bbox
std::vector< Point3D > points
all query points, mixed inside/outside, in bbox order
long long primitives
triangles / analytic patches / boolean leaves
long long sidecarBytes
the file the representation was loaded from, on disk
long long bytes
the arithmetic below, exact for the arrays it counts