Optimization Forum Game?

Hey Guys,

sometimes I'm a bit of a fizz-buzzkill. so how about we play the opposite of this game and see how hard we can optimize a piece of code i'm guilty of?

things that still need to be done:

  1. somehow stop the actual work and go on if it takes longer than a cycle should take
  2. measure the time for the cycle calculation stuff and give it less time to sleep
    (3. is the bit poking in readData right?)

and maybe some rules:
No Quake-Hacks, only things you can explain at the time of writing. If you explain it hacks and hardware abuse is fine.
Comment your code, give future generations a chance to learn. And no, "self-explanatory code" isn't a thing :wink:

and here the code i have so far comes:

///
/// Tool for performing tasks at a given interval
/// 
/// (C) 2021/2/16 by NO ONE! Public domain. Everyone did it. I do not take ANY 
/// responsibilities. Use at your own risk. Better don't use it in production 
/// at all! You have been warned!
///

#define MDEBUG 1  // send some useful data to Serial

#define PIN_BUTTON 7  // pin on which the button to pause is plugged in
#define PIN_DATA A0 // pin from which to read sensor data, leave unplugged for random data

// delayMicroseconds longer than this would become inaccurate, use delay instead, https://www.arduino.cc/reference/en/language/functions/time/delaymicroseconds/
#define MAX_DELAY_MICROS 16383  // don't we all love magic numbers!

const uint32_t cyclesPerUpdate = 42;  // do stuff every cyclesPerUpdate'th cycles
const uint32_t cyclesPerSecond = 30;  // aim for this many cycles every second

void readData();
void computeData();
void printData();

struct Timings {
  uint64_t start = 0; // start of a cycle
  uint64_t end = 0; // end of a cycle
  uint64_t cycleTime = 0; // time the cycle needed

  uint64_t sleep = 0; // maximum time a cycle may take
  uint64_t delay = 0; // how long to delay between cycles

  uint64_t cycleStart = 0;  // time the cycles-per-seconds counter was started
  uint32_t cyclesPerSecond = 0; // number of cycles per million microseconds (=one second)
  uint64_t cyclesMicros = 0;  // how long the second actually was

  uint64_t cycleCount = 0; // number of cycles we survived so far. will overflow quickly.

  float workload = 0.0f;  // how much of the total time available (sleep) we use for a cycle
} timings;

bool bPaused = false;
struct Button {
  uint64_t change = 0;  // millis() when the change happened
  uint64_t cooldown = 250;  // how long to wait before accepting the next event
  bool lastState = false; // what it changed from
  bool state = false; // what it changed to
} button;

// just some data for benchmarking
#define DATA_SIZE 8
struct Data {
  uint8_t d1[DATA_SIZE];
  uint16_t d2;
  float d3;
} data;

void setup() {
  Serial.begin(115200);

  pinMode(LED_BUILTIN, OUTPUT);

  pinMode(PIN_BUTTON, INPUT);

  pinMode(PIN_DATA, INPUT);

  timings.sleep = 1000000 / cyclesPerSecond;  // 1 second divided by how often we want to run per second gives the total time we have per cycle

  timings.cycleStart = micros();
}

void loop() {
  timings.start = micros();
  timings.cyclesPerSecond++;
  timings.cycleCount++;

  button.lastState = digitalRead(PIN_BUTTON);
  if ((button.state != button.lastState) && (timings.start - button.change > button.cooldown)) {
    button.state = !button.state;
    button.change = timings.start;

#ifdef MDEBUG
    Serial.print("Button changed: ");
    Serial.print(button.state);
    Serial.print(", Paused: ");
    Serial.println(bPaused);
#endif

    // only change if the button was pressed, not when it was released
    if (button.state == HIGH) {
      bPaused = !bPaused;
    }
  }

  // if we are paused take a break and see in the next cycle if the user wants to go on...
  if (bPaused == true) {
    delay(250);
    timings.end = micros(); // useless at the moment
    return;
  }

  // if the boss is looking pretend to do some useful work
  if (timings.cycleCount % cyclesPerUpdate == 0) {
    digitalWrite(LED_BUILTIN, HIGH);
    readData();
    digitalWrite(LED_BUILTIN, LOW);
    computeData();
    digitalWrite(LED_BUILTIN, HIGH);
    printData();
    digitalWrite(LED_BUILTIN, LOW);
  }

  timings.end = micros(); // yay! we are done with the work for this cycle! calculating cycle stuff now...
  timings.cyclesMicros = timings.end - timings.cycleStart;  // we will need this to know if the second to measure the number of cycles in a second is over

  timings.cycleTime = timings.end - timings.start;  // get an idea about how long this one cycle took...

  // see how much time we have left so we can rest a bit maybe
  if (timings.sleep > timings.cycleTime) {
    timings.delay = timings.sleep - timings.cycleTime;
  } else {
    timings.delay = 0;
  }

  timings.workload = 100.0 - ((100.0 / timings.sleep) * timings.delay); // how much of the total time per cycle we used in percent, = approx. cpu workload

#ifdef MDEBUG
  Serial.print("Timings: Time spent: ");
  Serial.print((uint32_t) timings.cycleTime);
  Serial.print(" um, sleeping: ");  // the micro-m isn't ascii, is it?
  Serial.print((uint32_t) timings.delay);
  Serial.print(" um ( of ");
  Serial.print((uint32_t) timings.sleep);
  Serial.print(" um, workload: ");
  Serial.print(timings.workload);
  Serial.println(" %)");
#endif

  // if we ran for a whole second (or more) let us know how many cycles we actually had ./. the number of cycles we want
  // keep in mind there is a 0th cycle, so it's cyclesPerSecond-1
  if (timings.cyclesMicros >= 1000000) {
#ifdef MDEBUG
    Serial.print("Cycles per second (");
    Serial.print((uint32_t) (timings.cyclesMicros / 1000));
    Serial.print(" ms): ");
    Serial.print(timings.cyclesPerSecond);
    Serial.print("(target = ");
    Serial.print(cyclesPerSecond);
    Serial.println(")");
#endif

    // reset the counter for how many cycles we've run in a second
    timings.cyclesPerSecond = 0;
    timings.cycleStart = micros();
  }

  // if we can sleep...
  if (timings.delay > 0) {
    if (timings.delay < MAX_DELAY_MICROS) { // ...see if the delayMicroseconds is precise enough...
      delayMicroseconds(timings.delay);
    } else { // ...or if we should rather use the delay(millis)
      delay((uint64_t) ((double) timings.delay / 1000.0));
    }
  }
}

