Sizeof question with a struct

I have this doubt, and I have a theory. You guys tell me if I'm right.

I have this code running on an UNO-R4 WiFi

enum PID_Direction {
	DIRECT, REVERSE
};

struct PID_Settings {
	double setpoint;

	double Kp;
	double Ki;
	double Kd;

	double outputMax;
	double outputMin;

	PID_Direction direction;

};

ID_Settings angleSettings = {
  .setpoint = 0,
  .Kp = 72.0,
  .Ki = 0.8,
  .Kd = 10.0,
  .outputMax = 5000,
  .outputMin = -5000,
  .direction = DIRECT
};

void setup() {

  Serial.begin(115200);
  delay(1000);
  Serial.print("Size of PID_Settings ");
  Serial.println(sizeof(PID_Settings));
  Serial.println(sizeof(angleSettings.Kp));
  Serial.println(sizeof(angleSettings.direction));


}

void loop() {

}

And the output is:

Size of PID_Settings 56
8
1

So the 8 I expect to see. A double on the R4 is 8 bytes. The 1 makes sense for an enum with only two options. I was expecting 4 since that's the size of an int but ok.

But then the 56. I have 6 doubles and a byte, so that should be 6*8 +1 = 49 bytes.

Is this an alignment thing where it's allotting 8 bytes for the direction variable even though it only needs one so that the struct stays aligned nicely? Can I rely on that size not changing even if I create more instances?

I would certainly expect a double would have to be aligned on either a 32-bit word or 64-bit word boundary. But that will be processor and compiler dependant. You can test this by creating another data object BEFORE the struct, to shift its address, and see if the size changes.

An enum, I think, does not have a defined size, as it is, effectively, a compile-time constant. So, a good compiler will use the smallest data type capable of holding the maximum enum value, in this case a byte.

Yes. But, the question makes me nervous. You generally should not need to rely on that.

I will be copying a few of these struct to and from EEPROM using put and get and I want to be sure they're all the same size when I lay out my addresses and offsets. So I know that they will be where I expect them to be when I come back for them later.

I just want to make sure that it's not something that is going to mysteriously change on me one day because I made a change elsewhere in the code and the compiler decides to do something different and suddenly my EEPROM images don't fit back into the struct right.

It's something that works but I can't 100% explain why it works and those are the things that scare me the most.

packs to 52..

struct __attribute__((__packed__))  PID_Settings {
	double setpoint;

	double Kp;
	double Ki;
	double Kd;

	double outputMax;
	double outputMin;

	PID_Direction direction;

};

i tend to pack structs im passing or saving..
good luck.. ~q

1 Like

enums are native ints, i believe..
could 2 or 4..

~q

Interesting, it packs to 49 here (compiling on Arduino IDE versions 1.8.19 and 2.2.1).

The output is showing that it has a size of 1.

im compiling on esp32, don't have the uno r4..
my enum is 32 bit yours is 16??

~q

You can explicitly specify the size of the enum

enum PID_Direction:uint8_t {
  DIRECT, REVERSE
};
2 Likes

figured it was something like that, just too tired, sorry..
uno with out it is 2 bytes..
~q

:+1:

Two things not mentioned...

  • The compiler is not allowed to reorder the fields. You can potentially use that to your advantage to save some space. For example, putting two enum together in your definition allows the compiler to place them together in the layout. In your case, enum is byte-sized so they can be byte-aligned. Your example just has the one enum so this is only helpful for PID_Settings if you add another field that's 1, 2, or 4 bytes in size.
  • I cannot find any references one way or the other but I suspect the optimization level could affect layout.

As mentioned above, "packing" can make the structure smaller. Were I in your shoes I'd have two versions. One that's stored in EEPROM and the one you've shown us that's used by the running program. The EEPROM one would be packed to save space and include a revision number so the future me can fairly easily alter the layout. You'd need a bit of code to pack and unpack (copy the fields).

Indeed. I put a header value (that can change with version I suppose) and a simple checksum with it when I put it into EEPROM.

I'm doing like this. I'm not sure how to take out the dead bytes here without copying fields one by one. a few empty bytes aren't going to hurt me I don't think. And like you said it leaves room in case I want to add something later without changing my layout.

