Changelog:

  • 12 Sep 2025: move context switch forcing instructions out of main exercise part
  • 16 Sep 2025: update module load command to something that works with current portal modules
  • 17 Sep 2025: mention using module load gcc clang to get access to clang on portal before running make
  • 17 Sep 2025: add note about trying to remove venv directories in case they’re half setup

Work, perhaps with a partner:

  1. Download and extract the skeleton code [last updated 1 Sep 2025] (either on portal or NX or on another Linux system. The Windows Subsystem for Linux will probably not work).

  2. Look at the function measure_times_plain in empty-time.c and/or read its description below.

  3. Use make to build the empty-time and long-loop programs. (On portal, you may need to use module load gcc clang first or edit the Makefile to use gcc instead of clang or similar.)

  4. Obtain measurements from empty-time for at least 1 million iterations. You can do this with a command like ./empty-time plain 1000000 output:

    In this command:

    • plain selects the measure_time_plain function in empty-time.c; the other option is threshold
    • 10000000 options selects to obtain 1000000 measurements
    • output selects the base name of the output files

    This will produce three output files:

    • output-times.csv — records the amount of time between iterations of an otherwise empty loop
    • output-before-int, output-after-int — records the contents of /proc/interrupts, which contains counters of asynchronous exceptions, separated by cause and which core handled them. The contents are sampled before and after (respectively) executing the timing loop
  5. Examine the output files:

    • Plot the times you recorded (and save the plots for later). I strongly recommend plotting using a log-scale for the duration axis.

      You can use the plot.py script to do this, as described below.

    • Look at what asynchronous exceptions occurred based on the before-int and after-int files. You can use the interrupts-difference.py program to aid in comparing these (since doing so by hand would be tedious.)

  6. You’ll notice that, in contrast to what a very simple model of a processor would suggest, that the time varies significantly.

    There are probably a few main causes:

    • exceptions interrupting the execution of the program;
    • variations in what data is cached (already loaded into fast memory) versus not;
    • variations in speculative execution’s accuracy (the processor guessing what instructions to run before it is sure); and
    • variations in processor clock speed (usually done by the hardware to control power usage and/or heat dissipation)
  7. Try to estimate a threshold time which will separate time measurements which are high from an exception from the other measurements.

    We can then use this threshold to avoid measuring these shorter measurements and get a sample of more longer durations. empty-time.c has a measure_time_threshold function (described below) we’ll use for this purpose.

  8. Then verify and, if needed, refine your threshold:

    • Choose a core number to run on (one of the indices listed by lscpu --extended=CPU).

    • Obtain measurements only greater than the threshold and on that core using a command like

      THRESHOLD=AAA taskset -c BBB ./empty-time threshold 10000 new-output

      where AAA is your threshold, BBB is your selected core number.

      In this command:

      • THRESHOLD=AAA sets an environment variable called THRESHOLD, which empty-loop.c reads with the getenv function
      • taskset -c BBB ... runs ... only on the cores specified by BBB.
      • 10000 is the number of measurements to obtain (you can adjust this number if you wish)
      • new-output selects the name of the output files
    • Plot the resulting times (and save the plots for later). Try to determine if it’s likely that your program was context-switched away from for an extended period of time.

    • Use interrupts-difference.py to see how closely the actual number of asynchronous exceptions corresponds to the actual count on that core.

      Note that if your program was context switched away from, then we would not expect to see asynchronous exceptions from the time when it was not running.

    Write a brief explanation of how you choose your threshold and whether your attempts to verify it give you confidence in it.

  9. If you have time, estimate how long your measurement program is context switched away from. If your measurement program is not context switched away from, see the advice on encouraging context switches below.

  10. Show a TA:

    • Your plots.

    • Your explanation for item 8.

    OR place an explanation for item 8 in a file README.txt and submit your plots and explanation to the submission site.

1 measure_times_plain

static void measure_times_plain(int count, long *starts, long *durations) {
    struct timespec first, current, previous;
    clock_gettime(WHICH_CLOCK, &first);
    clock_gettime(WHICH_CLOCK, &previous);
    for (int i = 0; i < count; i += 1) {
        clock_gettime(WHICH_CLOCK, &current);
        starts[i] = nsec_difference(&first, &previous);
        durations[i] = nsec_difference(&previous, &current);
        previous = current;
    }
}

This function reads a clock in a loop count times, and records the results to the starts and durations array. The only thing that happens between obtaining samples is storing the time and related bookkeeping.

If this loop were the only thing running, we would expect to see the durations are roughly constant. You should

2 measure_times_threhsold

This function adds the following to measure_times_plain

  1. Code to read from the environment variable THRESHOLD into the local variable threshold:

    const char *threshold_string = getenv("THRESHOLD");
    if (!threshold_string) {
        fprintf(stderr, "THRESHOLD environment variable not set (example: THRESHOLD=100 ./empty-time ...)\n");
        exit(EXIT_FAILURE);
    }
    long threshold = atoi(threshold_string);
  2. Code to check the the threshold before recording a value (and corresponding adjustments to the loop):

    long difference = nsec_difference(&previous, &current);
    if (difference > threshold) {
        starts[i] = nsec_difference(&first, &previous);
        durations[i] = nsec_difference(&previous, &current);
        i += 1;
    }

3 Plotting script

We supply a plotting script in plot.py, which requires the Python libraries numpy and matplotlib.

To make these work on portal:

After you’ve done the above setup you can use

python plot.py time output-times.csv output-times.png

to plot data from output-times.csv and write the result to the image file output-times.png.

4 interrupts-difference.py

The interrupts-difference.py script will compute the difference between two /proc/interrupts snapshot.

python interrupts-difference.py output-before-int output-after-int

will show the number of interrupts, split up by the type idenified. Interrupts will be attributes to specific cores, labeled CPU0, CPU1 and so on. If an interrupt type or CPU is not in the output, then the difference was zero.

If you only want the count of interrupts for a specific core, you can use a command like

python3 interrupts-difference.py --core=3 output-before-int output-after-int

to show only the differences for core number 3. (Core numbers start at 0.)

5 encouraging context switches