Annotation of researchv10dc/cmd/sml/doc/papers/profiling/paper.ms, revision 1.1.1.1

1.1       root        1: .LP
                      2: -
                      3: .ft 3
                      4: .ce 99
                      5: .sp 2i
                      6: .LG
                      7: Profiling in the Presence of Optimization and Garbage Collection
                      8: .sp
                      9: .ft 2
                     10: .ce 99
                     11: .NL
                     12: Andrew W. Appel*
                     13: Bruce F. Duba\(dg
                     14: David B. MacQueen\(dg
                     15: .sp 0.6i
                     16: .ce 99
                     17: .NL
                     18: November 1988
                     19: .sp 1i
                     20: .ce
                     21: .ft  2
                     22: ABSTRACT
                     23: .ft 1
                     24: .IP
                     25: Profiling the execution of programs can be a great help in tuning their
                     26: performance, and programs written in functional languages are no exception.
                     27: The standard techniques of call-counting and statistical (interrupt-driven)
                     28: execution time measurement work well, but with some modification.  In
                     29: particular, the program counter is not the best indicator of ``current
                     30: function.''
                     31: Our profiler inserts explicit increment and assignment statements into
                     32: the intermediate representation, and is therefore very simple to implement
                     33: and completely independent of the code-generator.
                     34: .LP
                     35: .sp 1i
                     36: .nr PS  8
                     37: .nr VS 10
                     38: .LP
                     39: * Supported in part by NSF Grant CCR-8806121 and by a Digital Equipment Corp.
                     40: Faculty Incentive Grant.
                     41: .LP
                     42: \(dg AT&T Bell Laboratories, Murray Hill, NJ.
                     43: .nr PS 10
                     44: .nr VS 16
                     45: .LP
                     46: .bp
                     47: .NH
                     48: Execution profiling
                     49: .LP
                     50: A large program usually consists of many small functions.  
                     51: When such a program is to be tuned for efficiency, it is necessary to
                     52: identify which of those functions are taking the bulk of the execution time.
                     53: Then the commonly-used functions can be made more efficient, or called less
                     54: often, or both.  By using a theoretical analysis of the algorithms used
                     55: in a program, such functions can be identified; but a complete
                     56: theoretical analysis is complex and
                     57: impractical for large programs.
                     58: 
                     59: An execution profiler provides an empirical measurement of the time spent
                     60: in each function.  A widely-used Unix tool, \fBprof\fP [Unix],
                     61: provides a count of how many times each function is called, and how many
                     62: seconds were spent in each function.   This information is very useful
                     63: in identifying which functions are in need of improvement, after which
                     64: a theoretical analysis of just those functions might be carried out,
                     65: a much less forbidding endeavor than analyzing the whole program.
                     66: 
                     67: \fBProf\fP gathers
                     68: call-count information 
                     69: by having the compiler insert at the beginning of each function
                     70: an instruction that increments a call-count variable associated with
                     71: the function.  An approximation to the amount
                     72: of total time spent in the function is gathered by the use of a timer
                     73: interrupt:  every 1/60th of a second, the operating system notes
                     74: in a ``histogram'' array the value of the program counter.
                     75: (This is an ancient technique [Johnston70].)
                     76: Then, at the end of program execution, \fBprof\fP estimates
                     77: the amount of time spent in each
                     78: function by summing the values in the histogram array
                     79: corresponding to program counter samples between the beginning and end of the
                     80: machine code for that function.
                     81: The interrupt-driven sampling method has a much lower overhead than
                     82: querying a clock on each entry to and exit from a function.
                     83: 
                     84: A more elaborate profiling tool, \fBgprof\fP [Graham82],
                     85: provides even more information.  
                     86: When one primitive function (e.g. a
                     87: table-lookup routine) is used in many places, it is useful to know,
                     88: not only the total time for the execution of the primitive, but also, how
                     89: much time to ``charge'' the calling functions. With \fBgprof\fP this is
                     90: approximated by keeping a count of the number of
                     91: times each call-site is used, and on that basis apportioning the
                     92: average execution time
                     93: of the called function to the functions that call it.
                     94: 
                     95: These approaches to execution-time measurement and apportionment pose certain
                     96: problems for optimizing compilers and for functional languages:
                     97: .IP 1.
                     98: The machine-code for a function is not necessarily all contiguous.
                     99: A function may be turned into several pieces of code, with portions of
                    100: the code for other functions interspersed.  This problem could certainly
                    101: be solved by elaborate bookkeeping in the optimizer and code generator,
                    102: but we wanted to avoid that complexity.
                    103: .IP 2.
                    104: An optimizer can expand functions in-line in other functions.
                    105: The program-counter method will charge the calling function
                    106: instead of the called function,
                    107: even though it might be desirable for in-line expansion to be made
                    108: semantically invisible.
                    109: .IP 3.
                    110: The histogram array in \fBprof\fP
                    111: must be proportional in size to the address range spanned by pieces of
                    112: code for executable functions.  Our runtime
                    113: system intersperses code and data
                    114: throughout memory; even worse, it periodically garbage collects, moving
                    115: code and data from place to place.  This problem could have been solved
                    116: by elaborate bookkeeping in the runtime system, which we also wanted to avoid.
                    117: .LP
                    118: We had to deal with these problems in the course of implementing a
                    119: profiler for an optimizing compiler for the functional language
                    120: Standard ML [Appel87].  The approach we used is described in the next
                    121: section.
                    122: .NH
                    123: Intermediate Representation of call-counting and current-function
                    124: .LP
                    125: For execution-time estimation we use a timer interrupt, as does
                    126: \fBprof\fP, to increment a histogram entry.  However, we don't use the
                    127: program counter to calculate which histogram entry to increment.
                    128: Instead, we maintain a ``pointer-to-current-function-entry'' in a
                    129: global variable called \fBcurrent\fP that is accessible to the
                    130: timer-interrupt handler.  Each function has associated with it two
                    131: auxiliary variables: a call-count and an interrupt-count.  On entry to
                    132: a function, it increments the call-count and assigns the address of
                    133: the interrupt-count variable into the global \fBcurrent\fP variable.
                    134: Then, when a timer interrupt occurs, the interrupt handler just increments the
                    135: variable that \fBcurrent\fP points to.
                    136: 
                    137: When a function returns \(em either normally or via an exception \(em
                    138: \fBcurrent\fP must be set back to the interrupt-count variable of the
                    139: function that it is returning to.  This resetting could be done either
                    140: by the calling function (after the called function has returned), or by
                    141: the called function before exit.  For several reasons, it is better done by
                    142: the calling function.  If the called function does the reset,  a stack
                    143: of current-function pointers is required; this is expensive to maintain.
                    144: A stack of current-pointers would also greatly complicate the treatment
                    145: of exception-handlers; with the caller-reset method, the exception-handler
                    146: justs sets the \fBcurrent\fP to point at the appropriate counter variable
                    147: on entry.
                    148: On recursive calls, \fBcurrent\fP need not be reset (as the calling
                    149: and called function are the same), but only the calling function knows
                    150: which calls are recursive.  And finally, tail-calls can be optimized
                    151: if the caller resets \fBcurrent\fP.
                    152: 
                    153: A tail-call is one that is not followed by any
                    154: executable code before the function returns.  After a tail-call, the
                    155: function will immediately return and therefore \fBcurrent\fP will
                    156: immediately be reset.  Therefore, it is not necessary for the calling
                    157: function to reset \fBcurrent\fP after a tail-call.
                    158: This is a useful optimization, and it is particularly important
                    159: when used with a compiler that optimizes tail-calls into jumps; if the
                    160: current pointer had to be reset after the tail-call, it would no
                    161: longer be a tail-call and performance would suffer dramatically.
                    162: Fortunately, it is easy to identify tail-calls statically as the
                    163: profiling instructions are being inserted.
                    164: 
                    165: We insert the profiling instructions as ordinary assignment statements
                    166: in the intermediate representation.  In almost any compiler's
                    167: intermediate representation it is easy to represent the operations
                    168: of fetching, adding one, and storing, for the call-count increment operation;
                    169: and storing, for the assignment to the \fBcurrent\fP variable.
                    170: 
                    171: Functional programming languages introduce another problem for the
                    172: design of profilers: what to do with anonymous, first-class functions.
                    173: The simplest choice is to do nothing; collect no call-counts and let
                    174: the time be charged to the caller of the unnamed function. The main
                    175: disadvantage of this solution, besides not having the call-counts, is
                    176: that there is no convenient way to find the code that contributes to
                    177: the cost of a profiled function that calls anonymous functions.
                    178: 
                    179: Probably the most general solution is to make up names for the unnamed
                    180: functions (for example, an unnamed function statically enclosed in function
                    181: \fIf\fP might be called \fIf.anon\fP).
                    182: If anonymous functions are given names they can be treated
                    183: just as any other function; call counts and execution time will be
                    184: reported.  Of course, the user will need to associate the new names
                    185: with the correct function, but in practice this is rarely a problem.
                    186: .NH
                    187: An example
                    188: .LP
                    189: To illustrate the technique, we present a simple example (figure 1).  The ML function
                    190: \f(CWsubset\fP takes a predicate function as an argument, and returns
                    191: a function that maps lists to lists; the output list will be that sublist
                    192: of the input list containing just those elements that satisfy the predicate.  
                    193: The user's program is displayed in typewriter font; the compiler puts some
                    194: scaffolding around it (indicated in italics) to make a record
                    195: containing all the functions declared by the user.
                    196: .KF
                    197: .DS
                    198: .ft CW
                    199: \fIlet\fP fun subset pred =
                    200:        let fun f nil = nil
                    201:              | f (a::r) = if pred a then a::f(r) else f(r)
                    202:         in f
                    203:        end
                    204: 
                    205:    fun isPrime x = 
                    206:        let fun test i = i>=x orelse (x mod i <> 0 andalso test(i+1))
                    207:         in test 2
                    208:        end
                    209: 
                    210:    val primes = subset isPrime
                    211: \fI in (subset, isPrime, primes)
                    212: end\fP
                    213: .ft R
                    214: .DE
                    215: .DS C
                    216: Figure 1.
                    217: .DE
                    218: .KE
                    219: If this code is compiled with profiling enabled, the compiler inserts
                    220: the call-counting and current-function instructions into the intermediate
                    221: representation.  Here, we display the effects as if written in the source language
                    222: (figure 2).
                    223: .KF
                    224: .DS
                    225: .ft CW
                    226: \fIlet val subset.CC = ref 0 and subset.IC = ref 0
                    227:     and subset.f.CC = ref 0 and subset.f.IC = ref 0
                    228:     and isPrime.CC = ref 0 and isPrime.IC = ref 0
                    229:     and isPrime.test.CC = ref 0 and isPrime.test.IC = ref 0
                    230: \fP
                    231:    fun subset pred =
                    232:        \fI(subset.CC := !subset.CC + 1;\fP
                    233:        \fIcurrent := subset.IC;\fP
                    234:        let fun f x =
                    235:            \fI(subset.f.CC := !subset.f.CC + 1;\fP
                    236:            \fIcurrent := subset.f.IC;\fP
                    237:            case x of
                    238:              nil => nil
                    239:            | a::r => let val pa = pred a
                    240:                       in \fIcurrent := subset.f.IC;\fP
                    241:                          if pa then a :: f(r) else f(r)
                    242:                      end
                    243:         in f
                    244:        end
                    245: 
                    246:    fun isPrime x = \fI(isPrime.CC := !isPrime.CC + 1;\fP
                    247:                    \fIcurrent := isPrime.IC;\fP
                    248:                    . . . )
                    249: 
                    250:    val primes = subset isPrime
                    251: \fI
                    252:  in ((subset, isPrime, primes),
                    253:      ((subset.CC, subset.IC, "subset"),
                    254:       (subset.f.CC, subset.f.IC, "subset.f"),
                    255:       (isPrime.test.CC, isPrime.test.IC, "isPrime.test"),
                    256:       (isPrime.CC, isPrime.IC, "isPrime")))
                    257: end\fP
                    258: .ft R
                    259: .DE
                    260: .DS C
                    261: Figure 2.
                    262: .DE
                    263: .KE
                    264: 
                    265: For each function, two variables are introduced:  a call-count and an interrupt-count.
                    266: On entry to a function, the call-count is incremented, and the global variable
                    267: \fBcurrent\fP is set to point to the interrupt-count.  On re-entry to a function
                    268: after a subroutine call, \fBcurrent\fP is reset to the function's
                    269: interrupt-count variable.  However, this is not necessary after recursive calls
                    270: and tail calls, e.g. the calls to \f(CWf\fP.
                    271: 
                    272: The initial \fIlet\fP-bindings create all the count variables, and the
                    273: last four lines produce, instead of just a record containing the user's
                    274: declared objects, a pair of records:  the user's declared objects, and a
                    275: list of records containing profiling variables, each with an identifying
                    276: string constant.  These string constants will be embedded in the executable
                    277: code for this module, and will enable the call-count variables to be self-identifying.
                    278: Our runtime system maintains a global list of these
                    279: 3-element records; when it is time to print an execution profile, they are
                    280: sorted in decreasing order of interrupt-count.
                    281: 
                    282: .KS
                    283: Our output looks like the output of \fBprof\fP:
                    284: .DS
                    285: .ft CW
                    286: %time  cumsecs   #call ms/call  name
                    287:  90.4     3.52   78189    .045  isPrime.test
                    288:   8.4     3.85    1000    .330  isPrime
                    289:    .7     3.88       0          (unprofiled)
                    290:    .2     3.89    1001    .009  subset.f
                    291:    .0     3.89    1001    .000  natlist
                    292:    .0     3.89       1    .000  subset
                    293: .ft R
                    294: .DE
                    295: .KE
                    296: 
                    297: Now, armed with this information, a programmer
                    298: might decide that it is worthwhile
                    299: re-writing the \f(CWisPrime\fP function
                    300: to make it as efficient as possible.  But at a certain point
                    301: the programmer will want to know what functions are calling \f(CWisPrime\fP
                    302: so he can make them call it less often.  By re-compiling with
                    303: \f(CWisPrime\fP unprofiled, any time spent in \f(CWisPrime\fP will
                    304: now be charged to the function that called it.  This is because
                    305: \f(CWisPrime\fP will not change the \fBcurrent\fP variable, so that the
                    306: timer-interrupt will increment the  count for the function that last
                    307: set \fBcurrent\fP \(em and this will be the one that called \fPisPrime\fP.
                    308: The profiling system won't do this automatically, but by comparing two
                    309: different execution profiles, one with \f(CWisPrime\fP compiled with
                    310: profile instructions and one with \f(CWisPrime\fP unprofiled, an accurate estimate
                    311: can be made of who is calling it.
                    312: .NH 
                    313: Advantages of our current-function method
                    314: .LP
                    315: Since we use ordinary intermediate-representation operators
                    316: for profiling,
                    317: the optimizer and code-generator ``believe'' that 
                    318: profiling operations are part of the program.
                    319: Since an optimizer must not modify the semantics of the program,
                    320: the semantics of profiling will not be modified either.
                    321: Therefore, if one function is copied and inserted in-line into another,
                    322: the call-count and current-function instructions will be copied and
                    323: inserted at the right place.  Other optimizations that break functions
                    324: into several disjoint pieces of code will leave the profiling
                    325: instructions in the appropriate places.
                    326: 
                    327: Furthermore, the result is that the implementation of the profiler
                    328: is completely independent of the code generator.  We have four different
                    329: code generators for our compiler (two different algorithms each for the
                    330: Vax and the Motorola 68020), and not a line of any of them was modified
                    331: for the installation of the profiler.
                    332: 
                    333: By compiling some functions unprofiled, as described in the previous section,
                    334: we can find out what callers are responsible for most of their execution time.
                    335: This kind of trick serves much the same purpose that the more elaborate
                    336: program \fBgprof\fP does; and it's a trick that wouldn't work with
                    337: a program-counter histogram.
                    338: Furthermore, our method is more accurate than \fBgprof\fP.  Suppose
                    339: functions \fIf\fP and \fIg\fP both call a function \fIisPrime\fP, but
                    340: \fIf\fP consistently makes expensive calls (that take a long time) while
                    341: \fIg\fP makes cheap ones.  \fBGprof\fP allocates the total time spent in
                    342: \fIisPrime\fP on the basis of call counts from \fIf\fP and \fIg\fP;
                    343: this will miss the fact that \fIf\fP is responsible for most of the cost.
                    344: In this example, when profiling for \fIisPrime\fP is turned off, \fIf\fP
                    345: and \fIg\fP will be charged for the actual time spent in \fIisPrime\fP
                    346: on their behalf.  (On the other hand, \fBGprof\fP will give an accurate
                    347: breakdown of call-site counts that our method does not provide.)
                    348: 
                    349: If a profiled function calls an unprofiled function, then during the
                    350: execution of the called function, all timer interrupts will be charged
                    351: to the caller (since \fBcurrent\fP still points to the caller's
                    352: variable).  This is often desirable, as described above.  But if an
                    353: unprofiled function calls a profiled function, then upon return to the
                    354: unprofiled function the \fBcurrent\fP pointer won't be reset, and
                    355: interrupts will continue to be charged to the called function after it
                    356: has returned.  This is undesirable, and should be prevented by the
                    357: compiler.  In a language with first-class functions, it is difficult
                    358: to prevent profiled functions from being passed as arguments to
                    359: unprofiled functions that might then call them.  In practice, this has
                    360: not proved to be a problem, probably because unprofiled functionals
                    361: are typically simple primitives like \fIapp\fP and \fImap\fP, which do
                    362: little intrinsic computation.
                    363: .NH
                    364: Overhead measurements
                    365: .LP
                    366: We ran the same program several times with various of our profiling
                    367: features enabled; this gives a reasonably accurate measurement of profiling
                    368: overhead:
                    369: .KF
                    370: .TS
                    371: tab(|) box center;
                    372: l c c
                    373: l n n.
                    374: |Time|%Overhead|LenL|LenR|Comp
                    375: _
                    376: User code|2801 sec|
                    377: _
                    378: Call counts|568|20.3%
                    379: Setting current function|286|10.2
                    380: Interrupts|47|1.7
                    381: _
                    382: Total Overhead|901|32.2%
                    383: .TE
                    384: .KE
                    385: The total overhead of 32% is not prohibitively expensive.  Our code generator
                    386: takes three instructions to increment a call-count (fetch, add, store);
                    387: a better instruction-selector could probably reduce this overhead to 8%, and the
                    388: total overhead to 20%.
                    389: 
                    390: .KF
                    391: There is also an implementation overhead; it turned out to be fairly simple
                    392: to get this profiler running.
                    393: .TS
                    394: tab(|) center;
                    395: l n.
                    396: Insertion of profiling instructions|49 lines
                    397: Interrupt handling|32
                    398: Global database|16
                    399: Report generation|72
                    400: _
                    401: Total|169 lines
                    402: .TE
                    403: In contrast, this paper is about 500 lines long.
                    404: .KE
                    405: .NH
                    406: Conclusion
                    407: .LP
                    408: Traditional approaches to profiling run into problems when we attempt to apply
                    409: them to functional languages where code may be moved around by garbage collection,
                    410: and the task is further complicated when an optimizing compiler freely rearranges
                    411: the code.  The basic difficulty is that the mapping between the current pc and the
                    412: currently executing function is difficult to maintain.  
                    413: 
                    414: We have found a simple way around this difficulty, which consists of
                    415: maintaining a global variable that always points to the interrupt
                    416: count for the current function, and which is to be charged whenever
                    417: there is a timer interrupt.  Because we manipulate this variable in the
                    418: intermediate representation of the compiler, our method is very easy
                    419: to implement and has no nasty interactions with code generation or
                    420: garbage collection algorithms (which already preserve semantics of
                    421: intermediate-representation operations).
                    422: 
                    423: This method has acceptable overhead and accuracy.  Furthermore, by
                    424: judiciously mixing profiled and unprofiled functions, one can extract
                    425: information on inherited costs as well as the direct costs of calling
                    426: particular functions.  This information is similar to that provided by
                    427: sophisticated profilers like gprof, but is more accurate.
                    428: .SH
                    429: References
                    430: .LP
                    431: .IP [Appel87] 1i
                    432: Appel, Andrew W. and MacQueen, David B.  ``A Standard ML compiler,''
                    433: in \fIFunctional Programming Languages and Computer Architecture\fP,
                    434: LNCS 274, G. Kahn, ed., pp 301-324, 1987
                    435: .IP [Graham82]
                    436: Graham, Susan L. Graham, Peter B. Kessler, and Marshall K. McKusick.
                    437: ``gprof: a call graph execution profiler''" in 
                    438: \fIProc. SIGPLAN '82 Symp. on Compiler Construction, SIGPLAN Notices\fP
                    439: 17(4), pp. 120-126, 1982.
                    440: .IP [Johnston70]
                    441: Johnston, T. Y., and Johnson, R. H., \fIProgram Performance
                    442: Measurement\fP, SLAC User Note 33, Rev. 1, Stanford University, California, 1970.
                    443: .IP [Unix]
                    444: Unix Programmer's Manual, ``prof command,'' section 1, Bell Laboratories, Murray Hill,
                    445: NJ, 1979.

unix.superglobalmegacorp.com

This archive runs on limited infrastructure. Preserving old code on modern bandwidth. Automated agents are requested to crawl responsibly.