struct PID_Settings_Store {
  byte header = 0x42;
  unsigned char settings[sizeof(PID_Settings)];
  byte sum = 0;

  void updateSum();
  bool validateSum();
  unsigned char calculateSum();
};

unsigned char PID_Settings_Store::calculateSum() {
  byte rv = header;
  for (int i = 0; i < sizeof(PID_Settings); i++) {
    rv += settings[i];
  }
  return rv;
}

void PID_Settings_Store::updateSum() {
  sum = calculateSum();
}

bool PID_Settings_Store::validateSum() {
  return calculateSum() == sum;
}

void storePIDSettings(unsigned int address, PID_Settings &settings) {
  PID_Settings_Store store;
  memcpy(&(store.settings), &settings, sizeof(PID_Settings));
  store.updateSum();
  EEPROM.put(address, store);
}

bool getPIDSettings(unsigned int address, PID_Settings &settings) {
  bool rv = false;
  PID_Settings_Store store;
  EEPROM.get(address, store);
  if (store.validateSum()) {
    memcpy(&settings, &(store.settings), sizeof(PID_Settings));
    rv = true;
  }
  return rv;
}

As soon as I need that bit again it will become a template and work with more than just PID_Settings.

Oh, I just got this. So this:

struct PID_Settings {
	double setpoint;

	double Kp;
	double Ki;
	double Kd;

	double outputMax;
	double outputMin;

	PID_Direction direction;
    SomeOtherEnumType someOtherValue;

};

might show as a different size from:

struct PID_Settings {
	double setpoint;

	double Kp;
	double Ki;        
    SomeOtherEnumType someOtherValue;
	double Kd;

	double outputMax;
	double outputMin;

	PID_Direction direction;

};

because it can't pack the enum types together.

Am I reading you right?

I'd be tempted to embed anything that might 'vary' in between the doubles, singly, though it's wasteful. May still not be foolproof, but if the compiler is busy aligning the doubles, chances are your size won't change. But hey, no compiler expert here.

Interesting question.

Does the same problem arise if we mix uint8_t, uint16_t and uint32_t variables in a struct? Will every compiler/version generate the same size of structure, or are those constructs still "at the compiler's discretion", and our EEPROM reads will be victim if we change? And, will the answer be different if we add 'packed' to our struct declarations?

you could have alignment decisions made based on the architecture. On a 8 bit UNO for example , you would not get any padding, but on a 32 bit ESP32 or MKR, the compiler might decide to align some members on a 32 bit boundary.

GCC offers the option to set some __attribute__ for the struct so you could do something like

struct __attribute__((packed, aligned(4))) Payload {
  •••
  •••
}

you can read more about packed or aligned in the doc

1 Like

:+1:

Yup. That's what you'd have to do. I'm a bit paranoid so that motivates what I'd do. It's probably not be worth the effort.

I suggest using a CRC-32 instead of a one-byte sum. The CRC is guaranteed to fail on single bit errors and runs. It's my understanding those are the common EEPROM failures.

Exactly.

Compiler / version / platform / optimization level ... yes. The optimization level may not matter. The compiler definitely matters. The version doesn't matter very often.

With time, that becomes less of a problem. There are strong forces pushing towards specific struct layouts. But, if the compiler vendor does not provide a written guarantee, you cannot assume it's true.

Yes. Packing forces the compiler's hand. The tradeoff with using a packed structure is performance. The double in @Delta_G's structure are eight-byte aligned because that makes the processor go fast.

This conversation has moved along since I last read it. Has there been a definitive answer as to why the struct in @Delta_G's initial post reported a sizeof() of 56 bytes? I would have expected a maximum of 6 * 8 + 4 = 52, allowing 4 bytes for the enum.

but this

and this

struct PID_Settings2 {
	double setpoint;

	double Kp;
	double Ki;
	double Kd;

	double outputMax;
	double outputMin;

	PID_Direction direction;
    PID_Direction direction2;
    };

should show the same size

8 bytes for the enum since there is an alignment to 8 bytes, so 7 * 8.

@b707 yes, it should.