On this page:
1.1 Instrumenting and optimizing
1.2 My First Profile-Guided Meta-Program

1 PGMP Quick Start

1.1 Instrumenting and optimizing

This section presents the standard workflow for profiling and optimizing a profile-guided macro. We present a small example program using case, a profile-guided macro defined by pgmp/case.

"example-parser.rkt"

#lang racket/base
(require pgmp)
 
...
 
(define (parse stream)
 (case (peek-char stream)
  [(#\space #\tab) (white-space stream)]
  [(0 1 2 3 4 5 6 7 8 9) (digit stream)]
  [(#\() (start-paren stream)]
  [(#\)) (end-paren stream)]
  ...))
 
(module+ main (parse input-stream))

Here we define a module that uses case. To get the benefits of case, we first need to instrument and run the module. There are two ways to do this.

pgmp provides a commandline utility through raco. We could instrument and profile the module by running the command raco pgmp --profile example-parser.rkt. To optimize and run the module, we can henceforth use racket -t example-parser.rkt. case will automatically load and use the profile data from the instrumented run.

Alternatively, pgmp also provides API functions to instrument and profile one module from another. We can use the API to instrument and profile the module, then optimize and run the module via dynamic-require.

> (run-with-profiling `(submod "example-parser.rkt" main))
> (save-profile "example-parser.rkt")
> (dynamic-require `(submod "example-parser.rkt" main) 0)

1.2 My First Profile-Guided Meta-Program

In this tutorial, we present the API defined in pgmp/api/exact via an example profiled-guided macro. We define if-r, a macro that reorders its branches based on which branch is executed most frequently.

> (require pgmp)
> (define-syntax (if-r stx)
    (define profile-query
      (let ([f (load-profile-query-weight stx)])
        (lambda (x) (or (f x) 0))))
    (syntax-case stx ()
      [(_ test t f)
       (let ([t-prof (profile-query #'t)]
             [f-prof (profile-query #'f)])
         (if (< t-prof f-prof)
             #'(if (not test) f t)
             #'(if test       t f)))]))

First, the function load-profile-query-weight, which executes at compile-time, loads any profile information associated with (syntax-source stx) that has been saved from previous executions and returns a query function. The query function may return #f, but in this example we ignore the distinction between #f and 0 by always returning 0 when the query returns #f.

Next, we use profile-query to get the profile weight associated with each branch. profile-query can accept either a syntax? or profile-point?. Here, we query the syntax of each branch.

Finally, we generate code. If the false branch f is executed most often, then we negate the test and flip the branches when generating the if form. Otherwise, we generate a normal if form.

> (syntax->datum
    (expand-once
      #'(if-r (subject-contains-ci email "PLDI")
              (flag email 'important)
              (flag email 'spam))))

'(if (subject-contains-ci email "PLDI")

   (flag email 'important)

   (flag email 'spam))

We see that when we expand if-r before any profile information is generated and saved, the test and branches stay in the original order. While expanding is useful for debugging, it can expose implementation specific details, so do not rely on the output of expand being in a specific form.

Before if-r will reorder its branches, we need to generate some profile information. While we could use run-with-profiling and save-profile to actually profile the profile and save the profiling data, for this example we instead use the API to generate some new profile points example profile data.

> (define make-profile-point (make-profile-point-factory "my-first-pgmp"))
> (define profile-point-t (make-profile-point syntax))
> (define profile-point-f (make-profile-point syntax))

We use make-profile-point-factory to create a function that generates fresh profile points using the prefix "my-first-pgmp", and define two new profile points profile-point-t and profile-point-f. The profile point generator expects a piece of syntax. For this example, we assume we have some appropriate piece of syntax syntax which we use where needed places. In practice, this piece of syntax will be something in scope when defining a new profile-guidedmacro. If you are following along in the REPL, use some arbitrary piece of syntax such as #'void.

Next, we generate some profile information and save it to the file expected by load-profile-query-weight. We use profile-file to generate the filename based on syntax. The generated profile information claims that the expression associated with profile-point-t was executed 10 times, while the expression associated with profile-point-f was executed 20 times. Therefore, if-r should reorder the branches.

> (require racket/serialize)
> (with-output-to-file (profile-file syntax)
    (lambda ()
      (write (serialize (list (cons profile-point-t 10)
                              (cons profile-point-f 20)))))
    #:exists 'replace)

Finally, we annotate branches with the generated profile points, so profile-query will find the profile information for the generated profile points. We also annotate the whole (if-r ...) with syntax so load-profile-query-weight will load the right file.

> (syntax->datum
    (expand-once
     (quasisyntax/loc
       syntax
       (if-r (subject-contains-ci email "PLDI")
             #,(annotate-syn
               profile-point-t
               (flag email 'important))
             #,(annotate-syn
               profile-point-f
               (flag email 'spam))))))

'(if (not (subject-contains-ci email "PLDI"))

   ((lambda () (flag email 'spam)))

   ((lambda () (flag email 'important))))

Note that the branches have been eta-expanded by annotate-syn. This is an implementation detail used to coerce errortrace into accepting the new profile point.