1
0
Fork 0
mirror of https://github.com/AquariaOSE/Aquaria.git synced 2024-12-27 07:06:00 +00:00
Aquaria/ExternalLibs/iprof/iprof.txt

617 lines
26 KiB
Text

IPROF: A Portable Industrial-Strength Interactive Profiler for C++ and C
by Sean Barrett
Version 0.2
CONTENTS
Overview
User Manual
Platform
Instrumentation
Private Zones
Public Zones
Initialization
Processing Data
Displaying Results
Controlling Display
Understanding CALL GRAPH output
Performance Expectation
Implementation Notes
Version History
OVERVIEW
IProf is an interactive profiler which works by intrusively instrumenting
code. Code is divided into zones by programmer-inserted statements. Zones
are both lexically and dynamically scoped--all time spent within a
lexically scoped zone, and any code which it calls which is not itself
zoned, is attributed to that zone.
Profiling occurs interactively; time is divided into "frames", and the
profiler shows time spent on the previous frame (or a smoothed average
or possibly even a frame a second or two ago).
Like a traditional profiler, IProf records or can compute the number of
times a zone is entered, the amount of time spent in the zone ("self
time"), and the amount of time spent in the zone and its descendents
("hierarchical time" -- "self + child time" in gprof).
Furthermore, IProf computes information along the lines of gprof--number of
times a given zone is entered from any other specific zone; self and
hierarchical time spent in a given zone on behalf of a specific parent
zone, etc. (However, where gprof estimates this information based only on
call counts, IProf measures the actual values. So, for example, IProf will
accurately report if a ray-casting routine called by both physics and AI
always spends longer per AI-call because the casts are longer.)
Precise information is available for recursive routines, including call
depths etc. [The current version of IProf does not yet completely handle
reporting of recursive data, although it is measured correctly.]
Additionally, IProf provides all numbers in instantaneous form or as two
differently weighted moving averages. It's easy to pause the profile
updating so that you can switch between multiple views of the paused data
set. Two optional flags allow trading off memory for deeper historical
views. The cheaper option provides only zone-self-time history, suitable
for a real-time graph of behavior. The more memory-expensive flag keeps a
history of all the data for a certain number of frames, allowing full
profile analysis of old frames.
IProf is designed for its monitoring/gathering mode to be "always on", even
in release/optimized builds. The monitoring routines are designed to be
reasonably efficient--the full hash on every function entry required by
gprof is avoided in most cases--and the programmer can minimize the impact
by limiting the instrumentation to relatively large routines. (One could
certainly instrument a vector add function and possibly get useful call
count data from it, but the monitoring overhead would be significant and
noticeable in that case.) In combination with history information, it
becomes possible to run an application, notice poor behavior, pause the
(always on) profiling and the application, and start browsing through the
historical profiling information.
IProf uses both per-call monitoring and a separate per-frame
gathering/analysis phase. The latter is itself instrumented so the overhead
due to it is easy to see.
USER MANUAL
These sections document the necessary code you must use and code changes
you must make to use the profiler.
The profiling system expects to be able to use any identifier which is
prefixed with "Prof_" with exactly that pattern of uppercase/lowercase
(i.e. "PROF_" and "prof_" can be used freely by other code).
COMPILING THE PROFILING SYSTEM
The profiler was developed using MSVC 6.0, but should be reasonably
portable. The implementation files are provided as .C files so they can be
used with C compilers; however, they can be renamed to C++ files and
compiled in that form. The implementations automatically insert extern "C"
on the public routines. Internal routines will use either C or C++ linkage
depending on which way you compile them; you must compile all the profiler
files as either C or C++, without intermixing.
[[ NOTE: Originally the code was written in C++, and then it was
converted to compile with C, and then some additional small changes
were made. As of this writing, I haven't actually tested compiling
everything as C++ again. Feel free to test for me. Or just compile
the C files as C--you can still USE the C++ interfaces fine.]]
Needed files:
prof_win32.c -- Win32 implementation of seconds-based timer
prof_gather.c -- raw data collection
prof_process.c -- high-level data collection, report generator
prof_draw.c -- opengl rendering interface
prof.h -- public front-end
prof_win32.h -- Win32 implementation of fast integer timestamp
prof_gather.h -- instrumentation macros (included by prof.h)
prof_internal.h -- private interfaces
PLATFORM SUPPORT
IProf requires a small amount--less than fifty lines--of platform-specific
code.
Win32 under MSVC is automatically supported with no further effort on your
part, using the files prof_win32.c and prof_win32.h
To use other platforms, just create equivalent files for your platform. The
C file contains a routine for getting an accurate floating point time
reading; the H file contains the definition of a 64-bit integer type and a
fast routine for reading a timestamp of that size. If 64-bit math isn't
available on your platform, or if your timestamp is only 32-bit, you can
replace the 64-bit type with a 32-bit type, as long as that item won't
overflow in the course of running the application. (A 31-bit millisecond
timer is good for 24 days, but is very imprecise for this application.) If
reading the timestamp is slow, you will want to minimize how often the zone
entry and exit points are called.
Also required is a display interface; an opengl one is provided, although
others would be easy to code. (The primary display is purely textual, and
is available through a text interface.)
INSTRUMENTATION
First, #include "prof.h" in files that need profiling.
The flag Prof_ENABLED determines whether the monitoring code is compiled or
not, to make it easy to turn off all profiling code for final shippable
builds. Additional flags controlling amount of history data and memory
usage therein are defined at the top of the file prof_process.c and should
just be changed there since they affect no other files.
There are two main ways of instrumenting, and each offers a C++ interface
and a C interface.
Private zones
C++ Prof(zone);
C Prof_Begin(zone)
Prof_End
Public zones
Prof_Define(zone);
Prof_Declare(zone);
C++ Prof_Scope(zone);
C Prof_Region(zone)
Prof_End
Zone names--indicated by "zone" above--must obey the rules for identifiers,
although they can begin with a number, and they exist in a separate
namespace from regular identifiers.
So these are valid zone names:
my_zone_2
2_my_zone
__
And these are NOT valid zone names:
"my_zone"
my_class::my_zone
PRIVATE ZONES
The simplest, and highly recommended, approach to instrumentation is to
create a private zone which only exists in a single location. In C++, you
do this by declaring a lexically scoped zone with a statement which behaves
semantically like a variable declaration:
// C++ instrumentation
void my_routine()
{
Prof(my_routine_name);
... my code ...
}
This will cause all time spent after Prof(my_routine_name) to accumulate in
a zone in the profiling reports labeled "my_routine_name". The zone ends
when the name goes out of scope, that is, when a destructor would be called
corresponding to this declaration.
Zones don't have to appear at routine-level function scope; for example:
// C++ instrumentation
void my_routine()
{
Prof(my_routine);
... // zone my_routine
if (...)
{
Prof(my_routine_special_case);
... // zone my_routine_special_case
}
... // zone my_routine
}
Instrumenting in C requires more work, because C doesn't provide
destructors, so it's not possible to lexically scope zones automatically.
Instead, the programmer must insert Begin/End pairs and make sure those
pairs are accurately balanced. All paths out of a function must be
accounted for. A crash or severe slowdown is likely to occur with
unbalanced pairs.
// C instrumentation
void my_routine(void)
{
Prof_Begin(my_routine)
int x = some_func();
if (x == 0) {
Prof_End
return;
}
...
Prof_End
}
Prof_Begin() is declaration-like; however, it takes no trailing semicolon.
(This is necessary so it can be compiled out; C doesn't allow the empty
statement ";" to precede variable declarations.) Prof_End takes no
trailing semicolon or parentheses to help remind you of this. (You can
change the definition of Prof_End in prof_gather.h if you don't like that.)
Profiling instructions like Prof() and Prof_Begin() can be placed anywhere
that variable declarations are legal; generally you want to define them
before other variables so the variable initializations are profiled.
The C interfaces are also available in C++ if you should want to use a not-
exactly-lexically-scoped zone, e.g. end a zone before the destructor would
go out of scope. (You can't, however, end Prof() with Prof_End.)
PUBLIC ZONES
If you define multiple private zones with the same name, they will be
treated as entirely unrelated zones that happen to have the same name, and
you will see the same name multiple times in the profiling output.
Instead, you probably want to use public zones, to use the same zone in
multiple regions of code. For example, we might have two routines that
serve the same purpose which we always want to measure as one. Or we might
have two blocks of code within a single routine which we want to credit to
the same zone.
To do this, first define the zone with Prof_Define(zone), and then use it
with Prof_Scope(zone) [C++] or Prof_Region(zone) ... Prof_End [C].
// C++ instrumentation
Prof_Define(my_routine);
void my_routine_v1()
{
Prof_Scope(my_routine);
...
}
void my_routine_v2()
{
Prof_Scope(my_routine);
...
}
or
// C instrumentation
Prof_Define(my_routine);
void my_routine_v1(void)
{
Prof_Region(my_routine)
...
Prof_End
}
void my_routine_v2(void)
{
Prof_Region(my_routine)
...
Prof_End
}
Because Prof_Define defines an actual global symbol (if used at file
scope), the symbol can even be referenced from other files by saying:
extern Prof_Declare(my_routine);
void my_routine()
{
Prof_Scope(my_routine);
}
You can use 'extern "C" Prof_Declare()' or Prof_Define() to share a zone
between C and C++ code.
USER MANUAL - INIIALIZATION
The profiling system is self-initializing.
USER MANUAL - PROCESSING DATA
Every frame, you should call Prof_update(). Prof_update() will gather
results and record frame-history information on the assumption that each
call is a frame. Prof_update() takes a boolean flag which indicates whether
to update the history or not; passing in false means profiling is "paused"
and doesn't change.
You might wire this to its own toggle, or you might simply pass in a pre-
existing flag for whether the simulation itself is active or not, thus
allowing you to pause the simulation and automatically pause the profiling.
(On the other hand, if you're profiling a renderer, you might want to
pause the simulation and keep profiling.)
USER MANUAL - DISPLAYING RESULTS
IProf offers two separate types of display: the report, which is primarily
textual, and the graph, which is entirely graphical.
If you're using OpenGL, output is straightforward. For the text report,
call Prof_draw_gl() with the display set to a 2d rendering mode--one that
can use integer addressing, e.g. integers the size of pixels, virtual
pixels (e.g. a 640x480 screen regardless of actual dimension), or even
characters. Set the blending state to whatever blending mode you want for
the report display. For the graphics report, call Prof_draw_graph_gl().
Details of the parameters to these functions are available in the header
file.
For other output devices (Direct3D, text), you'll have to write your own
functions equivalent to Prof_draw_gl() and Prof_draw_graph_gl(). These
should not be too difficult; these functions don't compute any of the
profiling information; they simply format a text report or dataset to the
screen. The text report format consists of several title fields to be
printed, and then a collection of data records. Each data record has a name
and an indentation amount for that name (used for call graph
parent/children formatting), a collection of unnamed data "values", and a
flag field indicating which of the data values should be displayed.
Additionally, data records have a "heat" which indicates how rapidly
changing they are, and one record may be "highlighted" indicating a virtual
cursor is on that line.
[[ In practice, Prof_draw_gl makes few enough GL calls that maybe it's
worth modularizing things out further. ]]
USER MANUAL - CONTROLLING DISPLAY
IProf features some easy-to-use UI elements that allow program-direct
control or user-interaction-based control over what data is reported.
Simply hook these calls up to hotkey presses to complete your working
profile system. (You could even write code to support mouse clicking on the
report by calling Prof_set_cursor and on the graph by calling
Prof_set_frame, but the hit detection is up to you.)
These are in rough order of the priority with which you might want to
implement them.
Most important
Prof_set_report_mode(enum ...)
Selects what to show in the report:
Prof_SELF_TIME: flat times sorted by self time
Prof_HIERARCHICAL_TIME: flat times sorted by hierarchical time
Prof_CALL_GRAPH: call graph parent/children information
Prof_move_cursor(int delta)
Move the cursor up-or-down by delta lines
Prof_select(void)
Switch to call graph mode on whichever zone is currently selected
Prof_select_parent(void)
Go to largest-hierarchical-time parent of the active zone in
the call graph. (Roughly like "go up a directory".)
Important if you support history
Prof_move_frame(int delta)
Move backwards or forwards in history by delta frames
Not too important
Prof_set_average(int type)
Selects which moving average to use (0 == instantaneous, 1=default);
only meaningful if frame# = 0; when looking at history, instantaneous
values are always used.
Prof_set_frame(int frame)
Selects which history entry to view (0==current, 1==previous, etc.)
Prof_set_cursor(int pos)
Set the position of the up-and-down cursor.
Prof_set_recursion(enum ...)
Selects whether to show recursive routines as a single zone or
as a series of distinct zones for each recursion level.
[[ currently unimplemented ]]
UNDERSTANDING CALL GRAPH OUTPUT
The call graph output focuses on a single zone, and provides information
about the parents (callers) and children (callees) of that zone.
The general format is something like this:
zone self hier count
+my_parent1 0.75 2.50 4.0
+my_parent2 1.00 3.25 6.0
-my_routine 1.75 5.75 10.0
+my_child1 1.00 2.00 15.0
+my_child2 0.25 1.50 500.0
my_child3 0.50 0.50 3.0
"self" indicates self-time (time in this zone), "hier" is hierarchical-time
(time in this zone or its descendents), and "count" is the number of times
the zone was entered. (Entry counts are inherently integral, but are shown
as floating point since they may be a moving average of several integers.)
Currently the zone "my_routine" is being examined. It accounts for 5.75
milliseconds of time between itself and the zones it calls. 1.75ms are
spent in itself. The zone was entered (called) 10 times this frame.
The difference between my_routine's self time and hierarchical time is
4.00ms; that much time must be being spent in its descendents. Its
immediate children--the zones that my_routine calls directly--appear below
it on the table. The hierarchical times of each child represents the time
spent in that child and all its descendents *on behalf of my_routine*--
other calls to that child are not counted. Thus, the sum of all the
children's hierarchical time should account for all time spent in
descendents of my_routine; hence, the sum of the child hier times is 4.00,
identical to the difference between self and hier for my_routine.
Above "my_routine" in the chart is information about the callers of
my_routine. However, the timings and counts in this section are not the
self and hierarchical times of the parent functions themselves--there is no
sensible meaning of "on behalf of my_routine" for the parents. Instead, the
self, hier, and count fields show the time spent *in my_routine* on behalf
of those parents. Thus, for each field, all of the parent entries sum to
the corresponding entry in my_routine. Again, these are computed exactly.
If my_routine was the public interface to a raycaster called by both AI and
physics, but it passed the raycast on to further routines which were
themselves explicitly zoned, then most of the my_routine time would be
spent in descendents. This would show up in the "hierarchical time" field,
and the parent zones, AI and physics, would show that hierarchical time
attributed accurately.
There is additional data available in the system--it would be possible to
drill down into lower-level functions and still attribute them to zones
several parent levels above; there just isn't currently any user interface
or computation functionality to do it.
PERFORMANCE EXPECTATION
Except for recursive routines (see Implementation Notes section), the
expected performance on zone entry comes from running roughly the following
code:
extern Something *p0,*p1;
if (p0->ptr_field != p1) { ... /* rarely runs */ }
p0->int64_field0 = RDTSC; // read timestamp counter
p0->int32_field += 1;
p1->int64_field1 += p0->int64_field0 - p1->int64_field0;
p1 = p0;
Zone exit costs a bit less.
IMPLEMENTATION NOTES
IProf uses two relatively unknown techniques to produce accurate call
information with minimal overhead. The first technique produces accurate
call information at a similar cost to gprof's mcount monitoring; the second
reduces the overhead.
_Zone Stack Tracking_
gprof's mcount technique combines two separate measurements. At every
function entry, the function and the caller (grabbed from the return
address on the stack) are hashed to determine a unique "data-gathering
slot", and an integer in that slot is incremented. Thus, exact pairwise
call counts are computed. Simultaneously, gprof periodically samples the
instruction pointer to measure the time spent in any given routine--"self
times". Hierarchical times are computed by distributing the self times up
the tree based on the call graph counts. (If routine X is called 9 times
from routine Z, and one time from routine Y, then 90% of X's time is
attributed to Z, and 10% to Y.)
An intrusive profiler which samples a timer at zone entry and again at zone
exit will compute accurate hierarchical times. By keeping a stack of zones,
it's possible to compute accurate hierarchical and self times. The stack of
zones also provides caller information, so hierarchical and self times can
be attributed to each unique pair of caller & callee zones (via hashing).
This will allow much more accurate attribution. In fact, it is sufficient
to compute exact values for all the information gprof outputs, except in
the face of recursion. Performance is fairly good; unlike a single-zone
intrusive profiler, which must measure both self and hierarchical time,
since neither can be derived from the other, the zone-pair profiler can
only measure hierarchical time; self-time can be derived from hierarchical
time (but not vice versa).
A further improvement is, instead of having one data-gathering slot per
zone--that is, representing the state of the top of the zone stack--and
instead of having one data-gathering slot per caller/callee zone pair--that
is, representing the state of the top two entries of the zone stack--to
have one data-gathering slot per unique full stack state. This can be done
straightforwardly by building the stack as a linked list (creating an
inverted tree--a tree of all stack states with only parent-pointer links),
and hashing the "zone to be pushed" and the current stack to find the new
stack. Thus the cost of the hash computation is essentially identical to
the previous case. If every zone is only called from one specific place,
there will still only be one data-gathering slot per zone; if a routine is
recursive, it will create a large number of data-gathering slots, one for
each depth of recursion. (A complex mutually recursive program like a
compiler might generate an unreasonable number of unique states.)
With zone-stack tracking, it's possible to measure only either hierarchical
time or self-time and derive the other. Hierarchical time is actually more
efficient to measure, but it leaves handling the top-level overarching
global state as a special case (since it will have a timer that starts but
never ends). It's easier to instead measure self-time and rederive
hierarchical time. Moreover, a recursive routine will automatically
"overcount" hierchical time (it's accrued at each level of the hierarchy),
requiring significant fixup. It's more straightforward to just compute the
recursive data correctly from the self times in the first place.
_Hash Cacheing_
Although the hash lookup described above is coded to proceed as quickly as
possible if the hash hits on the first probe, it still requires enough
computation and a function call that it is worth avoiding if possible. To
that end, each zone-entry location declares a hidden static variable
private to that zone-entry point which caches the hash lookup. At zone-
entry, the code checks the cache's "next node in the linked list" field
with the current stack state. If the two are equal, then the cache is
valid, and no hash lookup occurs. If it does not much, then the cache is
wrong, and the hash lookup proceeds, and updates the cache. The cache is
initialized to a impossible value, so the first time the code is run a hash
lookup always occurs.
The result is that in the normal case, a routine called from a single
place, the cache is always valid (after the very first call). Furthermore,
the branch will always predict correctly, since it always branches
identically. However, for a routine that is called from several places,
there is a "switching" overhead each time it's called from a different
place. So, for example, a raycaster called by both physics and AI might pay
the overhead twice per frame, if all the AI calls occur before all the
physics calls. However, a common low-level routine (e.g. a vector add)
called alternately from two different zones would have to perform the hash
lookup every time.
The actual common "failure" case is a recursive routine, for which, each
time the routine is entered, the state of the call stack is different from
the last time, thus almost always paying the hash lookup case. For
something like a recursive linked list traversal, the hash occurs every
time. (It doesn't matter if the routine is tail-recursive; once you insert
the profiling instrumentation, it's no longer tail-recursive.) A full
binary tree traversal will always enter a different zone-stack-state from
last time, except after reaching a left-child leaf. (The recursion then
returns and then goes down to the right child, which is at the same height
as the left child.) So a full binary tree traversal will have to hash about
3/4 of the time. A full quadtree traversal will have to hash about 2/5 of
the time. If the traversal is doing anything complicated, this should not
be a problem; but if it's a simple traversal, the performance overhead may
be significant. Like the vector add case, it may be better to remove
instrumentation from low-inherent-cost recursive routines except when
absolutely needed. Of course, it's easy enough to compare performance
behavior before and after adding the instrumenting and see if the overhead
is acceptable.
VERSION HISTORY
version 0.2 -- 2003-02-06 STB
- Significant interface changes to Prof_draw_gl:
- accepts floating point instead of int for 2d screen metrics
- accepts a total width and height of the display and conforms
to that
- accepts a precision specification for display of time values
- added little '+' and '-' signs reminiscent of list displays
so you know which ones can be drilled down on
- expanded this doc's description of what's legal for a zone-name
- fixed an error trying to compile the C files as C++
- added Prof_select_parent() for moving up the tree
version 0.1 -- 2003-02-05 STB
- First public version, heavily refactored, 1500 lines
- win32 timing interface and smooth "moving average" code derived
from Jonathan Blow's Game Developer Magazine articles
- missing functionality:
- correct attribution of time to zones that are parents of
recursive zones in call graph view (hierarchical times don't
bubble up correctly)
- spread recursion display (displaying each depth of a recursive
zone as if it were a separate zone)