-
-
Notifications
You must be signed in to change notification settings - Fork 742
Fix emplace #1082
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix emplace #1082
Conversation
Waiting for the auto tester, then I'll review. |
else static if (is(typeof(T(args)))) | ||
{ | ||
// Struct without constructor that has one matching field for | ||
// each argument | ||
*chunk = T(args); | ||
// each argument. Individually emplace each attribute |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: field
Did dmd git HEAD break again? Why is the autotester failing in dmd test cases when this is only a Phobos pull request? |
Because the dmd test suite uses Phobos. |
The reason I opened my pull request was that I use |
memcpy(chunk, p, T.sizeof); | ||
else | ||
memset(chunk, 0, T.sizeof); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IIUC the purpose of bitblit+optional postblit is performance. Here you prefer the compiler generated assign-bitblit over memcpy.
Can the compiler actually do better than calling memcpy?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AFAIK, the difference is that memcpy is a 100% run-time call: memcpy does not know the size of the thing to be copied, nor the source/destination addresses. This makes it inherently slower. It has to do a lot of run-time checking, and must implement a copy loop.
By comparison a (non-elaborate) an opAssign is a straight up assembly memcopy: the compiler knows at all (most) the parameters at compile time: the source, and the size. This means the call is just replaced by an assembly copy.
Also: memcpy is neither safe nor CTFE-able.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, I had the same intuition.
Even though you have separate functions for the different semantics it wasn't immediately obvious to me that this two static if branches are supposed to do the same. Maybe a comment or not separating the functions would be more expressive.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
memcpy
is handled as an intrinsic in many C++ compiler so it does exploit size and alignment when known statically. Not sure whether dmd does anything in particular with it. cc @WalterBright
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In any case, regardless of memcpy's performance, it is neither safe nor CTFE. It may also have to pay for a pre-typeid run-time call (That's run-time... right?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Currentlymemcpy
is not handled as intrinsic by dmd
. Also it does not seem to take any advantage for D array copies with know size.
One of the reasons it is so complex, is that emplace (for structs) merges both the notions of emplace from another instance (postblit) and actual construction from args. This means it has to try to Analise which one you are trying to call, which can be difficult when you take into account that D does have constructors that take the same type: struct S
{
this(this){writeln("postblit");}
this(S) {writeln("CC");}
this(int) {writeln("this(int)");}
}
void main()
{
S a; //Nothing
S b = a; //postblit
S c = S(a); //CC
S d = S(5); //Constructor
} In particular, there are some strange corner cases, where if the target type and destination types match, but postblit is disabled, then emplace has to try and guess if maybe you want to try to fall back to CC'ing. It makes a mess of things. Things would have been much simpler if we had an extra explicit functions: First would be the normal emplace (basically emplace with 1 arg, which matching types) that basically just postblits. Then, we'd have one that calls actual constructor (eg emplace from arguments). The caller should always know which of the two he wants anyways. It would have made things simpler and safer. EG: S a = void;
S b;
emplace(&a, b); //Explicit request for postblit initialization
emplaceArgs(&a, 1); //Explicit request to *construct* a from the *arguments* "1". Oh well, that's how it is now... Still, I think it is worth thinking about such a change. |
I'm not particularly worried, though indeed simpler would be nicer. This is highly generic and highly leveraged code, the kind is usually inside the compiler. We can hoist it into the language proper because of introspection, and from that perspective it looks as expected. |
Thanks for the detailed explanation. |
That's the goal yes. The idea is:
Well... technically, static opCall is not a construction scheme, so emplace is not supposed to call it. The reason for this is that opCall is just a function like any other that returns a value, and that value can then be assigned/postBlittted onto your current instance. If you want to emplace from an That's my stance anyways. It's up to debate. |
Given preparations for beta, it would great if we could squeeze this into the next release. Thoughts? |
Rebased. Added unittest for, new but already fixed by this, bug 9559: |
unittest | ||
{ | ||
////Works, but breaks in "-w -O" because of @@@9332@@@. | ||
////Uncomment test when 9332 is fixed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be the case now?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, TY. Uncommenting.
Fixes: Issue 9824 - Emplace is broken |
What the status of this pull?
|
Thank you for your code participation. Allow me to answer:
Waiting on a good Samaritan to review it. I am also crawling the boards looking for usecases for emplace to test it.
Very partially, and experimentally. Partially, because it requires the types to have a non-elaborate assign. Experimentally, because it has a tendency to break the compiler (pointer stuff).
There is a dual problem in that code. The first problem is the struct Pair(A,B)
{
A first;
B second;
this(A f, B s)
{
emplace(cast(Unqual!A*)&first, f);
emplace(cast(Unqual!B*)&second, s);
}
} This works, although there may be some "unexpected consequences" to the cast I have not thought of? AFAIK, it should be safe. The second problem is that this can't be done at compile time, because |
I rebased, did some more tweaking/simplifying/documenting. Added some more early diagnosticating for qualified objects. I also hit a compiler bug with CTFE: |
@donc please see this bug: http://d.puremagic.com/issues/show_bug.cgi?id=9982 |
Apart from the compiler bug (which is not blocking, and only sometimes triggers with CTFE), AFAIK, everything works. Could I get a second review on this? I reworked the code a little, and now the "flow" inside emplace is (IMO) simple, clear and straight forward. There are (I'd say) enough unittests to validate correct behavior. Could we try to get this through? It's important. |
Do emplace strongly depends on phobos? Maybe this function correctly place to the druntime? |
Not much, it only uses a few minor traits: I think it would be better to leave it in phobos for now. If you ask me though, the best place to put such a functionality though would be straight into the compiler, via placement new. Compiler knows best; emplace merely reverse engineers what the compiler does for its construction sequence... |
I too think it belongs into druntime because it's kind of the complement to
The template is not the problem, not having access to std.traits is. |
New reg: I think we should try to review this. Even if the plan is to move it to druntime, or change it to typeid(construct), there is currently a lot of code that relies on emplace, and it should be fixed. |
I would like to push for a fix of emplace: it's like disseminating mines in the phobos fields, and it's a pain for learners like me to hit some of them (see http://forum.dlang.org/thread/nxbdgtdlmwscocbiypjs@forum.dlang.org) |
Thank you for your comment. I just checked, and your code does work perfectly fine with this fix. I added your code to the test suite. I think I've had just about enough of this broken emplace. Bugs are reported for it all the time, but it doesn't get fixed. |
Returns: A pointer to the newly constructed object (which is the same | ||
as $(D chunk)). | ||
*/ | ||
T* emplace(T)(T* chunk) | ||
if (!is(T == class)) | ||
T* emplace(T)(T* chunk) @safe nothrow pure |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A function that dereferences a pointer isn't @safe.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, it is safe as long as it dereferences it at offset 0. It's the arithmetic that is unsafe.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm unsure anyways in these cases, as emplacing over something already constructed can bypass the destructor, leading to a state that may corrupt memory integrity, and/or leak.
That said, extracting a pointer is usually an unsafe operation to begin with...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right @klickverbot it's pointer arithmetics and reinterpreting memory which are unsafe.
Using a reference would be cleaner solution though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, while emplace might be "memory safe", it is still a very dangerous to use function. By making emplace take a pointer as an argument, it means emplace cannot be used in a pure @safe
scope, unless some @trusted
function first provided the pointer. I think this is a good thing.
I will review it a second time when I find some time. |
Okay, I went out on a limb and merged this. While it is potentially a high-impact change (since There is some cleanup/further fixes left to do (see e.g. the comments – @monarchdodra, are there issues for those?), but this fixes a host of difficult to track down bugs, and we absolutely need to ship a fix for those soon. |
Thankyou @klickverbot. A bold move, but I think it was the right move. As you saw, this fixes a couple of bugs. I'm now doing some follow up, and fixing things that depended on emplace being correct. I just opened 2 new pulls:
|
else | ||
{ | ||
static immutable T i; | ||
()@trusted{memcpy(chunk, &i, T.sizeof);}(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We discussed compiler optimization, so how about (cast(ubyte*)chunk)[0..T.sizeof] = (cast(ubyte*)&i)[0..T.sizeof]
? It will be rewritten as memcpy but the compiler might directly copy small arrays.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or better yet ?
enum N = T.sizeof;
(*cast(ubyte[N]*)chunk) = (*cast(ubyte[N]*)&i);
This statically calls static array copy. I'm no assembler expert, but I'd be curious to compare the generated asm.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I also thought of this after posting. It looks slightly better (and saves a few keystrokes to type) but in both cases the compiler has the same knowledge, it's copying array with constant boundaries.
Also note that dmd doesn't use this, it will simply call memcpy
.
It should be fairly simple to add an optimization because the backend already has an IR elem OPmemcpy
and also directly uses rep movsq
for struct blitting.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So what is your recommendation? Leave it as memcpy
, or, cast to ubyte[N]
?
If you think we should be casting, then it might be worth writing a helper void binaryBlit(T)(T* chunk, ref S s)
function. It might be worth writing it either way, as the emplace implementations use this a lot, it would consolidate it to whatever we choose to do.
Sorry, but I have to write it again. It is obvious for me for ages that |
Also I'm sure this opinion is already proven by (tons of?) fundamental error in e.g. Phobos ranges algorithms because of So I sincerely ask to think about it those who care about D. |
Could you elaborate how it is broken "by design" ? There is, to my knowlege, no more broken cases with emplace (bar using static arrays, which I am currently fixing). If you do now of a broken use case, please share it. |
as I wrote:
|
Builds a |
How do you define "builds"? |
As for |
Because it's been broken for months, with no improvements in sight. Having a correct
emplace
is (IMO) critical. I'm opening this again (3rd time)I wrote this fix, which is
Fixes:
emplace(void)
implementation for static arrays.emplace(args)
for static arrays....phew ! And unittests for all of that, of course. Did I miss anything?
Also, can work in safe and nothrow code! And sometimes CTFE to boot (yay).
Has a deprecated branch for calling opCall: arguably, opCall is not construction, so it should not be supported. However, the old implementation allowed opCall, so I added a deprecated branch.
Finally: ALWAYS diagnoses illegal args at top level with a verbose explanation for special cases (EG, no internal errors).
In regards to context pointer, it is copied when available. Otherwise, emplace will initialize it to null. This is kind of ugly, but it consistent behavior with dealing with voldemort types (such as when they are constructed inside any template).
PLEASE PLEASE PLEASE take the time to review this.
This isn't some small bug fix, or some petty performance improvement. This needs to make it into phobos.
There may be ways to optimize certain branches (thinking static array default emplacement), but correctness trumps efficiency at this point...
If you have any doubts, please voice them, as much as you can. I can justify the behavior (or lack thereof) of everything in this pull.