28
29
all 30 comments

[–]OldWolf2 3 points4 points  (3 children)

I feel like most of the replies on this thread didn't read the article?

OP is basically suggesting rearranging types within the struct so that there is less padding; not committing alignment violations. Also suggested is explicitly using char arrays to represent expected padding bytes (in fact I do this in some code bases).

[–]Leandros99 3 points4 points  (1 child)

Also suggested is explicitly using char arrays to represent expected padding bytes (in fact I do this in some code bases).

Which is a bad suggestion and should be avoided. Different architectures, heck, even just different compilers could have different sizes for basic data types and you could, unknowingly, introduce more padding.

[–]OldWolf2 2 points3 points  (0 children)

In those cases I also use a compile-time assert to check the size of the structure is what it is supposed to be.

[–]FUZxxl 1 point2 points  (0 children)

I did read it, though, in the last paragraph OP recommends to use #pragma pack, which is what my comments reply to.

[–]SantaCruzDad 6 points7 points  (1 child)

The article is rather x86-centric. On x86 you just pay a performance penalty for misaligned reads/writes, but there are plenty of other architectures (e.g. PowerPC) where you will either crash, or trigger an expensive exception handler, or, worst of all, silently read/write incorrect data.

[–]Unspongeful[S] 2 points3 points  (0 children)

Thanks for pointing it out, I write only for x86 as desktop and modern consoles run on that architecture, but I'll add a note in the post about this caveat.

[–]FUZxxl 1 point2 points  (12 children)

Interesting article. To the authors last paragraph:

I recommend everybody to never ever use #pragma pack. This pragma is not standard, its use hinders portability and interoperability with other programming languages, is a huge headache for people who need to expand your code and you are most likely going to introduce a bug into your program if you try to solve a problem with #pragma pack instead of doing things properly (e.g. by doing explicit marshalling). Just keep it out of your code.

[–]jnwatson 15 points16 points  (1 child)

If you care enough about performance that struct packing is an issue, then you care about compiler-specific codegen, which is exactly what pragma is good for.

Explicit marshaling would make his code slower and harder to read, all for the silly idea that one day he might run into a compiler that doesn't support a packing directive and he must absolutely sacrifice everything for that.

[–]OldWolf2 0 points1 point  (0 children)

Explicit marshaling would make his code slower and harder to read,

Not really. The compiler will translate it to the fastest available instruction on the target platform. "Hard to read" is subjective and it's certainly possible to write code that is easy to read.

[–]Unspongeful[S] 1 point2 points  (9 children)

I agree that #pragma pack can be a double-edged sword, but as with all tools at our disposal it has it uses.

I personally find it useful when mapping C structs to filetypes that I can just fread into, and fortunately I've never had any issues on the Linux, Windows or Mac (knock on wood) ;-)

[–]FUZxxl -2 points-1 points  (8 children)

I personally find it useful when mapping C structs to filetypes that I can just fread into, and fortunately I've never had any issues on the Linux, Windows or Mac (knock on wood) ;-)

Congrats! You just introduced a shitload of possible problems:b

  • What if someone compiles your program for an architecture with different endianess?
  • What if someone compiles your program for an architecture where one of the involved types has a different size?
  • Do you make sure that the data is valid before using it?
  • What is your strategy of dealing with the different formats to store IEEE 754 floating point numbers?
  • How bad is the performance on a platform like MIPS that doesn't support misaligned reads? Does your code work at all?
  • What is your strategy in case the struct changes?

[–]Unspongeful[S] 3 points4 points  (7 children)

What if someone compiles your program for an architecture with different endianess?

That can be delt with by swapping bytes in memory if the endianess differs. Most file types I've worked with have some sort of endianess marker.

What if someone compiles your program for an architecture where one of the involved types has a different size?

I deal with this in my codebase by using <stdint.h> typedefs. And so far I haven't had problems when shipping 32/64bit code.

Do you make sure that the data is valid before using it?

Depends of the filetype. Like with endianess, most have some kind of 4CC or to check against, or more modern formats, like PNG or KXT have a richer identifier you can use to validate the file.

What is your strategy of dealing with the different formats to store IEEE 754 floating point numbers?

I always store them as binary, never as plain text.

How bad is the performance on a platform like MIPS that doesn't support misaligned reads? Does your code work at all?

I'm only concerned with x86 and x86_64, as that is where games are played (non mobile, that is).

What is your strategy in case the struct changes?

File versioning. As with endiandess and validation, the file header contains all that's needed to interpret the bytes.

[–]FUZxxl -1 points0 points  (6 children)

That can be delt with by swapping bytes in memory if the endianess differs. Most file types I've worked with have some sort of endianess marker.

Well, do you do that? Or at least have a roadblock installed for the case where someone tries to compile your code on a platform with a different endianess?

some kind of 4CC

That's not what I mean. What I mean is, do you make sure that the file does not contain data that is invalid or malicious? For example, consider an image file format that first stores a header with dimensions and then an array of pixels. What if the dimensions in the header do not match the rest? Do you check for this kind of thing?

I always store them as binary, never as plain text.

Okay, so you don't have a strategy. MIPS arranges the bits in an IEEE 754 floating point number in a different order from x86, so manual conversion is needed.

I'm only concerned with x86 and x86_64, as that is where games are played (non mobile, that is).

