On 9/17/2024 4:39 AM, David Brown wrote:
On 16/09/2024 21:46, BGB wrote:
On 9/16/2024 4:27 AM, David Brown wrote:
On 16/09/2024 09:18, BGB wrote:
On 9/15/2024 12:46 PM, Anton Ertl wrote:
Michael S <already5chosen@yahoo.com> writes:
Padding is another thing that should be Implementation Defined.
>
It is. It's defined in the ABI, so when the compiler documents to
follow some ABI, you automatically get that ABI's structure layout.
And if a compiler does not follow an ABI, it is practically useless.
>
>
Though, there also isn't a whole lot of freedom of choice here regarding layout.
>
If member ordering or padding differs from typical expectations, then any code which serializes structures to files is liable to break, and this practice isn't particularly uncommon.
>
>
Your expectations here should match up with the ABI - otherwise things are going to go wrong pretty quickly. But I think most ABIs will have fairly sensible choices for padding and alignments.
>
>
Yeah. It is "almost fixed", as there are a lot of programs that are liable to break if these assumptions differ.
>
>
Say, typical pattern:
Members are organized in the same order they appear in the source code;
>
That is required by the C standards. (A compiler can re-arrange the order if that does not affect any observable behaviour. gcc used to have an optimisation option that allowed it to re-arrange struct ordering when it was safe to do so, but it was removed as it was rarely used and a serious PITA to support with LTO.)
>
>
OK.
>
>
If the current position is not a multiple of the member's alignment, it is padded to an offset that is a multiple of the member's alignment;
>
That is a requirement in the C standards.
>
The only implementation-defined option is whether or not there is / extra/ padding - and I have never seen that in practice. (And there are more implementation-defined options for bit-fields.)
>
>
Extra padding seems like it wouldn't have much benefit.
No, generally not - which is why it would be a really strange implementation if it had extra padding. It's possible that extra padding at the end of a struct could lead to more efficient array access by aligning to cache line sizes, but I think such things are better left to the programmer (possibly with the aid of compiler extensions) rather than attempting to specify them in the ABI.
I haven't seen much in this area.
Usually just normally aligned structs, and packed structs.
>
Albeit, types like _Bool in my implementation are padded to a full byte (it is treated as an "unsigned char" that is assumed to always hold either 0 or 1).
That's the usual way to handle them.
Another option would be for adjacent _Bool values to merge similar to bitfields...
Though, seems that simply turning it into a byte is the typical option.
>
>
For primitive types, the alignment is equal to the size, which is also a power of 2;
>
That is the norm, up to the maximum appropriate alignment for the architecture. A 16-bit cpu has nothing to gain by making 32-bit types 32-bit aligned.
>
>
This comes up as an issue in some Windows file formats, where one can't just naively use a struct with 32-bit fields because some 32-bit members only have 16-bit alignment.
Ah, the joys of using ancient formats with new systems!
I was around when this stuff was still newish.
Some are essentially frozen in time with their misaligned members.
Still better than:
"Well, initial field wasn't big enough";
"Repurpose those bytes from over there, and glue them on".
My comment above was in reference to data remaining on the system, rather than moving off-system.
If I am making a format that is accessible externally - a file format, a network packet, etc., - I generally make sure all types are "naturally" aligned up to at least 8-byte types, even if the processor's maximum useful alignment is much smaller.
Makes sense. I usually also try to design things with everything properly aligned and (typically) any structures that are used in arrays having a power-of-2 size.
But, it seems people coming up with the file formats in the 80s/90s were a little more lax.
>
If needed, the total size of the struct is padded to a multiple of the largest alignment of the struct members.
>
That is required by the C standards.
>
>
>
>
For C++ classes, it is more chaotic (and more compiler dependent), but:
>
Not really, no. Apart from a few hidden bits such as pointers to handle virtual methods and virtual inheritance, the data fields are ordered, padded and aligned just like in C structs. And these hidden pointers follow the same rules as any other pointer.
>
The only other special bit is empty base class optimisation, and that's pretty simple too.
>
>
For simple cases, they may match up, like a POD class may look just like an equivalent struct, or single-inheritance classes with virtual methods like a struct with a vtable, etc... But in more complex cases there may be compiler differences (along with differences in things like name mangling, etc).
I've never seen or header of a case where there there is anything unexpected here.
Sure, different C++ implementations or ABIs might have different details around these hidden pointers and the way they organise their vtables. But they are still hidden /pointers/, and these are aligned and padded like any other pointer. Even if the hidden data contained a bunch of extra bits, flags, etc., to handle complicated inheritance setups, these would still be padded and aligned like any other structs with bits, flags, etc.
OK.
I had thought you were implying that if one took two arbitrary C++ compilers (with different ABIs), with the same class definitions, that they would always end up with the same in-memory layout.
This is not my experience though, say:
The specifics of how multiple inheritance and virtual inheritance are handled;
How many entries are reserved at the start of the VTable;
What information (about any) is represented for a class in the case of RTTI, and how this information is represented in memory;
...
Though, things do seem to be more standardized in the "GCC adjacent" ecosystem, as GCC uses the same basic name mangling scheme and rules as given in the IA64 ABI, and many others follow with this.
BGBCC was partly influenced by this, but had essentially hybridized it with the scheme used in the JVM and JNI.
And, it tends to aggregate interfaces at the end of the object, rather than at the start of the object as with multiple base-classes in typical C++ ABIs.
Though, almost may as well put interface vtable pointers before data members, but practically it wouldn't matter that much.
Though, IIRC, JVM would differ some in that it would aggregate all of the interfaces for the inheritance tree at the end of the object instance, rather than treating them more like members. Though, seems more practical for a static compiler to treat them more like members, since this means casting a class to an interface can be done in a single operation.
Though, formally, JNI did keep a lot of this stuff hidden (so C code accessing objects via the JNI vtable didn't need to know about object layout specifics).
>
Though, unlike with structs, programs seem less inclined to rely on the memory layout specifics of class instances.
>
Of course they shouldn't be relying on such details!
It could be relevant for things like "reflective object serialization", which AFAIK still isn't really a thing in (portable) C++.
Well, in the sense of, say, taking an arbitrary class and then asking the language runtime to give back the object's contents in the form of a blob of XML or similar (or, some other less wasteful format). Or, dumping a graph of objects to a file and reloading them later.
Is more of a thing though in some other languages.
For C and C++, usual process to serialize objects has been a bit more manual. Granted, unlike, say, Python, these languages tend not to have typesystems where the entire typesystem could be handled by the runtime library (would break about as soon as it hits an untagged pointer for a type that can't be entirely understood based on compiler generated metadata).
Likely only way to handle this would be to limit the parts of the typesystem that could be used for this (using sort of a hybrid system). Say, for example, if it requires using specific container objects which know how to serialize themselves, with RTTI and/or type-tagging being used in other cases.
Doing this in the runtime would also require a runtime-provided DOM or some other API capable of representing the data (assuming it is not using opaque binary-blobs or an ASCII string serialization).
But, I guess, sadly as much as XML DOM kinda sucks, there isn't really a "clearly better" alternative (while JSON and JS-like objects are popular, there isn't a good way to map things 1:1 for a generic serializer, and if each serialized objects ends up as an object with an array of other objects to express each member, this doesn't really save much over a DOM API).
Though, one can design the DOM API in a way to conserve memory and CPU time (vs a more traditional design):
Use integer tags internally rather than ASCII strings;
Represent integer and floating-point values as numeric values (not strings);
Represent very short strings inline, and moderately short strings via interning;
Use UTF-8 strings rather than UTF-16 (mostly to save memory);
...
Though, this did lead (in my case) to a slightly non-standard XML variant where there is some extra metadata to the parser encoded in the way attributes are quoted:
Double quotes indicating the value should always be understood as a string;
Single quotes may use a relaxed interpretation if holding a numerical value, interpreting the value as a number.
Say:
<foo x='123' y='456'/>
My parsers would understand x and y as being numeric fields.
Most other XML printers always use double-quotes (single quotes are more rare), so making single quotes special limits potential ambiguity. Parser will still interpret it as a string if it is not a well-formed number though.
This is more relevant to floating-point numbers though, as "1.0" and "1.000000" are equivalent as far as the number parser is concerned, but non-equivalent if understood as strings. Well, or things like "0xBC614E" vs "12345678".
...
In other news:
Did write out another random idea:
https://github.com/cr88192/bgbtech_btsr1arch/wiki/(Misc-Idea)-BSR4I
Essentially for an idea to try to do a "hybrid ISA":
Could function either as a self-contained ISA partway between XG2 and RISC-V;
Or, possibly combined with RISC-V (at the expense of 16-bit instructions) in a "Co-Execute" mode, where the CoEx mode could use whichever encoding was "better" (at the expense of both 16-bit ops and predication).
There would need to be a mechanism in the ISA to select between these modes though (probably a "magic branch" scheme different from the one used for Inter-ISA branches).
This would likely include an RV64 encoding for "Branch to/from CoEx", and an encoding within this ISA to jump between CoEx and "Native" mode.
Magic branches make sense mostly as any such mode switch is going to require a pipeline flush.
This is assuming an implementation that would want to be able to support both this ISA and also RV64GC.
One possibility could be (in native RV notation):
RV64 (Branches if supported, NOP if not):
LBU X0, Xs, Disp12s //Dest=RV64GC
LWU X0, Xs, Disp12s //Dest=CoEx
LHU X0, Xs, Disp12s //Dest=Native
New ISA:
LBU X0, Xs, Disp10s //Dest=RV64GC
LWU X0, Xs, Disp10s //Dest=CoEx
LHU X0, Xs, Disp10s //Dest=Native
Which will have the behavior of branching if the operation is supported (similar to a JALR), or behave as a NOP otherwise (so program can gracefully fall back to a purely RISC-V implementation of a function).
One other possibility being LW or LD with X0 as a destination, but IIRC these may also be used as cache prefetch hint instructions (so, should not trigger a branch).
...