void readData() {
  for (int i = 0;i < DATA_SIZE;i++) {
    data.d1[i] = analogRead(PIN_DATA);
  }

  data.d2 = analogRead(PIN_DATA);
  data.d2 <<= 8;
  data.d2 |= analogRead(PIN_DATA);
}

void computeData() {
  for (int i = 0;i < DATA_SIZE;i++) {
    data.d3 += data.d1[i] * i;
  }

  data.d3 = (float) data.d3 / (float) data.d2;
}

void printData() {
  Serial.println("Data: ");
  Serial.print("d1:");
  for (int i = 0;i < DATA_SIZE;i++) {
    Serial.print(" ");
    Serial.print(data.d1[i], HEX);
  }
  Serial.println("");

  Serial.print("d2: ");
  Serial.println(data.d2, HEX);

  Serial.print("d3: ");
  Serial.println(data.d3);
}

what's the purpose of all those uint64_t?

I did not get exactly what you were trying to achieve?

J-M-L:
what's the purpose of all those uint64_t ?

you mean in the Timings struct? to keep track of the time passed and calculate some statistics about how much time a cycle took, how many cycles were run in a second and such.
it's uint_64t (=unsigned long) because that's the datatype used by the delay, millis, micros and delayMicroseconds functions.

they use unsigned long so uint32_t to my knowledge (in Arduino land)

J-M-L:
they use unsigned long so uint32_t to my knowledge (in Arduino land)

i just checked, you are partially right. the delayMicroseconds takes an uint32 as argument, the others use unsigned longs:
delayMicroseconds (unsigned int)

delay (unsigned long)
(unsigned long) micros
(unsigned long) millis

uint32_t and unsigned long are the same thing on most processor / board combinations in the Arduino ecosystem.

uint64_t is an unsigned long long.

yeah, but unsigned long IS only 32 bit with the compiler we have for AVR, ESP, SAMD21 and other ARM based stuff.

frozenrat:
delayMicroseconds (unsigned int)

And unsigned int will be 16 bits (uint16_t) on AVR and uint32_t on 32 bits based boards

my fault. just the due has 32 bit ints, so i changed the code, creating time_uX_t and use that instead:

#ifdef __SAM3X8E__
  typedef uint32_t time_uint_t; // arduino due board uses 32 bit ints
#else
  typedef uint16_t time_uint_t; // while the rest of the world uses 16 bits
#endif

typedef uint32_t time_ulong_t;

and the timings struct:

struct Timings {
  time_ulong_t start = 0; // start of a cycle
  time_ulong_t end = 0; // end of a cycle
  time_ulong_t cycleTime = 0; // time the cycle needed

  time_ulong_t sleep = 0; // maximum time a cycle may take
  time_ulong_t delay = 0; // how long to delay between cycles

  time_ulong_t cycleStart = 0;  // time the cycles-per-seconds counter was started
  time_uint_t cyclesPerSecond = 0; // number of cycles per million microseconds (=one second)
  time_ulong_t cyclesMicros = 0;  // how long the second actually was

  time_ulong_t cycleCount = 0; // number of cycles we survived so far. will overflow quickly.

  float workload = 0.0f;  // how much of the total time available (sleep) we use for a cycle
} timings;

frozenrat, thanks for explanation. If we talk about developing and programming processes, especially game dev processes, I can share with you my own experience, and tell that after I had some issues with development process in the past, I've searched for dozens of dev companies, but the best one was iLogos with it's video game development solutions. I've looked at their portfolio and understood that it's the best developer of indie games, which also can port any game from PC to mobile, and that's why they helped me a lot.

This topic was automatically closed 120 days after the last reply. New replies are no longer allowed.