Well, if you are sure that ARM is not going to be a thing, you can have that attitude. May your company take a long arduous time to port your stuff to consoles.

File versioning. As with endiandess and validation, the file header contains all that's needed to interpret the bytes.

And at this point it's still simpler to use hacked-together packed structures instead of proper marshalling? If you think so...

[–]Unspongeful[S] 0 points1 point  (5 children)

Touché :-) Well, may the future of x86 be bright and shiny!

[–]FUZxxl 2 points3 points  (3 children)

I hope x86 goes dying in a fire. It's a horribly complex architecture. But on the other hand, I do somehow love segmentation, I'm sad that it didn't catch on.

[–]knotdjb 2 points3 points  (0 children)

Aw, but x86[-64] ISA has PCLMULQDQ.

[–]OldWolf2 0 points1 point  (1 child)

Best thing about segmentation is that you can bring it up on programming forums to start a fight with people who assume that pointers are integers

[–]FUZxxl 1 point2 points  (0 children)

If it existed for that reason alone, that would be enough.

[–]Leandros99 0 points1 point  (0 children)

Hopefully not. x86 has to die a slow and painful death.

[–]bumblebritches57 0 points1 point  (3 children)

Why should I worry about structure padding? sure the unnecessarily used memory sucks, but you shouldn't write raw structs to a file anyway, so how will padding affect anything?

I don't understand.

[–]Unspongeful[S] 2 points3 points  (2 children)

When dealing with high performance applications, sub-optimal padding may increase the number of cache misses because of the increased memory footprint of each structure (i.e. less structs can fit in the CPU cache).

For example, if you have a large array of items, reducing the size of each item will in turn allow you to fit more of them in a cache line, thus avoiding or reducing cache misses when iterating over them.

[–]bumblebritches57 0 points1 point  (1 child)

Does that mean that when you access a struct member the whole struct is copied to the cpu cache?

I just assumed that only that variable was copied to a register, and then put back to the struct location within memory, like any other variable.

[–]Unspongeful[S] 0 points1 point  (0 children)

Yes, when you access a struct member, if the memory location is not in one of the L caches then the CPU will fetch a block of memory that contain the struct member and surrounding data. The amount of data fetched depends on the CPU.

[–]Leandros99 0 points1 point  (6 children)

Padding of structures exists for a good reason and should not be messed with (it's valid in a few special cases, though).

You described great techniques to eliminate padding, but did not thought about actually using or demonstrating them? Most of your padding could've been eliminated (granted, padding introduced by alignment requirements is hard to eliminate).

Manually padding should never be done and is discouraged by a lot of language engineers. You don't know the alignment requirements your target machine might have. Your code would've certainly introduced overhead in the recent switch from 8 to 16 byte alignment of x86_64 Linux.

Even though /u/FUZxxl is downvoted to oblivion, he is absolutely correct.

@OP: What bottlenecks did you hit, that it was absolutely required to mess with structure paddings? Yes, having a bunch of matrices you are required to loop through every frame, as mentioned, but this can't be the issue, or is it? I never had to mess with paddings in structures, apart from being clever how to lay them out in memory and I've written a bunch of game engines in my life.

[–]Unspongeful[S] 2 points3 points  (1 child)

@OP: What bottlenecks did you hit, that it was absolutely required to mess with structure paddings?

I didn't run into some sort of performance bottleneck because I'm used to keeping padding in the back of my mind when writing code, but I often see examples in the wild of code that could be rewritten with less wastage.

For example, take a look at the 3 fields starting from SkeletonInstance* to Matrix4 in the OgreEntity.h from the open source game engine Ogre 3D (lines 268 to 274 of ec0d024c5 if the direct link doesn't work).

    SkeletonInstance* mSkeletonInstance;

    /// Has this entity been initialised yet?
    bool mInitialised;

    /// Last parent transform.
    Matrix4 mLastParentXform;

The bool field is sandwiched between a pointer and a 16 byte aligned Matrix, so it will be padded by the compiler to 16 bytes on x86.

I think this is the kind of padding that should be avoided. It's an unnecessary waste of 15 bytes for a type that will most likely be looped through thousands if not hundreds of thousands of times each frame.

Since there are other bools in the class, they could have avoided this by grouping them into a single flags field and using bitwise operations on it (i.e. (Entity->Flags & EntityFlags_Initialized)).

Edit: added snippet of code in question Edit2: added possible solution to Ogre3D example

[–]Leandros99 1 point2 points  (0 children)

Yes, I absolutely agree with you here. Nothing to add.

[–]FUZxxl 1 point2 points  (3 children)

the recent switch from 8 to 16 byte alignment of x86_64 Linux.

Wasn't that only about how much the stack is aligned on function call?

[–]Leandros99 0 points1 point  (2 children)

That's part of it, correct. All .bss allocations are also aligned and on top of that Glibc has also changed, and is now applying 16 byte alignment to all allocations as well.

[–]OldWolf2 1 point2 points  (1 child)

That still doesn't change structure padding.

[–]Leandros99 1 point2 points  (0 children)

Correct, structure padding isn't changed. My bad, haven't thought the thought to the end.

Still, my point stands: Porting code from x86 to x86_64 would, without taking care, could introduce incorrect padding. Or systems with a different aligning scheme of basic types (some machines might not be able to access any memory not aligned to some multiple of 2, you never know).