Estimation of PID computation time

Hi guys,

I’ve written a simple test that is a (sort of) simulation of a system just to estimate the computation time of a PID (I’m using the PID library).
It works (or seems to work) and on my UNO it prints a mean time of 78.4 us but I don’t understand why the time increases if I add some code at the end of setup (after the time calculation!).

This is the code:

#include <PID_v1.h>

/** CONSTANTS */
/**/  const uint8_t  MAX_COMPUTATIONS   = 10;
/**/
/**/  /* PID PARAMETERS */
/**/  const double   KP                 = 0.01;
/**/  const double   KI                 = 0.01;
/**/  const double   KD                 = 0.01;
/**/  const double   REFERENCE          = 10000;
/**/  const uint32_t COMPUTATION_PERIOD = 80;      /* Expressed in milliseconds */
/**/  /* END PID PARAMETERS */
/**/
/** END CONSTANTS */

void setup() {
  Serial.begin(9600);
  Serial.println("PID computation test.");

  double input=0, output=0;
  PID    pid(&input, &output, (double*)&REFERENCE, 0.1, 0.01, 0, DIRECT);

  pid.SetSampleTime(COMPUTATION_PERIOD);
  pid.SetMode(AUTOMATIC);

  bool    pidComputed  = false;
  uint8_t computations = MAX_COMPUTATIONS;
  long    startTime, diffTime;
  long    totComputationTime = 0;

  do {
    startTime = micros();
    pidComputed = pid.Compute();
    diffTime = micros() - startTime;
    if (pidComputed) {
      computations--;
      totComputationTime += diffTime;
      Serial.print("Computation #");
      Serial.println(MAX_COMPUTATIONS-computations);
      Serial.print("Input:  ");
      Serial.println(input, 15);
      Serial.print("Output: ");
      Serial.println(output, 15);
      Serial.println("-----------------------------");
      if (0 != output)
        input += output*REFERENCE/255.0;
      else
        input *= 0.9;
    }
  } while(computations > 0);
  Serial.print("Mean time: ");
  Serial.print((double)totComputationTime / (double)MAX_COMPUTATIONS);
  Serial.println(" us");

  /*  THE TIME PRINTED BEFORE _INCREASES_ IF I ADD SOME CODE HERE */
}

void loop() {
}

Any suggestion is really appreciated.
Thank you in advance for your attention and collaboration.

Regards,
Marco

My wild guess is that the compiler is optimizing away some of your code. Try making most of your variables volatile to prevent that.

...R

Hi Robin,

thanks for your answer.

Robin2: My wild guess is that the compiler is optimizing away some of your code. Try making most of your variables volatile to prevent that.

...R

That was the first thing that i thought too but, even declaring all variables volatile, the same increment appears. Any other idea? Than you in advance for your attention and collaboration.

Regards, Marco

You should put a small delay of ~50ms in your do..while to allow serial communication to complete. But in order to really help you, you need to post the full sketch - including the part that alters the computation time - instead of just snippets. Usually the culprit is not in the snippets :)

Hi Danois,

thanks for your answer.

This is the update of the code I’m using for testing:

#include <PID_v1.h>

/** CONSTANTS */
/**/  const uint8_t  MAX_COMPUTATIONS   = 10;
/**/
/**/  /* PID PARAMETERS */
/**/  const double   KP                 = 0.01;
/**/  const double   KI                 = 0.01;
/**/  const double   KD                 = 0.01;
/**/  const double   REFERENCE          = 10000;
/**/  const uint32_t COMPUTATION_PERIOD = 80;      /* Expressed in milliseconds */
/**/  /* END PID PARAMETERS */
/**/
/** END CONSTANTS */

