X128: A Modest Proposal

I just got done reading today's cppfront update while some recent discussions about ABI were still on my mind, and that resulted in the following idea:

Why not define a new platform for which no existing software exists, but to which existing software could be easily ported? This would provide the freedom to make many desirable changes without worrying about breaking anything.

The ideal testbed for this experiment would be on Linux, because of the availability of source code ease of spinning up a "minimum viable operating system", consisting solely of a kernel image with an embedded initramfs which runs busybox. That's all you need to boot to a command line.

Linux also already supports multiple ABIs. The current reigning monarch of the desktop platforms is amd64, which is defined as, "the System V C ABI and the Itanium C++ ABI running on the LP64 instruction set".

A less well known but still supported alternative is X32 which is defined as, "amd46, except sizeof(void*) == 4".

So the minimum change necessary to define a new platform us to just tweak any of the implementation defined knobs in the C or C++ standards and now you have an opportunity to create new standard libraries which of course still must remain API compatible with their respective standards, but for which all potential QOI changes are on the table.

The smallest non-trivial change I can think of is the inspiration for the post title: X128 is defined as, "amd64, except sizeof(long) == 8 and sizeof(long long) == 16".

That change wasn't made 20 years ago when the System V ABI for AMD64 was defined because we were just starting the 32 bit to 64 bit transition and they wanted to preserve the ability to run software compiled for 32 bit. For the purposes of this thought experiment, however, requiring all software to be recompiled before it can run on the new platform is the explicit goal.

Realizing a proof of concept for this new platform would require porting four pieces of software: a C compiler and standard library, the kernel, and busybox.

After that the doors open to make any QOI changes to the C++ standard library (or C standard library for that matter) one desires. I'd suggest adding type erasure to std::regex and putting all standard library symbols in an inline version namespace, but that's just the most obvious low hanging fruit. Perhaps multithreaded stack unwinding to reduce the cost of exceptions would be another thing to consider.

Posted Sep 29, 2023, 1:28 PM UTC · edited

434
Top_Satisfaction6517Bulat•

for that?

1
Antervis•

microsoft will never stand for that, and neither will apple

1
ABlockInTheChainOP•

Why not? There is nothing that would prevent them from supporting X128 someday and in any case there's no obvious action they could take against such an initiative other than by not participating.

This would actually be a great opportunity to work with Microsoft at least to see about finding a common set of calling conventions that everybody would be happy with.

Sure there would be some customers on all platforms who could never migrate to X128 since they want to keep running binaries that were compiled in the 1990s, however it's not like amd64 would go away immediately and even after everybody else migrated those users could keep it alive for as long as they need it.

1
Antervis•

microsoft has a cast history of ignoring or opposing industry-wide standards. As of now, you'd have to break a truckton of patents to emulate windows environment.

1
ABlockInTheChainOP•

I guess I considered it obvious that only Microsoft could ever port WIndows to X128.

Linux and BSD could adopt X128 without any issue though and that would migrate a majority of the mobile and server market share. Apple could migrate or not.

Just getting Linux on a new ABI with the possible improvements that enables probably would have kept Google in C++ instead of rage quitting since I'm sure most of their backends run on Linux.

1
Antervis•

try as they might, there's no way google can abandon c++. They invented Go simply to pay less for development of trivial workloads.

1

Microsoft already sort of supports mixing ABIs in a way. For example, a 64-bit executable running on Windows 11 can load both Aarch64 and AMD64 DLLs and you can end up with a callstack in which you cross this boundary multiple times. Even better: you can have a single DLL that can act as an AMD64 DLL for AMD64 processes, and as an Aarch64 one for arm processes.

The machinery for switching from one architecture to the other is already in place.

Arguably it was in place even before Windows on ARM was a thing via the WoW64 mechanism.

I wonder if something like this can allow for gradual migration to the new ABI.

The first thing that is a huge problem that comes to mind is constructing one object in one "realm" and passing it to the other, but similar translations already happen in the WoW64 layer when a 32-bit syscall is emulated. Granted, all the types used in this case are PODs so the translation is more or less trivial.

The second problem is that the AMD64 emulation that happens on ARM is already sort of magical and I can't even imagine the effort it required, so is something like this worth it just to support two different C++ ABIs?

1
Antervis•

what you are talking about is in another dimension entirely

1

Is it tho? Well, yes, lol.

But an argument against an ABI break is that people might depend on libraries built 10 years ago and never updated.

If those libraries are dynamically linked, a way of mixing the two ABIs across DLL boundaries could allow for a partial adoption of the new ABI.

1
Antervis•

what you were talking about is not "mixing the two ABIs across DLL boundaries", but just libraries built to run on either x64 or ARM architectures. Same as apple's universal binaries. It's little different from gluing together libraries built for two different architectures into a single file. ARM system would only use ARM part of the binary, x64 system - x64 part, nothing "crosses boundaries", there's no "mixing ABIs" or whatever else your mind came up with.

1
no-sig-available•

People who a prepared to move to a new platform are not the ones that care for ABI stability. So you would be preaching for the choire!

The problem - if it is one - is that some don't want to break with their existing platform. And they are definitely not going to move to a 128 bit long long - just imaging what this would do to their intmax_t parameters... :-)

If you want to make a revolution, just produce a "C+++" compiler and standard library for an existing platform, show how amazing that is, and then propose all those changes for C++32.

1
ABlockInTheChainOP•

People who are prepared to move to a new platform are not the ones that care for ABI stability. So you would be preaching for the choire!

