6
\$\begingroup\$

The following code is intended to infer the alignment of double.

My questions include:

  1. Is there a better to do this? Either by refining the code or using an entirely different approach.

  2. Under what (reasonable) circumstances could this not work?

  3. Under what (plausible) circumstances could this not work? See below.

  4. What is the actual or practical complete list of types I would need to do this with to determine universal alignment?

    I think it's short,int,long,long long,float,double, long double, void*. Specifically char can't be have a alignment constraint (or c-strings are broken!), unsigned can't be different from signed (or type-aliasing is broken). Is there any reason why void* can't stand in for all pointer alignments?

#include <stdio.h>

// *********************************************************************** //
//WARNING: Do not allow this translation unit to be compiled under the 
//#pragma pack directive or similar unless whole application is compiled the same way.
// *********************************************************************** //

typedef struct {
    char mWedge;//Wedges the member off alignment with the start of the struct.
    double mMember;//Member with type we're going to get alignment of.
} DoubleAligned;

const size_t getDoubleAlignment(void){
//A bit of pointer shennanigans to count the padding between mWedge & mMember.
//See text. 
    const DoubleAligned lSpecimen;
    const size_t lOffset= (char*)&(lSpecimen.mMember)-(char*)&(lSpecimen.mWedge);
    return lOffset;
}

int main(void) {
    printf("Alignment : %zu\n",getDoubleAlignment());
    return 0;
}

Expected output on platform where double has 4-byte alignment:

Alignment : 4

Expected output on platform where double has no alignment (AKA 1-byte alignment:

Alignment : 1

NB: If the compiler 'packs' structures by default and just 'takes the hit' on slicing and shuffling to get them into hardware alignment, I don't mind getting an output of 'Alignment : 1'. I'm interested in how the platform (programming environment presented to me) is aligned not trying to infer underlying hardware design.

Reasoning

The C specification (C99 to be specific) makes the following commitments:

  1. malloc(.) returns a universally aligned address.

  2. Structures can pad between members and at the end but specifically not the beginning.

  3. Arrays of adjacent structures must be aligned in a way compatible with their members.

  4. Members will be laid out in the order they are declared.

So the compiler has to place mMember in an aligned position after mWedge itself aligned to the start of DoubleAligned. The C specification doesn't further constrain why or how structures are padded but there appears to be no normal reason to pad them other than minimally to satisfy 2, 3 & 4 above.

Hence my question about 'plausible' circumstances. Formally the compiler can do what it likes including adding arbitrary space into structures (but not at the beginning) for reasons that have nothing to do with alignment - maybe debug info or overwrite detection. Does that actually happen?

I'm not entirely sure about void* standing in for all pointer types. void* must have the lowest-common-factor alignment of all the pointer types (not the types pointed to!).

Bonus Note:

On many platforms I can get away with:

const size_t getDoubleAlignment(void){
//A bit of pointer shennanigans to count the padding between mWedge & mMember.
//See text. 
    const DoubleAligned* lSpecimen=NULL;
    const size_t lOffset= (char*)&(lSpecimen->mMember)-(char*)&(lSpecimen->mWedge);
    return lOffset;
} 

But I believe this is formally undefined because it performs arithmetic on NULL.

Final Note: Why do you need to know? (someone will ask) Suppose I need a portable alternative malloc(.) implementation that uses pre- and post- 'overhead' areas. I could implement it around the native malloc(.) but would need to know what "universal alignment" meant to comply with the C standard for malloc(.) to carve out a universally aligned return value.

\$\endgroup\$
0

2 Answers 2

1
\$\begingroup\$
  1. Is there a better to do this (infer the alignment of double)?

Curious as to why size_t type is used. The first choice should be ptrdiff_t.

When two pointers are subtracted, ... and its type (a signed integer type) is ptrdiff_t defined in the <stddef.h> header. C11dr §6.5.6 9

Since alignment is certainly something less than 127, any integer type will do for our purposes. Could use int, unsigned, size_t.

The cast of &(lSpecimen.mWedge) is redundant other than to remove the const

My GCC 4.5.3 compiler says the const on the return type is ignored.

int getDoubleAlignment(void) {
  const DoubleAligned lSpecimen;
  const int lOffset = (char*)&(lSpecimen.mMember) - &(lSpecimen.mWedge);
  return lOffset;
}

  1. Under what (reasonable) circumstances could this not work? (Nothing to add)

  2. Under what (plausible) circumstances could this not work? (Nothing to add)


  1. What is the actual or practical complete list of types I would need to do this with to determine universal alignment?

Rather than use double, use max_align_t if available. max_align_t is not in C99.

"... max_align_t which is an object type whose alignment is as great as is supported by the implementation in all contexts ..." C11dr §7.19 2

Otherwise instead of double use a union of all non-narrow types:

union {
  int i;
  long l;
  long long ll;
  float f;
  double d;
  long double ld;
  float _Complex fc;
  double _Complex dc;
  ptrdiff_t pd;
  // Suspect the below are the only 4(5) code may truly need
  long double _Complex ldc;
  void *p;
  void (*pf)();
  intmax_t im;
  // C11
  max_align_t ma;
} OneOfEach;

void* might be narrower than void (*pf)() so void* will not work for all pointers.

\$\endgroup\$
1
  • \$\begingroup\$ Thanks. I agree I should probably have used ptrdiff_t. I notice the offsetof(.,.) macro (C99) also returns size_t. It turns out I should have used that. I think there's a tendency to use size_t when (as here) the result is known to be non-negative. Though as you point out almost any type would do. I agree, I made one unnecessary cast and shouldn't have thrown away const. I wasn't aware of max_align_t that's useful in trying to emulate malloc(.) properly. It's interesting that portability support for alignment has been weak for so long. \$\endgroup\$
    – user59064
    Commented Dec 30, 2014 at 9:13
0
\$\begingroup\$

My revised code is (for the relevant part):

#include <stddef.h> //Defines offsetof(.,.) 

#define alignment_of(type) offsetof(struct { char w;type v;},v)

const size_t getDoubleAlignment(void){
    return alignment_of(double);
}

I should have used the offsetof(.,.) macro.

See a discussion here:

https://stackoverflow.com/questions/6700114/portability-of-using-stddef-hs-offsetof-rather-than-rolling-your-own

NOT RECOMMENDED

If offsetof(.,.) isn't available then the most portable solution is (I believe):

const size_t getDoubleAlignment(void){
    const DoubleAligned lSpecimen;
    const size_t lOffset= (const char*)&(lSpecimen.mMember)-&(lSpecimen.mWedge);
    return lOffset;
}

I set aside discussions of size_t vs ptrdiff_t for compatibility with offsetof(.,.).

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.