void setup() {
  Serial.begin(9600);
  Serial.println("PID computation test.");

  volatile double input=0, output=0;
  volatile PID    pid(&input, &output, (double*)&REFERENCE, 0.1, 0.01, 0, DIRECT);

  pid.SetSampleTime(COMPUTATION_PERIOD);
  pid.SetMode(AUTOMATIC);

  volatile bool    pidComputed  = false;
  volatile uint8_t computations = MAX_COMPUTATIONS;
  volatile long    startTime, diffTime;
  volatile long    totComputationTime = 0;

  do {
    startTime = micros();
    pidComputed = pid.Compute();
    diffTime = micros() - startTime;
    if (pidComputed) {
      computations--;
      totComputationTime += diffTime;
      Serial.print("Computation #");
      Serial.println(MAX_COMPUTATIONS-computations);
      Serial.print("Input:  ");
      Serial.println(input, 15);
      Serial.print("Output: ");
      Serial.println(output, 15);
      Serial.println("-----------------------------");
      if (0 != output)
        input += output*REFERENCE/255.0;
      else
        input *= 0.9;
    }
    delay(34);
  } while(computations > 0);
  Serial.print("Mean time: ");
  Serial.print((double)totComputationTime / (double)MAX_COMPUTATIONS, DEC);
  Serial.println(" us");
}

void loop() {
}

I’ve tested this code increasing progressively the delay between 1 ms and 50 ms.
These are the results:

  • without the delay or with a delay greater than 34 → the mean time is 92 us
  • with a delay between 1 ms and 34 ms → the mean time is between 95.19 us and 91.19 us (apparently it decreases linearly)

Danois90:
But in order to really help you, you need to post the full sketch - including the part that alters the computation time - instead of just snippets. Usually the culprit is not in the snippets :slight_smile:

I know and I haven’t posted the “full sketch” just because apparently each instruction at the end of the setup can influence differently the mean time.

For example, once I’ve declared all variables volatile the Serial.print() seems to have no effect on the mean time, otherwise if I insert another time evaluation

startTime = micros();
pid.Compute();
Serial.println(micros() - startTime);

the mean time increases (it becomes 102 us without the delay and 100.8 us with a 35 ms delay) and if I insert a

for(int i=0; i<1000000; i++){
    computations++;
}

the mean time doesn’t change with a 35 ms delay and decreases (!!) without the delay.

I don’t understand how the delay can influence the computation time of the PID and how some code that is executed once the mean time is calculated (!!) can influence the computation time of the PID!

Thank you in advance for your attention and collaboration.

Regards,
Marco

Not sure about the Arduino IDE, but loop unrolling is a optimization procedure a compiler can use to speed up execution by increasing code size.

Without a actual endless loop in your code, the compilers can rewrite your limited loops into repetitive straight line statements that process faster.

Everything about this platform is open source so it should be possible to read up on what techniques the compiler uses to optimize code.

Danois90: You should put a small delay of ~50ms in your do..while to allow serial communication to complete.

No. Properly written code would NEVER do that.....

Regards, Ray L.

RayLivingston: No. Properly written code would NEVER do that.....

Regards, Ray L.

Agreed, but in this instance the "benchmarking" prevents serial to complete and therefore it may - due to longer interrupt time - influence the speed of the code execution.

OP may experiment with disabling interrupts during computation, and see if that changes anything:

noInterrupts();
startTime = micros();
pidComputed = pid.Compute();
diffTime = micros() - startTime;
interrupts();

The "problem" may be related to compiler optimization, if this should be the case, OP may defer the benchmarking to an un-optimized method, like so:

void __attribute__((optimize("O0"))) unOptimized()
{

  //Code in this method will be exempt from optimization

}

Hi guys,

thank you all.
Good news: the code below (which uses the optimization attribute) doesn’t print different values of mean time adding some code after the calculation (at the end of the function pidComputationTime()).
Disabling the interrupts and re-enabling them at the end of each computation has no effect on the printed mean time.
BUT declaring the variables volatile or not or adding a short delay (35 ms) into the do-while make the mean time change slightly.
Declaring ALL the variables volatile and without the delay → the mean time is 103.59 us
Declaring ALL the variables NOT volatile and without the delay → the mean time is 104.00 us
Declaring ALL the variables volatile and adding the delay → the mean time is 102.00 us
Declaring ALL the variables NOT volatile and adding the delay → the mean time is 102.00 us

