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 issue 15293 #3802
fix issue 15293 #3802
Conversation
|
ping @rainers |
|
if (buf.length >= pos + n) // buf is already large enough | ||
return; | ||
|
||
if (buf.capacity >= pos + n) |
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 is going to kill performance. A call to capacity
means lookup of the metadata in the GC. reserve(1)
is called for every character added.
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 think you can fix this by doing this instead:
auto curCap = buf.capacity
if(curCap >= pos + n)
{
buf.length = curCap
newBuf = true
}
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.
Setting buf.length
to the capacity makes sense to me.
But why set newBuf = true
? newBuf
is supposed to be true only if the buffer has been allocated by ReadlnAppender
. But a buffer from outside may have capacity, too.
Ah, ok. I forgot that. |
char[] nbuf = new char[ncap]; | ||
memcpy(nbuf.ptr, buf.ptr, pos); | ||
cap = nbuf.capacity; | ||
buf = nbuf.ptr[0 .. buf.length]; // remember initial length |
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 isn't right. The original code ignored buf.length and used cap. You are using buf.length for what cap did. So this essentially allocates no new capacity.
I think you want buf = nbuf
I'm having second thoughts on this opinion. I didn't realize that the other versions (non ReadlnAppender) didn't care about the returned buffer's appending status. I'm thinking that this update (after issues noted above) will be acceptable, because readln isn't meant for looping (and when it is, you can use the same mechanism as byLine, which should be high performance). |
immutable curCap = buf.capacity; | ||
if (curCap >= pos + n) | ||
{ | ||
buf.length = curCap; |
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.
@schveiguy: If you think newBuf = true;
should be here, please elaborate.
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.
Sure.
I look at it this way:
buf
is not big enough to hold the data, so we must expand- Since
buf.capacity
is non-zero, we know (or we can safely assume) that the data afterbuf
is unused. - When we expand
buf
to its capacity, it's likely we will not use all the data, so the extra data we claim here for expanded capacity will be garbage if we don'tassumeSafeAppend
.
So if you set newBuf = true
, then assumeSafeAppend
will be called at the end, and the data we claimed but didn't use is still available for appending. Effectively it's the same thing as your original implementation, but without the capacity check in the inner loop.
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 think I got it. In this scenario, the slice that's returned from data
is guaranteed to be longer than the originally passed buffer. So it's safe to say that any capacity we didn't use is free for all. Amending.
Amended fixes for the noted issues. |
ReadlnAppender tried to claim the capacity of the passed buffer, calling assumeSafeAppend on the result so that on the next call it has a capacity again that can be claimed. The obvious problem with that: readln would stomp over memory that it has not been given. There was also a subtler problem with it (which caused issue 15293): When readln wasn't called with the previous line, but with the original buffer (byLine does that), then the passed buffer had no capacity, so ReadlnAppender would not assumeSafeAppend when slicing the new line from it. But without a new assumeSafeAppend, the last one would still be in effect, possibly on a sub slice of the new line.
Amended the |
LGTM. I think this is actually going to end up making @rainers please review, this changes the memory usage semantics of |
assert(pos == 0); // assume this is the only put call | ||
if (b.length > cap) | ||
if (b.length > max(buf.length, buf.capacity)) |
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 think you should take advantage of the case b.length <= buf.length
to avoid getting the capacity at all. This probably boils down to just calling putbuf unconditionally.
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.
How about extracting a reserveWithoutAllocating
from reserve
that returns false instead of allocating a new buffer. And then change putonly
to this:
void putonly(char[] b)
{
assert(pos == 0); // assume this is the only put call
if (reserveWithoutAllocating(b.length))
memcpy(buf.ptr + pos, b.ptr, b.length);
else
buf = b.dup;
pos = b.length;
}
No .capacity
call when buf.length
is sufficient, at most one .capacity
call. Tight allocation with putonly
(not sure if this is important, but it seems to be the point of putonly
), spacious allocation with putchar
.
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.
Done that.
I agree the test case is valid. With a slightly more elaborate GC, that reclaims memory "freed" by assumeSafeAppend, it might reuse memory still referenced by other slices. It just took 8 month for #2794 to be merged, and it was a more obvious memory corruption. It wasn't me who insisted on performance over correctness, but that was a concern to others, so you might want to check the benchmark given there to see the actual change in performance by this PR. |
no optimization flags: -release -O -inline: Base is 2.069.0. Tested in wine, not proper Windows. |
Here are some numbers from my Windows system
GC activity happens in the readln1 test case only. We can estimate the number of actually allocated memory by multiplying the number of GC runs (-1 for the final run by the runtime) by the size of the pool (1MB). My test file has a size of 14 MB. |
void testReadln2()
{
auto f = File(filename);
size_t n = 0;
StopWatch sw;
sw.start();
char storage[200];
char buf = storage[]
while(!f.eof())
{
char[] ln = buf;
f.readln(ln);
n += ln.length;
if(ln.length > buf.length) buf = ln;
}
auto dur = sw.peek();
writefln("readln2: %d ms", dur.to!("msecs", int));
} I agree with the changes made, still LGTM. |
Note that the most obvious problem was |
Sure, but you have to know pretty much the internals of readln to do it. The test cases are rather trying the "obvious" approaches which were considered necessary to be fast. |
Not really. If Seems like we agree it's worth merging, so I'll do so. |
Auto-merge toggled on |
ReadlnAppender tried to claim the capacity of the passed buffer, calling
assumeSafeAppend on the result so that on the next call it has a capacity
again that can be claimed.
The obvious problem with that: readln would stomp over memory that it has
not been given.
There was also a subtler problem with it (which caused issue 15293):
When readln wasn't called with the previous line, but with the original
buffer (byLine does that), then the passed buffer had no capacity, so
ReadlnAppender would not assumeSafeAppend when slicing the new line from
it. But without a new assumeSafeAppend, the last one would still be in
effect, possibly on a sub slice of the new line.
https://issues.dlang.org/show_bug.cgi?id=15293