The point is they wouldn't have to, because amd64 would still exist, but those that can migrate would get something worth migrating for.

128 bit ints in the language are a feature that would make some users very happy, and at least wouldn't get in the way of users who don't need them.

1
Tringi•

128 bit ints

There are countless of libraries for that. I have mine that can even be nested: https://github.com/tringi/doubleinteger

1
ABlockInTheChainOP•

That is neat. If it wasn't for this I'd use it over boost::multiprecision:

// - notes: - use of types with different width for LO and HI is unsupported

The one place I need a large int type it uses 320 bits.

1
Tringi•

Hah. It didn't even occur to me that people would need types that are not power of 2 in size.

I wish I had time to investigate. Would be an interesting exercise in templates to do it properly. But I could revisit why the current version requires sizeof (LO) == sizeof (HI)...

1
ChadJeeptey•

You're missing the point. People don't want an ABI break, they want a bunch of other changes that necessitate it. No one is interested in it in isolation because, as you suggest, anyone can do it at any time already.

1
ABlockInTheChainOP•

People don't want an ABI break, they want a bunch of other changes that necessitate it.

What I have suggested is making an ABI break that is not mandatory but which can be used by anyone who can tolerate one, and which gives them all the changes that can be made while preserving the API.

Maybe I should have been clearer, but the reason I mentioned version standard library symbols is because that's a change which could be made to potentially make future API changes less expensive.

Right now nothing is changing because the cost of moving from where we are to where we want to be in a single step is too high. Give that is the case, why not make a smaller change which gets us closer to the goal than we are now but is less expensive to achieve?

1
YARandomGuy777•

I like the proposal but I have no idea how could anyone propagate such changes from idea to metal.

1
TheThiefMasterC++latest fanatic (and game dev)•

The big change that C and C++ need is a 64-bit "int" on 64-bit platforms. This would necessitate adding "short short" or making int16_t into a distinct type not matched in the traditional types.

The reason for doing this? Both C and C++ promote to "int" by default when performing calculations, and there are a lot of bugs caused by accidentally using only 32-bit.

Historically, "int" was the register size - 16 or 32 bits. But that stopped with x64 and a lot of code gets weird behaviour as a result!

1
ABlockInTheChainOP•

This would necessitate adding "short short" or making int16_t into a distinct type not matched in the traditional types.

The only reason I didn't say that in the original post is exactly because we would need a new fundamental type like "short short".

Perhaps that is fine as a compiler extension that could eventually be standardized though.

If you add short short and bump int up to 64 bit then long should be 128 bit and long long should be 256 bit.

There's a lot of crypto code that operates on 256 bit integers so those users would be happy to have a native type in the language they could use without involving a third party library.

1
almost_useless•

Sounds like a terrible idea to make new meaning to old types.

We need more types with fixed size. Like int128_t and int256_t.

If you need types with platform depending size you can create alias for them yourself.

1
TomDuhamel•

The big change that C and C++ need is a 64-bit "int" on 64-bit platforms.

Neither C or C++ makes any kind of decision in this regard. They both describe an int to be the size of a word, which size is dictated by the platform i.e. the operating system. As it turns out, the majority of OSes (including Windows and most Linux flavours) opted for a 32 bit word memory model, because 98% of the engineers out there understand that 64 bit words would be absolutely wasteful. There aren't that many cases in the real world where more than 32 bits are required. Honestly, the vast majority of my ints would fit 8 bits just fine.

1
no-sig-available•

There aren't that many cases in the real world where more than 32 bits are required.

AMD found that when extending Intel 32-bit to AMD 64-bit. By keeping the 32-bit instructions like they were, and add a size prefix for 64-bit ops, they got the instruction stream as compact as they could. Making 64-bit the default, and add an override for 32-bit ops, made the code larger (and slower).

1
Tringi•

Yes, instruction cache pressure performance hit is a very real thing.

1
TheThiefMasterC++latest fanatic (and game dev)•

The standard doesn't prescribe it, but it doesn't properly support it either. It doesn't supply enough fundamental integer types to have char at 1 byte, int at 8 bytes, and two types in between at the 2 bytes and 4 byte sizes. This was mostly trouble because when the C x64 ABI was being standardised C and C++ didn't have the fixed width integer types like int16_t yet either.

"int" has the special behaviour of being the type that all integer constants are by default, and all arithmetic operations promote to. It's also people's go-to for loop indexes, which means (for example) indexed loops up to the size() of a vector silently breaks on 64-bit platforms if the vector gets too large (whereas on a 32-bit platform, it's impossible to make a vector that large in the first place).

Another fun one is when attempting to mask a bit out of a 64-bit number:

uint64_t get_flag(uint64_t val, int index)
{
    return val & (1<<i);
}

This silently fails with i over 31 because 1<<i is only an "int". It's even better if you make a template of this kind of function and it only breaks on the 64-bit types, because every other version promotes up to int which is only 32-bits.

1

I think this might work well with RISC-V

1
DanielMcLaury•

Okay, now suppose that I need to call functions in a library that's distributed in compiled form.

1
ABlockInTheChainOP•

As long as the library was compiled for x128 you can call those functions.

If it wasn't then you need to stick to x86.

Nobody asks, "how to I call my x86 compiled libraries from a risc-v app"? because everyone understands that's nonsense.

If you have compiled libraries you need to use that are only ported to x86 then you stay on x86 and either don't support risc-v or else do something completely different.

1
pjmlp•

Not at all, that is why formats like ARM64EC got invented.

1