This is the code:

#include <PID_v1.h>

/** CONSTANTS */
/**/  const uint8_t  MAX_COMPUTATIONS   = 10;
/**/
/**/  /* PID PARAMETERS */
/**/  const double   KP                 = 0.01;
/**/  const double   KI                 = 0.01;
/**/  const double   KD                 = 0.0;
/**/  const double   REFERENCE          = 10000;
/**/  const uint32_t COMPUTATION_PERIOD = 80;      /* Expressed in milliseconds */
/**/  /* END PID PARAMETERS */
/**/
/** END CONSTANTS */

/* Code in this method will be exempt from optimization */
void __attribute__((optimize("O0"))) pidComputationTime()
{
  volatile double input=0, output=0;
  volatile PID    pid(&input, &output, (double*)&REFERENCE,  KP, KI, KD, DIRECT);

  pid.SetSampleTime(COMPUTATION_PERIOD);
  pid.SetMode(AUTOMATIC);

  volatile bool    pidComputed  = false;
  volatile uint8_t computations = MAX_COMPUTATIONS;
  volatile long    startTime, diffTime;
  volatile long    totComputationTime = 0;

  do {
    startTime = micros();
    pidComputed = pid.Compute();
    diffTime = micros() - startTime;
    if (pidComputed) {
      computations--;
      totComputationTime += diffTime;
      Serial.print("Computation #");
      Serial.println(MAX_COMPUTATIONS-computations);
      Serial.print("Input:  ");
      Serial.println(input, 15);
      Serial.print("Output: ");
      Serial.println(output, 15);
      Serial.println("-----------------------------");
      if (0 != output)
        input += output*REFERENCE/255.0;
      else
        input *= 0.9;
    }
  } while(computations > 0);
  Serial.print("Mean time: ");
  Serial.print((double)totComputationTime / (double)MAX_COMPUTATIONS, DEC);
  Serial.println(" us");
}

void setup()
{
  Serial.begin(9600);
  Serial.println("PID computation test.");

  pidComputationTime();
}

void loop()
{
}

Now I’ve two questions:

  1. Why does the delay influence the mean time (even disabling the optimizations and the interrupts during the computation of the PID)??
  2. The optimizations that compiler applies on the variables can be disabled only declaring them volatile?
    I supposed that attribute((optimize(“O0”))) would have disabled all the optimizations of the compiler for the specific function BUT declaring the variables volatile still has effects if I don’t add the delay (I supposed that the optimization attribute would have implied all variables to be volatile, at most implicitly).

Thank you guys for the collaboration.

Regards,
Marco

1) Serial communication is buffered. When you execute "Serial.println("XY")", then "XY" will be put in the serial buffer from where it is "flushed" when time is granted or the buffer is full. Putting in a small delay will give time to flush the serial buffer without it having any (negative) influence on other code execution. Usually you would not mind serial communication unless the code is "time critical".

2) "volatile" does not only mean that the variable may not be optimized away, it also means that the variable must be handled differently. When a variable is volatile, it must be read from/written to memory every time it is used since it may be read/written by something else than the code in where it is declared. Had the variable not been volatile, it could have been living it's entire cycle in a register in order to optimize code execution.

  Serial.begin(9600);The 1970s called - they want their baud rate back.

ErVito: It works (or seems to work) and on my UNO it prints a mean time of 78.4 us but I don't understand why the time increases if I add some code at the end of setup (after the time calculation!).

Probably register pressure in code generation - more code and variables in the function, the processor has finite resources so register spilling(*) will increase.

If you don't understand that, either learn about compiler code-generation, or just accept that compiler writers know what they are doing and try to achieve reasonable compromises between compiler correctness, optimization smartness and predictability of behaviour.

(*) code generation allocates virtual registers to values in the calculations, then attempt to map these to actual machine registers (called register colouring), and those that don't fit are "spilled" to the stack. Optimization is mainly a question of weighting virtual registers by how often they are likely to be referenced (with inner loops getting more priority).