Voron Performance, the single biggest booster
One of the surprising points for improvement in our performance run was the following logic, responsible for copying the data from the user to our own memory:
1: using (var ums = new UnmanagedMemoryStream(pos, value.Length, value.Length, FileAccess.ReadWrite))2: {
3: value.CopyTo(ums);4: }
Those three lines of code were responsible for no less than 25% of our performance. It was obvious that something needed to be done. My belief is that the unmanaged memory stream is just not optimized for this scenario, resulting in a lot of copying, allocations and costs.
Here is what we did instead. We create a temporary space that is allocated once, like this:
You can see that we are doing some interesting stuff there. In particular, we are allocated a managed buffer, but also force the GC to pin it. We keep this around for the entire lifetime of the database, too. The idea here is that we want to avoid the cost of pinning & unpinning it all the time, even if it means that we have an unmovable memory.
At any rate that important thing about this is that it gives us access to the same memory from managed and unmanaged perspectives. And that, in turn, leads to the following code:
We first read the values from the stream into the managed buffer, then copy them from the unmanaged pointer to the buffer to our own memory.
The idea here is that we accept a Stream abstraction, and that can only work with managed buffers, so we have to go through this route, instead of having to copy the memory directly. The reason we do that is that we don’t want to force the user of our API to materialize the data fully. We want to be able to stream it into the database.
At any rate, this has made some serious improvement to our performance, but I’ll be showing the details on a future post.
Comments
Why TempPagePointer is just a byte* ?
Ohh... delete this question : )
Hi,
Have you considered what will happen over time (e.g. over many GC cycles) if you have a lot of these pinned memory segments inside your managed heap?
--larsw
Lars, There is just one such temporary page for the entire database.
This is exactly the kind of performance analysis and optimisation we should be teaching:
Any other kind of "optimisation" is just hand-waving.
I shall be using this article as a concise example to others on the matter.
Two observations: 1) you implement a read loop on a size (AbstractPager.PageSize) that is the same as the buffer length of the TemporaryPage object. 2) You should probably call GC.AddMemoryPresure in the ctor and GC.ReleaseMemoryPresure in the Dispose of the TemporaryPage class when the page size is "large" (whatever that is).
Marc, 1) I don't follow your first point. Is there something specific that you were trying to say? 2) There is no additional memory being allocated here.
Simon, Let assume that I start a read transaction (RT-1), which starts reading from the page table. Then we have a write transaction, that modify the page table. RT-1 is still operating, and needs to see the same page table that it had when it started.
1) You use a while(true) loop to block-Read the content of value ...? Doesn't the Read call always return 0 on the second pass? If so, the while loop is unnecessary. If not, I would expect value.Length for the count parameter in the Read method call...
2) You're right, it managed memory - I thought it was unmanaged. ;-)
1) Read() is freed to read LESS than the buffer size. The only contract it gives is that 0 means no more data. Simplest scenario, consider the case where you have a value that is 6000 bytes long.
Comment preview