Ayende @ Rahien

It's a girl

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:

image

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:

image

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

krlm
12/19/2013 10:21 AM by
krlm

Why TempPagePointer is just a byte* ?

krlm
12/19/2013 10:23 AM by
krlm

Ohh... delete this question : )

Lars Wilhelmsen
12/19/2013 10:52 AM by
Lars Wilhelmsen

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

Ayende Rahien
12/19/2013 11:00 AM by
Ayende Rahien

Lars, There is just one such temporary page for the entire database.

Paul Turner
12/19/2013 01:07 PM by
Paul Turner

This is exactly the kind of performance analysis and optimisation we should be teaching:

  1. Identify the part of an operation which is slowest by measuring.
  2. Replace a general-purpose component with something tailored to the specifics of the situation at hand.

Any other kind of "optimisation" is just hand-waving.

I shall be using this article as a concise example to others on the matter.

Marc Jacobi
12/19/2013 02:37 PM by
Marc Jacobi

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).

Ayende Rahien
12/20/2013 08:36 AM by
Ayende Rahien

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.

Ayende Rahien
12/20/2013 08:37 AM by
Ayende Rahien

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.

Marc Jacobi
12/20/2013 11:51 AM by
Marc Jacobi

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. ;-)

Ayende Rahien
12/20/2013 11:57 AM by
Ayende Rahien

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.

Comments have been closed on this topic.