Listed at the end of this section are the in-flight WG21 papers this proposal is dependent upon, and which would need to enter the standard before this library can be considered. However a bigger issue involves the potential changes to the C++ object model, plus new contracts with which to indicate the limited side effects of i/o functions in order to enable improved optimisation.
Firstly I wish to make it clear that there is the viable option of simply making no changes at all to the object model, and any placement of objects into mapped memory is declared to be undefined behaviour, as it is at present5 .
However I also think that an opportunity missed. Via memory maps, the proposed low level file i/o library herein enables object lifetime to exceed that of the program. It also enables an object to appear in multiple instances of concurrent C++ program execution, or indeed at multiple addresses within a C++ program. And finally one can mark individual pages of memory with different access permissions than read-only or read-write (e.g. no access i.e. unreachable), or kick individual pages out to swap, or throw away their contents. In other words, individual pages of memory can become unreachable with the objects within that storage still being alive.
All four things are problematic for the current C++ standard’s object model, not least that objects cannot currently outlive the program; that there is no awareness of it being possible for multiple concurrent C++ program instances to exist in the current standard; that objects not marked with [[no_unique_address]] and which are not bits in a bitfield must currently always have a single, unique address in the process; that all alive objects are equally reachable from anywhere in the program; and finally that objects are always available at their unique address, and are not also elsewhere.
Furthermore, there are reordering constraints peculiar to mapped memory if a modification to an object is to persist at all, or indeed in a form which is usable. This is because in most implementations of C++, atomic reordering constraints only affect apparent ordering to CPU cores within the same symmetric multiprocessing cluster, and may have no effect on the actual ordering of when modifications are sent to main memory and/or the storage device. Thus, if a C++ program modifies an object, and then modifies it again, some of the changes of the latter modification may land in main memory before some of the changes of the earlier modification, irrespective of any atomics or fences or anything else which a C++ program currently can legally do. This has obvious implications for the integrity of that object across a program restart.
I therefore suggest some changes to the C++ object model, listed below.
There are presently four kinds of storage duration. As [basic.stc] describes it:
The storage duration is the property of an object that defines the minimum potential lifetime of the storage containing the object. The storage duration is determined by the construct used to create the object and is one of the following:
[basic.life] describes in some detail the lifetime model, and it essentially reduces down to one of these possible states for each object (and apologies for the over-simplification for purposes of terse exposition):
You will note that due to their triviality, memory storing trivial types such as std::byte can either be unconstructed, or fully constructed, and no lifetime state exists apart from those two. It is important to understand that currently speaking, using allocated memory in which nothing has been constructed is undefined behaviour, so the common method of reinterpret casting the return from malloc() to a pointer of the type you want is presently undefined behaviour.
This is being dealt with in [P0593] Implicit creation of objects for low-level object manipulation. Amongst its many recommendations, it makes unconstructed memory unreachable by default. You can make it reachable by (i) constructing objects into it (ii) via the proposed std::bless() which tells the compiler that a region of unreachable memory (i.e. unconstructed) is now reachable (and contains alive trivial objects of some form). You can restore it to being unreachable by calling the destructor of the objects, even if those objects are completely trivial. P0593 doesn’t mention it explicitly (yet!), but given std::launder’s special casing for std::byte, it seems likely that calling the destructor for std::byte on a region with unknown trivially destructible types of objects in it will always mark it unreachable. And finally, P0593 names various functions as always returning memory which is reachable i.e. containing an array of alive objects of some unspecified trivial type.
This new concept of reachable vs unreachable memory is an important one, and it is leaned upon heavily in the proposed extensions.
As always, naming is very hard, but mapped storage duration seems a reasonable name for memory which can be shared between concurrently running processes, or between multiple start-stop cycles of the same program (note that a program using an object from mapped storage which it itself did not store there is undefined behaviour).
The storage for objects with mapped storage duration shall last for the duration of the filesystem entity (see TS definitions below for precise meaning) referred to by the section_handle instance which represents the storage. section_handle instances may refer to filesystem entities whose lifetimes automatically end when the program ends (‘anonymous inodes’), or which may persist for an indeterminate period, including across subsequent executions of the C++ program.
Mapped storage has the most similarity to dynamic storage, but with the following differences:
The storage represented by a section_handle instance can be mapped into (i.e. made available to) a C++ program by creating a map_handle sourcing the storage from a section_handle instance. The storage mapped by the low level map_handle shall represent unconstructed and unreachable memory, and will require the use of std::bless() or map_view to make it reachable (alternatively, use the convenience class mapped on a section_handle instance which bundles the aforementioned low level operations on your behalf).
Destroying a map_view does not make the storage unreachable. Decommitting individual pages does, as does destroying a mapped or map_handle.
I propose adding a new separate lifetime status for objects: unreachable. Under P0593, unconstructed objects are unreachable, but there is no possibility for a partially constructed, alive or partially destructed object to be unreachable. I propose that this new status ought to be added such that objects can now have the following lifetime states:
P0593 proposed these functions to mark regions as reachable:
// Requires: [start, (char*)start + length) denotes a region of allocated
// storage that is a subset of the region of storage reachable through start.
// Effects: implicitly creates objects within the denoted region.
void std::bless(void *start, size_t length);
// Effects: create an object of implicit lifetype type T in the storage
// pointed to by T, while preserving the object representation.
template<typename T> T *std::bless(void *p);Thus the obvious corrollary for marking regions as unreachable:
// Requires: [start, (char*)start + length) denotes a region of allocated
// storage that is a subset of the region of storage reachable through start.
// Effects: implicitly uncreates objects within the denoted region.
void std::unbless(void *start, size_t length);
// Effects: uncreate an object of implicit lifetype type T in the storage
// pointed to by T, while preserving the object representation.
template<typename T> void *std::unbless(T *p);
A gain of this new unreachable lifetime status is that it neatly solves the problem of objects appearing at multiple addresses in a running program. We can now say that only one of those addresses can be reachable for an alive object at a time. If a program wishes to change an object’s current reachable address, they unbless the old location, and bless the new location.
It should be emphasised that similarly to blessing, unblessing of trivial types causes no code emission. It simply tells the compiler what is now unreachable.
Finally, it may seem that unreachability is a bit of a big sledgehammer to throw at this problem. However, there are a number of other problems elsewhere in C++ where having unreachable but alive objects would be very useful – thread local storage on a million CPU core compute resource is an excellent example6 . That said, this is a big change to lifetime, and if strong arguments are made against it then I am happy to propose something more conservative.
Currently P0593 makes no mention of polymorphic objects i.e. ones with vptrs in many implementations. I can see lots of reasons why it would be useful to bless and unbless polymorphic objects i.e. update the vptr to the correct one for the currently running program, or clear the vptr to the system null pointer.
Imagine, for example, a polymorphic object with mapped storage duration whose constructing program instance terminates. Upon restart of the program, the storage is mapped back into the program, but now the polymorphic object has the wrong vptr! So we need to write the correct vptr for that object, after which all works as expected7 .
This also applies to shared mapped memory. Extending the C++ object model to handle memory being modifiable by concurrently running C++ programs would be very hard, so I propose that we don’t do that. Rather we say that objects in mapped storage can only be reachable in exactly one or none running C++ programs at a time i.e. at exactly one or no address at any one time, otherwise it is undefined behaviour. Therefore, if process A wants to make available an object to process B, it unblesses it and tells process B that the object is now available for use. Process B blesses the object into reachable, and uses it. When done, it unblesses it once more. Now process A can bless it and use it again, if so desired.
Implied in this is that blessing and unblessing becomes able to rewrite the vptr (or whatever the C++ implementation uses to implement polymorphism). To be truthful, I am torn on this. On the one hand, if you wish to be able to bless and unbless polymorphic objects, rewriting the vptr is unavoidable, and the frequency of relocating polymorphic objects in current C++ is low. On the other hand, future C++’s may do a lot more remapping of memory during large array expansion in order to skip doing a memory copy, and in this circumstance every single polymorphic object in a few million item array would need their vptr’s needlessly rewritten, which is unnecessary work.
I’m going to err on the side of caution, and propose that the polymorphic editions of bless and unbless shall be:
// Effects: create an object of implicit lifetype type T in the storage
// pointed to by T, while preserving the object representation
// apart from any necessary changes to make any polymorphic
// objects within and including T ready for use.
template<typename T> T *std::revive(void *p);
// Effects: uncreate an object of implicit lifetype type T in the storage
// pointed to by T, while preserving the object representation
// apart from any necessary changes to make any polymorphic
// functions within the objects within and including T unusable.
template<typename T> void *std::stun(T *p);Note that stun() and revive() not just change the vptrs of the type you pass them, but also any vptrs of any nested types within that type. They also perform blessing and unblessing, so you do not have to do that separately. Calling these functions on non-polymorphic types is permitted.
It is highly important for the future efficiency of any iostreams v2 that the low level i/o functions do not cause the compiler to dump and reload all state for every i/o, as they must do at present. The i/o functions proposed do not modify their handle on most platforms, and on those platforms we can guarantee to the compiler that there are either no side effects or no visible side effects except for those objects modified by parameters.
Reusing bless() and unbless(), I propose the following contracts syntax for the io_handle functions so we can tell the compiler what side effects the i/o functions have:
constexpr! void ensure_blessed(buffers_type buffers)
{
for(auto &buffer : buffers)
{
bless(buffer.data(), buffer.size());
}
}
// This function has no side effects visible to the caller except
// for the objects it creates in the scatter buffer list.
virtual buffers_type io_handle::read(io_request<buffers_type> reqs,
deadline d = deadline()) throws(file_io_error)
[[no_side_effects]]
[[ensures: ensure_blessed(reqs.buffers)]]
[[ensures: ensure_blessed(return)]];The key thing to note here is if the caller never accesses any of the scatter buffers returned by the function, the read can be optimised out entirely due to the [[no_side_effects]] attribute. Obviously the compiler also does not dump and reload any state around this scatter read apart from buffers supplied and returned, despite it calling a kernel system call, because we are giving a hard guarantee to the compiler that it is safe to assume no side effects apart from modifying the scatter buffers.
Gather write is less exciting, but straightforward:
virtual const_buffers_type io_handle::write(io_request<const_buffers_type> reqs,
deadline d = deadline())
throws(file_io_error) [[no_visible_side_effects]];
Here we are telling the compiler that there are side effects, just not ones visible to the caller. Hence the compiler cannot optimise out calling this function, but it can skip dumping and reloading state despite the kernel system call made by the function.
C and C++ allow the programmer to constrain memory read and write operations, specifically to constrain the extent of reordering that the compiler and CPU are permitted to do. One can use atomics with a memory order specified, or one can call a fence function which applies a memory reordering constraint for the current thread of execution either at just the compiler level, or also at the CPU level whereby the CPU is told what ordering its symmetric multiprocessing cores needs to manifest in visible side effects.
File i/o is no different: concurrent users of the same file or directory or file system will experience races unless they take special measures to coordinate amongst themselves.
Given that I have proposed above that C++ standardises mapped storage duration into the language itself, it might seem self evident that one would also need to propose a standardised mechanism for indicating reordering constraints on i/o, which are separate to those for threads. My current advice is that we should not do this – yet.
The first reason why is that persistent memory is just around the corner, and I think it would be unwise to standardise without the industry gaining plenty of empirical experience first. So kick that decision down a few standard releases.
Secondly, the proposed library herein does expose whatever acquire-release semantics is implemented by the host operating system for file i/o, plus the advisory locking infrastructure implemented by the host, plus the ability to enable write-through caching semantics. It also provides barrier(), though one should not write code which relies upon it working, as it frequently does not.
Note that if the file is opened in non-volatile RAM storage mode, barrier() calls the appropriate architecture-specific assembler instructions to correctly barrier writes to main memory, so in this sense there is library, if not language, support for persistent memory. The obvious sticker is that barrier() is a virtual function with significant preamble and epilogue, so for flushing a single cache line it is extremely inefficient relative to direct language support for this.
Nevertheless, one can write standards conforming code with the proposed library which performs better on persistent memory than on traditional storage, and my advice is that this is good enough for the next few years.
--
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposal...@isocpp.org.
To post to this group, send email to std-pr...@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/b214ce98-2aaa-4f7f-a04a-77687422111a%40isocpp.org.
Though the actual way of recognizing this work would be currently reading the proposal, and I will do my best during the weekend, I will do something that I think is not very common these days: to say an anonymous "thank you" for the (personal) time you took to contribute to the C++.Same for all the people putting effort, time and/or money in the progress of the language.
P0593 proposed these functions to mark regions as reachable:
So assuming people were put off by the length of the paper
This is being dealt with in [P0593] Implicit creation of objects for low-level object manipulation. Amongst its many recommendations, it makes unconstructed memory unreachable by default. You can make it reachable by (i) constructing objects into it (ii) via the proposed std::bless() which tells the compiler that a region of unreachable memory (i.e. unconstructed) is now reachable (and contains alive trivial objects of some form).
Mapped storage has the most similarity to dynamic storage, but with the following differences:
- It has allocation and alignment granularities with architecture specific coarse sizes (‘memory page’). 4Kb/2Mb/1Gb page sizes are common.
- New allocations are guaranteed to be all bits zero on creation.
- It has map on first read semantics. This means that the first read from a memory page can take hundreds of CPU cycles, and a TLB shootdown causes an interrupt for other CPUs.
- It has allocate on first write semantics. This means that the first write to a memory page can take thousands or even hundreds of thousands of CPU cycles, plus a TLB shootdown.
- Usually, but not always, mapped storage is a memory cache of equivalent storage a high latency storage device. Hence they can be individually pushed to storage, their contents thrown away, deallocated, given different access permission or caching strategies, and lots of other interesting (i.e. potentially game changing for large STL containers) operations. Individual pages can be:
- Read-only, read-write, or copy-on-write. These are self describing, and these are hard characteristics: violating them means program failure.
- Committed or uncommitted. This indicates whether the page counts towards the resources used by the C++ program. Uncommitted memory is inaccessible, and acts as a placeholder for later use (i.e. it is reserved address space, useful for expanding large arrays without content copying).
- Dirty or clean. This indicates whether the page contains data not yet mirrored onto its backing storage.
- Allocated or unallocated. This indicates whether storage backing the page has been allocated on the storage device. If not, the first write to a clean page may be very expensive as the page may need to be copied by the kernel and/or space allocated for it on the backing storage device.
The storage represented by a section_handle instance can be mapped into (i.e. made available to) a C++ program by creating a map_handle sourcing the storage from a section_handle instance. The storage mapped by the low level map_handle shall represent unconstructed and unreachable memory, and will require the use of std::bless() or map_view to make it reachable (alternatively, use the convenience class mapped on a section_handle instance which bundles the aforementioned low level operations on your behalf).
I propose adding a new separate lifetime status for objects: unreachable. Under P0593, unconstructed objects are unreachable, but there is no possibility for a partially constructed, alive or partially destructed object to be unreachable. I propose that this new status ought to be added [...]
P0593 proposed these functions to mark regions as reachable:
// Requires: [start, (char*)start + length) denotes a region of allocated
// storage that is a subset of the region of storage reachable through start.
// Effects: implicitly creates objects within the denoted region.
void std::bless(void *start, size_t length);
// Effects: create an object of implicit lifetype type T in the storage
// pointed to by T, while preserving the object representation.
template<typename T> T *std::bless(void *p);
Thus the obvious corrollary for marking regions as unreachable:
// Requires: [start, (char*)start + length) denotes a region of allocated
// storage that is a subset of the region of storage reachable through start.
// Effects: implicitly uncreates objects within the denoted region.
void std::unbless(void *start, size_t length);
// Effects: uncreate an object of implicit lifetype type T in the storage
// pointed to by T, while preserving the object representation.
template<typename T> void *std::unbless(T *p);
It should be emphasised that similarly to blessing, unblessing of trivial types causes no code emission. It simply tells the compiler what is now unreachable.
I’m going to err on the side of caution, and propose that the polymorphic editions of bless and unbless shall be:
// Effects: create an object of implicit lifetype type T in the storage
// pointed to by T, while preserving the object representation
// apart from any necessary changes to make any polymorphic
// objects within and including T ready for use.
template<typename T> T *std::revive(void *p);
// Effects: uncreate an object of implicit lifetype type T in the storage
// pointed to by T, while preserving the object representation
// apart from any necessary changes to make any polymorphic
// functions within the objects within and including T unusable.
template<typename T> void *std::stun(T *p);
For example, if you want to expand from Implicit Lifetime types to Trivially Relocatable types, then the object model need not be changed (save for the changes needed to make Trivially Relocatable work). You can still define it as destroying the old object and manifesting a new one.However, because such types are more than just their values, this is of limited value. `unique_ptr<T>` is the quintessential Trivially Relocatable example. But would it be a good thing to stick in a memory mapped object? I don't think so. If the object it points to is not in the same memory map, then it is not reasonable to persist the object across executions, since that object may well be long dead.And if `unique_ptr<T>` does point to an object in the same map, then its address will have to be updated before you can actually use it. That means you can't just cast and go; you have to do some work between mapping the memory and using it. Given that... what's the point of storing it in a memory map directly at all? Because it's part of some other object you do want to store there?
Somethign similar is true of other popular Trivially Relocatable types. `vector`, `fstream`, `any`, etc, all of them could be stored there, but the utility of doing so is... dubious.
Your proposal suggests allowing polymorphic types. Now with these, the destroy/recreate trick becomes tricky. [...]
So you have this operation you want to be able to do. You want to take objects in one address, make them disappear from that address, and then make them appear in a (potentially) different address. And you want to change the C++ object model to make this work.Here's a question: do you need to change the object model in order to get this effect?
If we limit our usage of mappable types to P0593's implicit lifetime types, then I don't think we to affect the object model at all. Implicit lifetime types are objects which are nothing more than a set of bits, where their value representation is their logical functionality. Two objects of such types with the same value representation have the same value and are distinct only in that they have different addresses.With such a limitation in place, you could define the mapped memory operation as a funny sort of memcpy. It doesn't move the object. Unmapping the memory range terminates the lifetime of all objects in that memory. The values are preserved, but not the objects. You then map the memory at potentially new addresses.Now, you still need a way to create new objects in those addresses with their value representations intact (see below for why non-template `bless` can't do this). But such a feature does not require substantial changes to the object model. It's just a new way to create an object.
To me, the principle advantage of significant changes to the object model here would be to allow a larger variety of objects to work. So now we have the next question: exactly which types of objects are you trying to support? What is the minimum set of features you need to make this worthwhile? That will help you define what changes you need to the object model, if any.
However, because such types are more than just their values, this is of limited value. `unique_ptr<T>` is the quintessential Trivially Relocatable example. But would it be a good thing to stick in a memory mapped object? I don't think so. If the object it points to is not in the same memory map, then it is not reasonable to persist the object across executions, since that object may well be long dead.
And if `unique_ptr<T>` does point to an object in the same map, then its address will have to be updated before you can actually use it. That means you can't just cast and go; you have to do some work between mapping the memory and using it. Given that... what's the point of storing it in a memory map directly at all? Because it's part of some other object you do want to store there?
Your proposal suggests allowing polymorphic types. Now with these, the destroy/recreate trick becomes tricky. The manifesting operation is based on the value being undisturbed. But you need the value to be disturbed, since the location of the vtable pointer may need to be fixed up.Which brings me to my own personal experience. See, I've been a part of a team that actually implemented a "memcpy"-style serialization system that could handle polymorphic types. This system required two phases: serialization and deserialization.Now admittedly, our system did the serialization part through copying (the host architecture and the destination architectures were not the same. And I mean not the same; in one remarkable case, the compiler for the deserialization part used a different order for the base classes than the serialization compiler). But part of what it did was to put something special in the vtable pointer slot: an index that represented which type it was. The deserialization code didn't copy anything; it just fixed up vtable pointers in-situ by reading the type index.This is important because one of the things this allowed us to do was to deserialize without knowing at compile time what each type was. We didn't have to know the dynamic type from outside of the system. We were able to use the base class type, and the system would know how that the pointer actually pointed to a more derived class and therefore adjust the vtable accordingly.That's really important, since most polymorphic type usage is based on base class types. If you have a polymorphic object in mapped memory, the code that wants to retrieve the address probably doesn't know what its dynamic type is; it knows the base class type it wants.And that's where things get tricky. Because now, we're not talking about some kind of memory barrier. You're talking about having to perform a process on each object in a region of memory before the unmap process can be considered complete. Then you have to perform a counter-process on each object afterwards to fully map it again. And what's worse is that C++... makes this impossible to implement generally.
We could do it in our system only because our system had a limited, fixed set of types to work with. C++ does not. Loading DLL/SOs adds more type indices. And while type indices will be the same for an execution of the code, it will be able to change between executions. But you need to be able to store a value in the mapped memory that contains something that is persistent across executions.C++ doesn't have a type identifier like that. So the only way to make this work is if you know the dynamic type of the object when you unpack it. And as someone who has used persistence systems of this sort, you generally don't know. And you generally don't want to know; if you wanted to know, you wouldn't be using a dynamic type ;)
So I don't think that polymorphic type support would be useful if it were done in an implementable fashion. However, even if it was implementable, it could still work based on having an explicit location in code where the lifetime of the old object ends (packing the polymorphic object; replacing the vtable pointer with an index) and the lifetime of the new begins (unpacking it; replacing the index with a valid vtable pointer). Obviously, we would need to create functions to explicitly do so on a per-object basis (as your proposal outlines), but this wouldn't need to affect the object model. Your "stun" function could be said to terminate the object's lifetime, while your "revive" function creates it.
I think "Mapped storage duration" is too on-point in terms of where the memory comes from, rather than how C++ thinks of it.The four storage durations are based on how C++ interacts with them. Automatic storage duration objects have storage durations determined entirely by their objects' scopes. Static storage duration objects have their storage duration be the lifetime of the program. Thread-local storage duration objects have their storage duration be the lifetime of the thread. And dynamic duration objects have their storage duration determined dynamically: by direct manipulation of the code.
But I think there is a deeper misunderstanding here. You are confusing properties of "storage" with the properties of "storage duration". The former is a thing that an object needs to have in order to exist. The latter is a property of an object, determined by the means used to construct it, and it's purpose is to govern the (default) lifetime of that object.For example, you can create an automatic variable with automatic storage duration. You can then reuse that variable's storage to create objects with dynamic storage duration. By reusing it, you will have ended the lifetime of the automatic variable. You would then have an object with dynamic storage duration whose storage happens to be on the stack.Which is why "stack storage duration" would be a misnomer, much like "mapped storage duration".
Of course, C++ has a bunch of rules about what happens when you do this and then try to use that variable's name later on. As well as what happens when that variable goes out of scope, and what you need to do to make leaving scope not invoke UB (namely, create an object of that type back in that storage).What you seem to want is simply a wider form of dynamic storage duration. You're not changing the meaning of the storage duration; you're simply adding ways to create/destroy such objects.
On Bless:P0593 proposed these functions to mark regions as reachable:Your notion of "reachability" has nothing to do with what P0593 is doing. `bless` does not make objects "reachable"; that proposal only uses the word "reachable" once, and that is merely a description about the relationship between a pointer and a size (which really ought to be a `std::span<std::byte>`).And your definition of "reachable" here does not map to what P0593 is doing. `std::bless` and equivalent functions declare that a piece of memory creates implicit objects. When you call `bless` on a range of memory, objects will be created to make your code work. What your "reachable" behavior seems to want is for them to already exist.
This may seem like a trivial difference, but it isn't. P0593 points it out: when you create objects, the contents of the storage are rendered unspecified. And this is just as true for objects implicitly created by `std::bless`.
You explicitly don't want this. You want the objects to already exist, with their values intact. Non-template `bless` can't do that; `bless<T>` can, but only for a single complete object of a specified type. And even then, it is still creating the object, not making it "reachable". It wasn't there until `bless<T>` was called.
On Focus:Your section 3.2 has nothing to do with the C++ object model or the needs of mapping memory. That's not to say that they aren't useful, but the presence of side effects is orthogonal to memory mapping or the object model.
My hot take is that this is waaay too low-level and implementation-detaily for a WG21 proposal. C++ programs have to be able to run on hardware that might not even have the concept of a "TLB".
So you have this operation you want to be able to do. You want to take objects in one address, make them disappear from that address, and then make them appear in a (potentially) different address. And you want to change the C++ object model to make this work.Here's a question: do you need to change the object model in order to get this effect?So, here's what we want to be able to do without UB:
- Modify where allocated memory appears in the process (use cases: expanding arrays without memory copies, patching binaries, mapping page cached files etc)
- Share a region of memory with another running process (use cases: IPC, multiprocessing, etc)
- Reduce C++ causing unnecessary page faults/prevent use of memory with changed access permissions by making the compiler aware of what memory is not to be touched.
- Handle objects written out in a previous incarnation of a program being at a different address in a new incarnation of a program.
- Enforce a new read/write reordering constraint separate to compiler and thread barriers.
In hindsight, I probably ought to have specified this more succinctly in the paper. I will change it.There are also multiple levels of persistability going on here:
- Trivially copyable types, which are just bits. You can use their in-memory representation directly, just need to constrain write reordering.
- Trivially relocatable types, which are moved with just bits, if we standardise those. You can use their in-memory representation directly, just need to constrain write reordering.
- Polymorphic types where if it weren't for the vptr, would be either of the first two categories. You can use their in-memory representation directly, bar the vptr restamp on load.
- Other types whose in-memory representation can be used directly after some trivial transformation e.g. replace all pointers with offset pointers.
So the common factor to all these is that their in-memory representation is sufficient for their persisted representation i.e. no memory copying needed, but there can be a "phase change" between "active" and "sleeping".Serialisation is where you need to create new objects which represent a persisted edition of an in-memory form. That's a whole separate topic, to be addressed as iostreams v2 after we've nailed down the in-memory stuff which is a prerequisite as the serialised form of an object is clearly a persistable object meeting at least the criteria of the above.
I think "Mapped storage duration" is too on-point in terms of where the memory comes from, rather than how C++ thinks of it.The four storage durations are based on how C++ interacts with them. Automatic storage duration objects have storage durations determined entirely by their objects' scopes. Static storage duration objects have their storage duration be the lifetime of the program. Thread-local storage duration objects have their storage duration be the lifetime of the thread. And dynamic duration objects have their storage duration determined dynamically: by direct manipulation of the code.Sure. And I propose that mapped storage duration has the lifetime of its backing filesystem entity. Same approach.But I think there is a deeper misunderstanding here. You are confusing properties of "storage" with the properties of "storage duration". The former is a thing that an object needs to have in order to exist. The latter is a property of an object, determined by the means used to construct it, and it's purpose is to govern the (default) lifetime of that object.For example, you can create an automatic variable with automatic storage duration. You can then reuse that variable's storage to create objects with dynamic storage duration. By reusing it, you will have ended the lifetime of the automatic variable. You would then have an object with dynamic storage duration whose storage happens to be on the stack.Which is why "stack storage duration" would be a misnomer, much like "mapped storage duration".Would "filesystem storage duration" be better?
I originally discounted that naming because there are mapped storage durations whose backing filesystem entity is the swap file/kernel, so I felt mentioning filesystem would be confusing.Of course, C++ has a bunch of rules about what happens when you do this and then try to use that variable's name later on. As well as what happens when that variable goes out of scope, and what you need to do to make leaving scope not invoke UB (namely, create an object of that type back in that storage).What you seem to want is simply a wider form of dynamic storage duration. You're not changing the meaning of the storage duration; you're simply adding ways to create/destroy such objects.It's more I'm looking for a storage duration which exceeds the duration of the program, but still has a well defined duration. Dynamic storage can't do that.
int i; //i has automatic storage duration.
auto pf = new(&i) float;
On Bless:P0593 proposed these functions to mark regions as reachable:Your notion of "reachability" has nothing to do with what P0593 is doing. `bless` does not make objects "reachable"; that proposal only uses the word "reachable" once, and that is merely a description about the relationship between a pointer and a size (which really ought to be a `std::span<std::byte>`).And your definition of "reachable" here does not map to what P0593 is doing. `std::bless` and equivalent functions declare that a piece of memory creates implicit objects. When you call `bless` on a range of memory, objects will be created to make your code work. What your "reachable" behavior seems to want is for them to already exist.Eh ... you're right that I find blessing underspecified. This is that fuzziness about lifetime I mentioned earlier. I've spent a good few weeks pondering P0593, indeed I asked a ton of questions about it as the SG12 meeting at Rapperswil. And I'm confused about what it precisely does.Personally, I find reachable vs unreachable a much less ambiguous explanation of what it does.
And more helpful for compiler writers and formal verification. But I totally get that it may simply be my incompetence, and I need to be corrected. I am not a compiler writer after all.(Richard Smith, feel free to jump in and correct me now!)This may seem like a trivial difference, but it isn't. P0593 points it out: when you create objects, the contents of the storage are rendered unspecified. And this is just as true for objects implicitly created by `std::bless`.Sure. They become reachable. It says nothing about their content.
Specifically, the value held by an object is only stable throughout its lifetime. When the lifetime of the int object in line #1 ends (when its storage is reused by the float object in line #2), its value is gone. Symmetrically, when the float object is created, the object has an indeterminate value ([dcl.init]p12), and therefore any attempt to load its value results in undefined behavior.
In some cases it is desirable to change the dynamic type of existing storage while maintaining the object representation. If the destination type is an implicit lifetime type, this can be accomplished by usage of std::bless to change the type, followed by std::launder to acquire a pointer to the newly-created object.
int i = 45;
int *pi = new(&i) int;
std::cout << i;int i = 45;
std::bless(&i, sizeof(int)); //"Creates objects" in `i`.
std::cout << i; //Newly created `int` object, due to `bless` and access
--
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposal...@isocpp.org.
To post to this group, send email to std-pr...@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/f7dffe3e-6e9c-47cb-a834-267e170640ed%40isocpp.org.
[...]Mapped storage has the most similarity to dynamic storage, but with the following differences:
- It has allocation and alignment granularities with architecture specific coarse sizes (‘memory page’). 4Kb/2Mb/1Gb page sizes are common.
- New allocations are guaranteed to be all bits zero on creation.
- It has map on first read semantics. This means that the first read from a memory page can take hundreds of CPU cycles, and a TLB shootdown causes an interrupt for other CPUs.
- It has allocate on first write semantics. This means that the first write to a memory page can take thousands or even hundreds of thousands of CPU cycles, plus a TLB shootdown.
- Usually, but not always, mapped storage is a memory cache of equivalent storage a high latency storage device. Hence they can be individually pushed to storage, their contents thrown away, deallocated, given different access permission or caching strategies, and lots of other interesting (i.e. potentially game changing for large STL containers) operations. Individual pages can be:
- Read-only, read-write, or copy-on-write. These are self describing, and these are hard characteristics: violating them means program failure.
- Committed or uncommitted. This indicates whether the page counts towards the resources used by the C++ program. Uncommitted memory is inaccessible, and acts as a placeholder for later use (i.e. it is reserved address space, useful for expanding large arrays without content copying).
- Dirty or clean. This indicates whether the page contains data not yet mirrored onto its backing storage.
- Allocated or unallocated. This indicates whether storage backing the page has been allocated on the storage device. If not, the first write to a clean page may be very expensive as the page may need to be copied by the kernel and/or space allocated for it on the backing storage device.
The storage represented by a section_handle instance can be mapped into (i.e. made available to) a C++ program by creating a map_handle sourcing the storage from a section_handle instance. The storage mapped by the low level map_handle shall represent unconstructed and unreachable memory, and will require the use of std::bless() or map_view to make it reachable (alternatively, use the convenience class mapped on a section_handle instance which bundles the aforementioned low level operations on your behalf).
My hot take is that this is waaay too low-level and implementation-detaily for a WG21 proposal. C++ programs have to be able to run on hardware that might not even have the concept of a "TLB". The stuff in this bulleted list is perfectly reasonable internal documentation for an implementation, but it seems like the sort of thing that ought to be abstracted away from everyday C++ programmers as much as possible.
I would want the trajectory to be:- implement this stuff- implement something higher-level and suitably abstract on top of this stuff- consider how to standardize that abstract interface- don't bother to mention the low-level details in that proposal (but have the reference implementation available for vendors to look at)
I'm sure I'm ill-informed and there will be apparently good reasons not to take this trajectory, but it's still what I would want to see as an end-user.(And yes, I wish the Networking TS would have taken the same trajectory.)
3.1.3 A new lifetime stage: unreachable
I propose adding a new separate lifetime status for objects: unreachable. Under P0593, unconstructed objects are unreachable, but there is no possibility for a partially constructed, alive or partially destructed object to be unreachable. I propose that this new status ought to be added [...]
The paper may answer this question, but: Is this related at all to the existing garbage-collection hooks, http://eel.is/c++draft/util.dynamic.safety, which already define terminology such as "declare reachable"?
P0593 proposed these functions to mark regions as reachable:
// Requires: [start, (char*)start + length) denotes a region of allocated
// storage that is a subset of the region of storage reachable through start.
// Effects: implicitly creates objects within the denoted region.
void std::bless(void *start, size_t length);
// Effects: create an object of implicit lifetype type T in the storage
// pointed to by T, while preserving the object representation.
template<typename T> T *std::bless(void *p);Hooray! For purposes of P1144 relocatability, btw, I am hoping that the words "implicit-lifetime" will be removed from the above comments. My understanding of "implicit-lifetime" is that implicit-lifetime types are precisely those types which it would be safe to access even without a call to std::bless. However, I am out of the loop.
Thus the obvious corrollary for marking regions as unreachable:
// Requires: [start, (char*)start + length) denotes a region of allocated
// storage that is a subset of the region of storage reachable through start.
// Effects: implicitly uncreates objects within the denoted region.
void std::unbless(void *start, size_t length);
// Effects: uncreate an object of implicit lifetype type T in the storage
// pointed to by T, while preserving the object representation.
template<typename T> void *std::unbless(T *p);Nit: The template version should return `void` not `void*`, right?
I also don't fully understand the desired semantics of the non-template version of either `bless` or `unbless`.
And I don't understand why you need "stun / polymorphic_unbless" at all. What does it even mean to make a polymorphic object "unusable"? How is that different from just doing nothing at all?
And if "revive / polymorphic_bless" is not a no-op but actually does have an observable effect on memory, that's a big deal; you should emphasize that somehow.
There are also multiple levels of persistability going on here:
- Trivially copyable types, which are just bits. You can use their in-memory representation directly, just need to constrain write reordering.
- Trivially relocatable types, which are moved with just bits, if we standardise those. You can use their in-memory representation directly, just need to constrain write reordering.
- Polymorphic types where if it weren't for the vptr, would be either of the first two categories. You can use their in-memory representation directly, bar the vptr restamp on load.
- Other types whose in-memory representation can be used directly after some trivial transformation e.g. replace all pointers with offset pointers.
There may be a common factor in these cases, but there is no common solution with them. Cases #1 and #2 are fundamentally different from #3 and #4. Why? Because it's impossible to do 3&4 on an arbitrary range of memory.
In cases 1&2, you can logically move these objects' values around and reconstitute them with no per-object code. This means you could just have the mapping/unmapping operation perform the "operation" needed to make this legal.With 3&4, you're packing an object's values into a form from which it can be unpacked to reconstitute the original object's value. This operation has to be done on a per-object basis. That is, you can't just do "unmap range of memory, map it latter, do some casting, and go." You have to go to each object and perform some operation; code must be executed for each object before each unmap and after each map. Also, this operation has to cascade up and down the subobject hierarchy, applying that operation to each subobject in turn. Preferably in an order similar to how subobjects are created and destroyed.
If you're executing code at map/unmap time, you are doing serialization. It may not be the most complex form of serialization, but it is still serialization.
As such, I would say that what you're talking about is best handled in the C++ object model via what I said: destruction and construction. If you're unmapping memory, then any objects within it are destroyed, ending their lifetimes. When you map them, a new object is constructed in the storage whose values contain the old objects' valuesSince this would be done via beginning and ending object lifetimes, that provides opportunities to inject user/compiler-code into the process via specialized constructors and destructors. For some objects (which may or may not equate to implicit lifetime or trivially relocatable types), these constructors/destructors would be trivial, and as such, we can give them special properties and effects.
If I were to give a name to this process, I would say that you are trivializing the value of an object. At its core, what you're wanting to do is to take an object, turn its values into data that is no longer considered to be "that object", but still is within the same storage. These bytes can be copied around verbatim, or the underlying storage can be persisted as you desire. You can later detrivialize the value back into an object of that exact type.The trivialized form of an object is not that object anymore. It's just bytes of data. For some objects, its trivial form is exactly its regular form. For other objects, this is not the case. But the trivial form has the same size as the regular form.
A trivializing destructor takes the object and alters its values to make it trivial. As implied by this being invoked by a destructor, once you trivialize the object's data, the object's lifetime has ended. The storage behind a trivialized object is preserved after the object's lifetime has ended.A detrivializing constructor takes the object representation of a trivialized object of that type and undoes the trivialization.The one change to the object model that would be needed to do all of this would be a guarantee of the preservation of the values of a destroyed/created object. Currently, the C++ object model makes no guarantees of the value of the object's storage after its lifetime has ended. And when you begin the lifetime of an object, its storage before initialization has either zero values (if it's static) or unspecified values.
So you would need to say that trivializing destructors preserve the value of the memory, and detrivializing initialization/construction operations would maintain the storage of the memory at the start of the detrivializing constructor. And of course, objects with trivial trivializing operations (maybe "trivializing" wasn't such a good term...) can do these implicitly.You will still need a placement-new replacement to be able to detrivialize objects with non-trivial detrivializing construction. The placement part obtains storage for the object, and that part is legally allowed to overwrite such storage. So you'd need an alternate version which isn't allowed to.
Note that a TriviallyCopyable type is not necessarily an object with trivial trivialization (wow, that's terrible naming ;). The reason being: pointers. An object with pointers is TriviallyCopyable, so you could theoretically map or unmap it. But if those pointers point to mapped memory, you need to manually trivialize and detrivialize them.
Indeed, I'm starting to wonder about that. It seems that the need for trivialization is more a property of the object than of the object's type. Trivialization ought to be distinct from trivial relocation and trivial copying. But it should still be something done via constructors/destructors.
Would "filesystem storage duration" be better?Let me try to explain again.Automatic storage duration is named based on its high-level concepts: the storage duration of the object is handled "automatically" by the scope of the declaration. The memory for that storage may come from something we call the "stack", but we don't name it "stack storage duration" even though such objects are (nominally) on the "stack".The same goes for your notion. Both "mapped" and "filesystem" are all named based on where the storage comes from. That is what is wrong with these terms. Terms should be based on the high-level concepts of the kind of storage duration you're imposing on the object, not based on where the memory is or how it's being interacted with.
Eh ... you're right that I find blessing underspecified. This is that fuzziness about lifetime I mentioned earlier. I've spent a good few weeks pondering P0593, indeed I asked a ton of questions about it as the SG12 meeting at Rapperswil. And I'm confused about what it precisely does.Personally, I find reachable vs unreachable a much less ambiguous explanation of what it does.If you're confused by "what it precisely does", then I'm not sure how good an idea it is to impose your own terminology on it. You need to understand what it currently means before you can say that your terminology describes that meaning better or worse. After all, if you don't understand it, your terminology could be imposing different behavior than what it's trying to do.
From the section I linked to (emphasis added):Specifically, the value held by an object is only stable throughout its lifetime. When the lifetime of the int object in line #1 ends (when its storage is reused by the float object in line #2), its value is gone. Symmetrically, when the float object is created, the object has an indeterminate value ([dcl.init]p12), and therefore any attempt to load its value results in undefined behavior.The thing about P0593 is that it's actually a bit inconsistent about this. Since we don't have actual standards wording, we have to do the best we can with the details we've been given. The area of inconsistency is that Section 4.1 of the same document says:In some cases it is desirable to change the dynamic type of existing storage while maintaining the object representation. If the destination type is an implicit lifetime type, this can be accomplished by usage of std::bless to change the type, followed by std::launder to acquire a pointer to the newly-created object.That section can only be true if the previously quoted section is false. If there's a newly created object in that storage, the only way to access the value from the recently-destroyed object that used to live there is if it's value is not "gone".However, section 4.1 is the odd man out in P0593. Throughout that paper, it refers to the operation which `bless` performs as "creating objects". If you create an object in storage that already contains an object, then you are reusing that object's storage. Reusing an object's storage ends that object's lifetime.And the C++ object model is very clear: the value of an object whose lifetime has ended is not preserved. The contents of an object's storage after its lifetime has ended is not defined by the standard.So my reading of P0593 is that if you `bless` a range of memory, any objects already residing there have their lifetimes ended. The objects which have been created by this process therefore have unspecified values, since all previous objects in that storage have been reused.And once an object's lifetime ends, the value of that object is gone.
Basically, consider this:int i = 45;
int *pi = new(&i) int;
std::cout << i;The standard says that this is not guaranteed to print 45. My reading of P0593 is that the following is the equivalent of the above:int i = 45;
std::bless(&i, sizeof(int)); //"Creates objects" in `i`.
std::cout << i; //Newly created `int` object, due to `bless` and accessSo this is not guaranteed to print 45 either. And neither `reinterpret_cast` nor `launder` would change that.
Now, I could be misinterpreting P0593. Like I said, it doesn't have formal standards wording. But the frequent use of the phrase "create objects", coupled with the explicit prohibition of type punning, suggests that I'm probably right.
"New allocations are guaranteed to be all bits zero on creation."This is almost always going to be the case in hosted environments for security reasons. On freestanding implementations, the zero'ing isn't a sunk cost like it is on hosted implementations. I think that you should say that the contents are unspecified on creation.
3.1.4I'm not super interested in mapped polymorphic objects. I am interested in having objects reachable from multiple mappings simultaneously. Inter-process communication with shared memory is a real thing, and I think mapped storage in combination with regular C++11 atomics makes it work.
I expect resistance from the implementers when it comes to modifying the vptr. Optimizers can currently assume it won't change, except when going through launder barriers.
3.2I generally like it. You might want to see if there is any further inspiration to draw from gcc assembly clobber lists.
I understand your point on the whole parallel constructor/destructor thing. The reason I suggested to use them is that it fits within the existing paradigm, which means you don't have to do something like create a new series of special member functions. But making new functions may be the cleaner alternative.I'm going to invent some new strawman terminology because "trivialization" just isn't working out. Let's call it "reduction" and "regeneration". So every type has "reducers", which work like destructors in that they end the object's lifetime, but they ensure that the object's values remain present. And every type has "regenerators", which work like constructors in that they begin the object's lifetime but can access the reduced value of the objects in their current space.Both reduction and regeneration functions need to be able to take arbitrary parameters. And you need special syntax to invoke regeneration, for a specific type, on a piece of memory.Reduction and regeneration can be trivial operations. For implicit lifetime and TriviallyRelocatable types, reduction can be done trivially. But you can also perform non-trivial reduction/regeneration of specific objects of those types.This is actually a lot like the relationship between such types and their constructors. implicit lifetime types can have non-trivial constructors, but there are still trivial ways of interacting with them.Some operations invoke "trivial reduction" on all complete objects in a piece of storage. These would be things like functions that unmap objects, but much like your original `unbless`, we can have a function to directly invoke this process. If that storage contains objects which do not have trivial reduction, undefined behavior results. So if you want to do non-trivial reduction, you have to do it before performing such reduction.Similarly, some operations invoke "trivial regeneration", like the memory mapping process. And this is where we have to start getting into `bless`-like wording, where objects of trivially regeneratable types can "magically" appear based on usage. But you can also explicitly invoke non-trivial regeneration for specific objects, which causes those objects to pop into being.So let's explore some rules. Trivial reduction requires:* There is no user-provided default reducer overload.* All subobjects must have trivial reduction.* The type must be TriviallyRelocatable or Implicit Lifetime (which requires a trivial destructor).And similarly, trivial regeneration requires:* There is no user-provided default regeration overload.* All subobjects must have trivial regeneration.* The type must be TriviallyRelocatable or Implicit Lifetime.So, here's what we need:1. Syntax for declaring reducers/regenerator member functions. It needs to not conflict with existing names.* Regenerators probably need similar abilities that constructors have. Things like member initializer lists, inheriting regenerators, forwarding regenerators, etc.2. Changes to the object model to allow for reducers/regenerators to destroy/create objects, but preserving the stored bitpattern of the results of the reduction and providing it to the regenerator on regeneration.* There needs to be some idea of how exceptions work with regeneration. That is, when exactly each subobject is considered live, so that exceptions thrown from later subobject regeneration can destroy them. These rules could work much like the rules of constructors, but we need to spell them out all the same.3. Syntax for invoking regeneration on a piece of memory (which is required to contain the data from a previous reduction operation for an object of that type).4. Syntax for invoking reduction on an object. It would probably look like an explicit destructor call.
Thinking about this further, "trivial relocation" could be redefined as trivial reduction, memcpy, and trivial regeneration. And thus, a type is trivially relocatable if it has no-argument reducer and regenerators that are trivial, which can be declared with `= default` syntax.Now, I'm not saying we should go changing Arthur's proposal, since it is much farther along than this. But it would allow us to have a second way to declare that a type is TriviallyRelocatable.
I understand your point on the whole parallel constructor/destructor thing. The reason I suggested to use them is that it fits within the existing paradigm, which means you don't have to do something like create a new series of special member functions. But making new functions may be the cleaner alternative.I'm going to invent some new strawman terminology because "trivialization" just isn't working out. Let's call it "reduction" and "regeneration". So every type has "reducers", which work like destructors in that they end the object's lifetime, but they ensure that the object's values remain present. And every type has "regenerators", which work like constructors in that they begin the object's lifetime but can access the reduced value of the objects in their current space.
So let's explore some rules. Trivial reduction requires:* There is no user-provided default reducer overload.* All subobjects must have trivial reduction.* The type must be TriviallyRelocatable or Implicit Lifetime (which requires a trivial destructor).
Thinking about this further, "trivial relocation" could be redefined as trivial reduction, memcpy, and trivial regeneration. And thus, a type is trivially relocatable if it has no-argument reducer and regenerators that are trivial, which can be declared with `= default` syntax.
On Friday, August 3, 2018 at 6:33:25 PM UTC+1, Nicol Bolas wrote:Thinking about this further, "trivial relocation" could be redefined as trivial reduction, memcpy, and trivial regeneration. And thus, a type is trivially relocatable if it has no-argument reducer and regenerators that are trivial, which can be declared with `= default` syntax.Now, I'm not saying we should go changing Arthur's proposal, since it is much farther along than this. But it would allow us to have a second way to declare that a type is TriviallyRelocatable.Still though, that's exciting because this gives Arthur the ability to relocate polymorphic objects. I agree that the current revision of his proposal shouldn't propose this, but a future addendum paper certainly could extend his work via this mechanism.
Thinking about this further, "trivial relocation" could be redefined as trivial reduction, memcpy, and trivial regeneration. And thus, a type is trivially relocatable if it has no-argument reducer and regenerators that are trivial, which can be declared with `= default` syntax.
Now, I'm not saying we should go changing Arthur's proposal, since it is much farther along than this. But it would allow us to have a second way to declare that a type is TriviallyRelocatable.
Still though, that's exciting because this gives Arthur the ability to relocate polymorphic objects. I agree that the current revision of his proposal shouldn't propose this, but a future addendum paper certainly could extend his work via this mechanism.
Thanks Nicol. That was a very productive discussion.
Now, I'll be frank here, I don't think I have the spare time between now and SD mailing deadline to write this up. Getting a lot of pressure to issue new editions of my other papers, especially on the deterministic exceptions front where WG14 are super keen for detail, as is Herb. But I'll certainly aim for Kona.
(BTW this is to not shut down any further feedback on my draft paper. I certainly would be very interested in what people think of Nicol's proposal. Oh, and should these operations be functions, or operators?)
Niall
--
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposal...@isocpp.org.
To post to this group, send email to std-pr...@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/76f49e93-09c4-4eec-98d3-c7f0bdeea573%40isocpp.org.
On 4 August 2018 at 00:49, Nicol Bolas <jmck...@gmail.com> wrote:
>> dematerialize / reify
>> dematerialize / manifest
>> dematerialize / materialize
>[...]
> Materialize is already taken by prvalues and temporaries. So "dematerialize"
> would be confusing.
>
> For the time being, I've settled on "activate/deactivate".
Fine. dissolve / reify, then.
On Fri, Aug 3, 2018 at 3:02 PM, Ville Voutilainen <ville.vo...@gmail.com> wrote:On 4 August 2018 at 00:49, Nicol Bolas <jmck...@gmail.com> wrote:
>> dematerialize / reify
>> dematerialize / manifest
>> dematerialize / materialize
>[...]
> Materialize is already taken by prvalues and temporaries. So "dematerialize"
> would be confusing.
>
> For the time being, I've settled on "activate/deactivate".
Fine. dissolve / reify, then.Before anyone can decide on a name for the thing, step 1 is to decide on what the thing actually is.Niall's original conception was that the thing is not related to beginning or ending object lifetime at all — object lifetime just kind of persists "through" this operation.(If the operation is related to beginning and ending object lifetime, then I propose "construct" and "destruct". ;))My own conception is that I want to see how this functionality would actually be used in a real-world high-level library; I imagine that we don't need any special names or semantics at all. For example, we could just state that if you use "libPersistence", thenauto p = libpersistence::open("somefile.bin");std::vector<int> foo = p.readobject("foo");p.writeobject("foo") = foo;will just work, and we don't necessarily need to describe the mechanism by which it works — not right away, anyway. Step one is to focus on that implementation: can it be implemented? is it efficient enough for real-world uses? is it robust enough for real-world uses?
Thinking about this further, "trivial relocation" could be redefined as trivial reduction, memcpy, and trivial regeneration. And thus, a type is trivially relocatable if it has no-argument reducer and regenerators that are trivial, which can be declared with `= default` syntax.
By my above definition, they would not have trivial relocation abilities, since reduction/regeneration would have to execute code. And as Arthur pointed out, there are differences between relocation and reduction/regeneration. Indeed, based on his post, TriviallyRelocatable is not something we can use to allow trivial reduction/regeneration. Or rather, it would have to be TriviallyRelocatable & non-polymorphic.I need to think on this some more.
Niall's original conception was that the thing is not related to beginning or ending object lifetime at all — object lifetime just kind of persists "through" this operation.(If the operation is related to beginning and ending object lifetime, then I propose "construct" and "destruct". ;))My own conception is that I want to see how this functionality would actually be used in a real-world high-level library; I imagine that we don't need any special names or semantics at all. For example, we could just state that if you use "libPersistence", thenauto p = libpersistence::open("somefile.bin");std::vector<int> foo = p.readobject("foo");p.writeobject("foo") = foo;will just work, and we don't necessarily need to describe the mechanism by which it works — not right away, anyway. Step one is to focus on that implementation: can it be implemented? is it efficient enough for real-world uses? is it robust enough for real-world uses?
But certainly, if there is any reason to subtly prefer "reify" over "manifest" or vice versa, it will strongly depend on the subtle semantics of the operation. And we do not have any semantics at all, yet. Heck, I haven't seen a use-case yet.
The kind of persistence that Niall is talking about is persistence at its lowest level: what happens to a C++ object if it is in mapped memory that gets unmapped? Can it survive? Can another process receive and manipulate that object? How can we make transmission of objects work at that level?
Defining this is the point of this exercise. The high level details you're talking about for some hypothetical library are beyond this exercise. We're talking about a low-level tool for low-level needs. How high-level code deals with it is something that can be looked at once we allow the object model to be able to consider these possibilities.Think about all of the issues that went into defining the C++11 memory model that included the possibility of data races. Those weren't based on high-level constructs and the like; it was solely about pinning down how memory works between threads. That's the kind of thing this is dealing with: how objects work in a world where storage for them can exist beyond the lifespan of the program.
Thinking about this further, "trivial relocation" could be redefined as trivial reduction, memcpy, and trivial regeneration. And thus, a type is trivially relocatable if it has no-argument reducer and regenerators that are trivial, which can be declared with `= default` syntax.By my above definition, they would not have trivial relocation abilities, since reduction/regeneration would have to execute code. And as Arthur pointed out, there are differences between relocation and reduction/regeneration. Indeed, based on his post, TriviallyRelocatable is not something we can use to allow trivial reduction/regeneration. Or rather, it would have to be TriviallyRelocatable & non-polymorphic.I need to think on this some more.Trivial operation X simply means that the compiler can perform operation X entirely on its own, as frequently and infrequently as it so chooses, including ignoring the programmer if it would be as-if the same. That's the meaning of triviality.
If the compiler can trivially reduce, copy and regenerate on its own, that's still trivial by definition. It's also optimisable, so if the compiler knows that nothing will modify the bytes between the reduction and regeneration, it can optimise out say vptr modification and just use straight memcpy.
Niall's original conception was that the thing is not related to beginning or ending object lifetime at all — object lifetime just kind of persists "through" this operation.(If the operation is related to beginning and ending object lifetime, then I propose "construct" and "destruct". ;))My own conception is that I want to see how this functionality would actually be used in a real-world high-level library; I imagine that we don't need any special names or semantics at all. For example, we could just state that if you use "libPersistence", thenauto p = libpersistence::open("somefile.bin");std::vector<int> foo = p.readobject("foo");p.writeobject("foo") = foo;will just work, and we don't necessarily need to describe the mechanism by which it works — not right away, anyway. Step one is to focus on that implementation: can it be implemented? is it efficient enough for real-world uses? is it robust enough for real-world uses?Nobody is in any doubt that a serialisation library can be written which works well on the major compilers. There are dozens to choose from. P1026 A call for a Data Persistence (iostream v2) study group lists and benchmarks some of them.
Intel's PMDK https://github.com/pmem/pmdk goes a step further, and stores STL objects directly in persistent memory. It can use mapped kernel cache memory in lieu of proper persistent memory. So again, nobody is in any doubt that these can be written and they work well on the major compilers.
So after many weeks of pondering and writing this outside of work each day, please find attached an early draft of revision 1 of P1031 destined for San Diego. Changes since R0:
- Wrote partial draft TS wording for deadline, handle, io_handle, mapped, mapped_view, native_handle_type and file_io_error.
- Added impact on the standard regarding potential changes to the C++ object model.
- Added impact on the standard regarding the proposed [[no_side_effects]] et al contracts attributes.
On Mon, Aug 6, 2018 at 10:13 AM, Niall Douglas <nialldo...@gmail.com> wrote:Thinking about this further, "trivial relocation" could be redefined as trivial reduction, memcpy, and trivial regeneration. And thus, a type is trivially relocatable if it has no-argument reducer and regenerators that are trivial, which can be declared with `= default` syntax.By my above definition, they would not have trivial relocation abilities, since reduction/regeneration would have to execute code. And as Arthur pointed out, there are differences between relocation and reduction/regeneration. Indeed, based on his post, TriviallyRelocatable is not something we can use to allow trivial reduction/regeneration. Or rather, it would have to be TriviallyRelocatable & non-polymorphic.I need to think on this some more.Trivial operation X simply means that the compiler can perform operation X entirely on its own, as frequently and infrequently as it so chooses, including ignoring the programmer if it would be as-if the same. That's the meaning of triviality.I should point out that that is not my definition of triviality. My definition of triviality is "as-if by memfoo." That is, for any given operation on an object-in-memory (e.g. copying, e.g. relocation, e.g. comparison, e.g. destruction, e.g. swapping) there is exactly one "trivial" implementation of that operation, which relies on absolutely nothing about the object's type except maybe its size.
This is subtly different from the current Standard's notion of "triviality," which is closer to Niall's (but still not identical). The Standard's notion is that any operation is "trivial" iff it runs no user-provided functions.E.g. copying a `volatile int` is "trivial" by the Standard's definition but not by mine (since it cannot be done by memcpy) nor Niall's (since it cannot be memoized).E.g. constructing a `float` from an `int` is "trivial" by the Standard's definition (is_trivially_constructible_v<float,int>) and by Niall's, but not by mine (since it cannot be done by memcpy).E.g. comparing two `float`s is "trivial" by the Standard's definition and by Niall's, but not by mine (since it cannot be done by memcmp).If the compiler can trivially reduce, copy and regenerate on its own, that's still trivial by definition. It's also optimisable, so if the compiler knows that nothing will modify the bytes between the reduction and regeneration, it can optimise out say vptr modification and just use straight memcpy.I still don't see what you hope to achieve with "vptr modification." If a persisted object contains native pointers, they will not point to the correct place when reloaded, unless <a global property of the program is carefully maintained by the programmer>. This applies to all kinds of structs, not just vptrs.
struct Vec { int *p; }; // cannot be written out and "regenerated" later
struct MemFn { T *p; void (*f)(T); }; // cannot be written out and "regenerated" laterstruct Poly { virtual void foo(); }; // cannot be written out and "regenerated" laterI definitely wouldn't worry about the third example until you've got something working-in-practice for the first two, simpler, examples.
> nobody is in any doubt that these can be written and they work well on the major compilers.> What we are doing here is figuring out how to standardise support for these sorts of library into the C++ standard. [...]> we need to improve the C++ object and lifetime model such that resorting to UB tricks is no longer necessary.I think that's simply "std::bless should work for non-implicit-lifetime types," as suggested in the "Further Work" section of P0593,> a combined operation to create an object of implicit lifetime type in-place while preserving the object representation may be useful ... we could imagine extending its semantics to also permit conversions where each subobject of non-implicit-lifetime type in the destination corresponds to an object of the same type (ignoring cv-qualifications) in the source ...All you need is a way to say "here are some bytes, please treat them as an object." Right now, everyone (including STL vendors) does that via reinterpret_cast, and it works great, and it isn't going to break anytime soon (because all STL implementations depend on it). Post-P0593-further-work, some people may switch to using reinterpret_cast plus std::bless, which will work equally great (modulo an initial crop of compiler bugs, I'm sure). I don't think the UB-ness of reinterpret_cast is really the biggest blocker to getting persistent data structures into C++.
On Monday, August 6, 2018 at 3:10:34 PM UTC-4, Arthur O'Dwyer wrote:On Mon, Aug 6, 2018 at 10:13 AM, Niall Douglas <nialldo...@gmail.com> wrote:Trivial operation X simply means that the compiler can perform operation X entirely on its own, as frequently and infrequently as it so chooses, including ignoring the programmer if it would be as-if the same. That's the meaning of triviality.I should point out that that is not my definition of triviality. My definition of triviality is "as-if by memfoo." That is, for any given operation on an object-in-memory (e.g. copying, e.g. relocation, e.g. comparison, e.g. destruction, e.g. swapping) there is exactly one "trivial" implementation of that operation, which relies on absolutely nothing about the object's type except maybe its size.This, exactly. This is what I think of when dealing with triviality.And note: types with virtuals are not trivially default constructible, nor are they Trivially Copyable. So the standard is at least partially in agreement with this notion.
I still don't see what you hope to achieve with "vptr modification." If a persisted object contains native pointers, they will not point to the correct place when reloaded, unless <a global property of the program is carefully maintained by the programmer>. This applies to all kinds of structs, not just vptrs.That's true. But unlike pointer members of the object, you can't modify the vtable pointer. By "you", I mean "the user". Such things are the domain of the compiler.So if you want to be able to persist polymorphic types, you have to have compiler involvement.
That's why I like thinking of persistence as a form of construction/destruction. When you construct a polymorphic type, it puts the vtable pointer in place. So when you unpack/whatever the value representation of a polymorphic type, that operation can likewise put the vtable pointer in place. And part of that operation should, just like object construction, allow you to execute whatever code you need for manual fixup of any pointers.
struct Vec { int *p; }; // cannot be written out and "regenerated" laterWhether it can be done without code execution or not depends entirely on what `p` points to and how long the object will be persistent. If `p` points to a static object and the particular persistent instance of `Vec` will not be persistent across program executions, then it's fine as is.
All you need is a way to say "here are some bytes, please treat them as an object." Right now, everyone (including STL vendors) does that via reinterpret_cast, and it works great, and it isn't going to break anytime soon (because all STL implementations depend on it). Post-P0593-further-work, some people may switch to using reinterpret_cast plus std::bless, which will work equally great (modulo an initial crop of compiler bugs, I'm sure). I don't think the UB-ness of reinterpret_cast is really the biggest blocker to getting persistent data structures into C++.You cannot standardize something if the object model says that its undefined behavior. You can't just say, "this has behavior X and it's undefined behavior." That's a contradiction.
On Mon, Aug 6, 2018 at 1:43 PM, Nicol Bolas <jmck...@gmail.com> wrote:On Monday, August 6, 2018 at 3:10:34 PM UTC-4, Arthur O'Dwyer wrote:On Mon, Aug 6, 2018 at 10:13 AM, Niall Douglas <nialldo...@gmail.com> wrote:Trivial operation X simply means that the compiler can perform operation X entirely on its own, as frequently and infrequently as it so chooses, including ignoring the programmer if it would be as-if the same. That's the meaning of triviality.I should point out that that is not my definition of triviality. My definition of triviality is "as-if by memfoo." That is, for any given operation on an object-in-memory (e.g. copying, e.g. relocation, e.g. comparison, e.g. destruction, e.g. swapping) there is exactly one "trivial" implementation of that operation, which relies on absolutely nothing about the object's type except maybe its size.This, exactly. This is what I think of when dealing with triviality.And note: types with virtuals are not trivially default constructible, nor are they Trivially Copyable. So the standard is at least partially in agreement with this notion.Once you're talking about types with vptrs, you're definitely shading into the area where "the Standard's notion of triviality" starts bifurcating into many subtly different definitions that all happen to share the same word. :/ I'm not a fan of the Standard's terminology in this area.Default-constructing an object-with-a-vptr can be done without running any user code, but is non-trivial by my definition and also non-trivial by the Standard's (and I'm not sure about Niall's).Copy-constructing an object-with-a-vptr is can be done without running any user code, and is "trivial" by my definition, and by Niall's, but not by the Standard's.Destructing an object-with-a-vptr is "trivial" by all three definitions.
Destructing an object-with-a-virtual-destructor is "non-trivial" by all three definitions.I still don't see what you hope to achieve with "vptr modification." If a persisted object contains native pointers, they will not point to the correct place when reloaded, unless <a global property of the program is carefully maintained by the programmer>. This applies to all kinds of structs, not just vptrs.That's true. But unlike pointer members of the object, you can't modify the vtable pointer. By "you", I mean "the user". Such things are the domain of the compiler.So if you want to be able to persist polymorphic types, you have to have compiler involvement.I still don't get it — and now I'm even more confused as to what we're talking about, after seeing all those serialization libraries that Niall thought were related to persistence.If you want to serialize a polymorphic object: awesome, just do it.If you want to deserialize a polymorphic object: you'll be constructing a new object on the fly, so you don't have to overwrite any vptrs or anything. No UB here.If you want to store a polymorphic object in persistent memory: awesome, just do it. The library (e.g. PMDK) will take care of the persistence for you.If you want to access a polymorphic object in existing memory, without the aid of any library to bless the pointer for you: adopt the "Further Work" of P0593r2 and call std::bless<T> on that memory.
If you want to construct a polymorphic object in existing memory, some of whose bytes have the correct values and some of which have garbage values, and you do not know the correct values for those bytes: well, that's currently impossible, but if you think that's what you want to do, I'd really like to see your use-case.That's why I like thinking of persistence as a form of construction/destruction. When you construct a polymorphic type, it puts the vtable pointer in place. So when you unpack/whatever the value representation of a polymorphic type, that operation can likewise put the vtable pointer in place. And part of that operation should, just like object construction, allow you to execute whatever code you need for manual fixup of any pointers.This sounds like you're talking about serialization, not persistence.
And deserialization will construct new objects anyway (via placement new, or regular new, or whatever). It never needs to rewrite vptrs. (Or, if I'm wrong, please point to an existing line of code in a serialization library that rewrites vptrs.)
struct Vec { int *p; }; // cannot be written out and "regenerated" laterWhether it can be done without code execution or not depends entirely on what `p` points to and how long the object will be persistent. If `p` points to a static object and the particular persistent instance of `Vec` will not be persistent across program executions, then it's fine as is.Right. And there are "less strongly typed" types, such as `std::pair<int, int*>`, where you can't even hope to tell from the type of the object what its runtime behavior will be like. But for a serialization library (like, one that reads and writes save-game files), this is fine. The programmer just never tries to serialize a `std::pair<int, int*>` per se.All you need is a way to say "here are some bytes, please treat them as an object." Right now, everyone (including STL vendors) does that via reinterpret_cast, and it works great, and it isn't going to break anytime soon (because all STL implementations depend on it). Post-P0593-further-work, some people may switch to using reinterpret_cast plus std::bless, which will work equally great (modulo an initial crop of compiler bugs, I'm sure). I don't think the UB-ness of reinterpret_cast is really the biggest blocker to getting persistent data structures into C++.You cannot standardize something if the object model says that its undefined behavior. You can't just say, "this has behavior X and it's undefined behavior." That's a contradiction.Sure, philosophically speaking it might seem that way. But this model has worked great for C++ for more than 20 years. People never tire of pointing out that std::vector is unimplementable... that malloc() is unimplementable... that std::async is unimplementable... that std::optional is unimplementable... And yet, they're part of the standard library so that working programmers can use them every single day without worrying about how unimplementable they are!
All I'm saying is that step 1 should be to pick some "unimplementable, but useful" semantics that would improve programmers' lives, and implement it.
Nobody is in any doubt that a serialisation library can be written which works well on the major compilers. There are dozens to choose from. P1026 A call for a Data Persistence (iostream v2) study group lists and benchmarks some of them.Do you mean http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p1026r0.pdf page 3? It lists:- memcpy- bitsery- yas- zpp- Cereal- Boost.Serialization- std::stringstreamThese are all what I would call serialization libraries, yes. They're ways of taking an object of type T as input, and producing some kind of formatted string as output (maybe JSON, maybe XML, maybe some proprietary binary format), in a neatly reversible way.
Notably they do not involve the filesystem at all, and they do not involve the object model at all. Serialization is a completely value-space operation.
T someobj;std::string s = serialize(someobj, FMT_JSON); // or FMT_BINARY, same differenceT otherobj = deserialize(s, FMT_JSON);assert(otherobj == someobj); // more or lessI say "more or less" because of course if `T` is `std::shared_ptr<U>`, then these libraries aren't going to make `otherobj == someobj` in the C++ sense; they're going to allocate a new object for `otherobj` to point to. That's because that's their use-case. Their use-case is things like save-games — things where the original objects don't need to persist in any sense. These libraries are all about serialization (marshalling, pickling, stringification...). They don't have anything to do with persistence. They also do not rely on UB.
Intel's PMDK https://github.com/pmem/pmdk goes a step further, and stores STL objects directly in persistent memory. It can use mapped kernel cache memory in lieu of proper persistent memory. So again, nobody is in any doubt that these can be written and they work well on the major compilers.PMDK is what I'm talking about when I say "persistence." I contend that PMDK is still fairly experimental, though; much more experimental than any of the serialization libraries you listed above. Its API doesn't look anything like a JSON-serialization library, nor does it look anything like what's in your P1031R0.
I observe that- Serialization is fundamentally value-semantic.- Persistence is fundamentally object-semantic.
`class mapped` is referred to as `mapped_span` in two places.
`class handle`'s move-constructor is marked [[move_relocates]] even though `class handle` has a virtual destructor.Your P1029R0 proposes that this should mean the attribute is silently ignored.
My D1144 proposes that if you were to mark `class handle` as [[trivially_relocatable]], the compiler would trust you; but that wouldn't change the fact that you'd be lying to it, and you'd get UB at runtime when the compiler incorrectly assumed it could replace some arbitrary derived class's destructor with a `memcpy`.Should we open a side thread to talk about under what circumstances it might be okay to memcpy a thing with a virtual destructor? (My D1144 has a well-formed opinion on the topic, but I admit I don't do much mixing of classical polymorphism with move-semantics in my own code.)
And note: types with virtuals are not trivially default constructible, nor are they Trivially Copyable. So the standard is at least partially in agreement with this notion.
If the compiler can trivially reduce, copy and regenerate on its own, that's still trivial by definition. It's also optimisable, so if the compiler knows that nothing will modify the bytes between the reduction and regeneration, it can optimise out say vptr modification and just use straight memcpy.I still don't see what you hope to achieve with "vptr modification." If a persisted object contains native pointers, they will not point to the correct place when reloaded, unless <a global property of the program is carefully maintained by the programmer>. This applies to all kinds of structs, not just vptrs.That's true. But unlike pointer members of the object, you can't modify the vtable pointer. By "you", I mean "the user". Such things are the domain of the compiler.So if you want to be able to persist polymorphic types, you have to have compiler involvement.
That's why I like thinking of persistence as a form of construction/destruction. When you construct a polymorphic type, it puts the vtable pointer in place. So when you unpack/whatever the value representation of a polymorphic type, that operation can likewise put the vtable pointer in place. And part of that operation should, just like object construction, allow you to execute whatever code you need for manual fixup of any pointers.
struct Base { virtual void foo(); };
struct Derived { void foo() override; };
Base* derived = /* allocate on persistent storage as Derived */;
/* persist derived object (as Base?) */
I'm not sure to fully understand, but I have the impression that persisting polymorphic objects in the unbounded case without adding any extra information is impossible.
What do you mean when say persisting a polymorphic object?
Well, we could just... not have coroutines ;)