HN user

GeorgeHaldane

47 karma

C++ developer. Working in the field of computational mathematics.

Posts4
Comments5
View on HN

Thank you for noticing! Turns out [0, 1] is an artifact of the old documentation carried back from the time where generic GCC / clang approach used to produce occasional 1's due to some issues in N4958 specification. This is fixed in a new commit.

For floats there are essentially 3 approaches that are selected based on the provided range & PRNG:

  1) Shift + multiply (like `(rng >> 11) * 0x1.0p-53`)
  2) "Low-high" from the paper by J. Doornik
  https://www.doornik.com/research/randomdouble.pdf
  3) Canonical GCC implementation
All of these generate values in [0, 1) range which is now reflected in the docs properly. For most actual cases 1st method is the one selected.

General rundown of the logic can be found in this comment on reddit: https://www.reddit.com/r/cpp/comments/1jy6ver/comment/mmze20...

About linear IDs: A call graph in general case is a tree of nodes, each node has a single parent and an arbitrary amount of children. Each node accumulates time spend in the "lower" branches. A neat property of the callgraph relative to a generic tree, is that every node can be associated with a callsite. For example, if a some function f() calls itself 3 recursively, there will be multiple nodes corresponding to it, but in terms of callsite there is still only one. So lets take some simple call graph as an example:

  Callgraph:         f() -> f() -> f() -> g()
                                       -> h()
  Node id:           0      1      2      3,4
Let's say f() has callsite id '0', g() has callsite id '1', h() has callsite id '2'. The callgraph will then consist of N=5 nodes with M=3 different callsites:
  Node id:         { 0   1   2   3   4 }
  Callsite id:     { 0   0   0   1   2 }
We can then encode all "prev."" nodes as a single N vector, and all "next" nodes as a MxN matrix, which has some kind of sentinel value (like -1) in places with no connection. For this example this will result in following:
  Node id:         { 0   1   2   3   4 }
  Prev. id:        { x   0   1   2   2 }
  Next id matrix:  [ 1   2   3   x   x ]
                   [ x   x   4   x   x ]
Every thread has a thread-local callgraph object that keeps track of all this graph traversal, it holds 'current_node_id'. Traversing backwards on the graph is a single array lookup:
  current_node_id = prev_node_ids[current_node_id];
Traversing forwards to an existing callgraph node is a lookup & branch:
  next_node_id = next_node_ids[callsite_id, current_node_id]
  if (next_node_id = x) create_node(next_node_id); // will be usually predicted
  else                  current_node_id = next_node_id;
New nodes can be created pretty cheaply too, but too verbose for a comment. The key to tracking the callsites and assigning them IDs is thread_local local variables generated by the macro:

https://github.com/DmitriBogdanov/UTL/blob/master/include/UT...

When callsite marker initializes (which only happens once), it gets a new ID. Timer then gets this 'callsite_id' an passes it to the forwards-traversal. The way we get function names is by simply remembering __FILE__, __func__, __LINE__ pointers in another array of the call graph, they get saved during the callsite marker initialization too. As far as performance goes everything we do is cheap & simple operations, at this point the main overhead is just from taking the timestamps.

Haven't worked with it, but based on initial look it's a quite different thing that stands closer to a frame-based profiler like Tracy (https://github.com/wolfpld/tracy).

As far as differences go:

Microprofile:

  - frame-based
  - needs a build system
  - memory usage starts at 2 MB per thread
  - runs 2 threads of its own
  - provides system-specific info
  - good support for GPU workloads
  - provides live view
  - seems like a good choice for gamedev / rendering
utl::profiler:
  - no specific pipeline
  - single include
  - memory usage starts at approx. nothing and would likely stay in kilobytes
  - doesn't run any additional threads
  - fully portable, nothing platform specific whatsoever, just standard C++
  - doesn't provide system-specific info, just pure timings
  - seems like a good choice for small projects or embedded (since the only thing it needs is a C++ compiler)