Login Skip Navigation LinksWilsonORMapper > Forums Search
Demo Version Demo Version
Download and try for yourself a fully working demo version, including sample apps and documentation.  The only limitation is that the demo version only works inside the debugger.

PayPal Subscribe
Get It All for $50 USD:
WebPortal, ORMapper,
Source Code, All Updates
PayPal

User Login User Login
Log In
 
 
Reset Password

Wilson ORMapper Forums Wilson ORMapper Forums : Chat & Share : Significant performance differences vs. NHibernate, thoughts?

Date Post
4/4/2007 2:48:23 PM

So, I've been investigating WORM vs NHibernate and think either would work for us. I like (very much) the simplicity of WORM (and find the complexity of NHibernate to be somewhat daunting), but NHibernate has somethings out of the box that I would have to modify WORM for (e.g. access to the mapping metadata that goes with an object).

I have run into a *huge* performance difference that I cannot  readily account for, though. Retrieving a significant number of objects from a SQL Server database can be 20-30 times faster in NHibernate. I'm using the Welter templates with WORM, and GenWise objects with NHibernate. 

Accessing a modestly wide table (40+ columns), NHibernate can retrieve 7000-8000 objects/second from a separate test SQL Server machine, returning 40,000 objects in about 5 seconds. The WORM+Welter combination takes over 2 minutes to retrieve the same object set, barely 300 objects/second.  With an Oracle database, the difference is less, but NHibernate is still 3 or more times faster, 800 objects/second versus WORM still being around 300.

Of course, this isn't something you would generally actually have to do in production, but I was just trying to weigh whether the "bloat" of NHibernate was a performance detriment. Also, the performance of WORM improves dramatically if fewer objects are retrieved. Speeds of 1000-1500 objects/second are attainable when 5000 or fewer objects are retrieved.

Note that this is not the database. The query is finished, and I can see the client CPU go to 100% while the result set is actually being loaded into the objects with both mappers. I also repeat the test several times with the same query to be sure that caching issues don't muddy the waters.

Any thoughts? All I can come up with is that NHibernate generates lightweight property accessors while IObjectHelper (in the Welter templates) uses a big switch statement that string compares 1/2 the properties on average just to find a single property, property loading is O(n) in NHibernate versus O(n x m). Maybe I'll try a hash table instead just to see if this makes a different in my Welter object. I can also turn off the optimizer in NHibernate and see what difference that makes.  

4/4/2007 3:54:17 PM Interesting to say the least.  Can you post your retrieval code?  Are you returning an ObjectSet or an ObjectReader?  I have not used NHibernate, so I don’t know if it has equivalents.  However this has for sure sparked my interests.
4/5/2007 3:02:41 PM This from the last version of the Welter templates (which have recently been redone to use a different pattern). Sorry about the reflection, but I'm basically trying to call the static "RetrievePage" method in the Welter templates for whichever object class I was testing, with a first row and number of rows. private void buttonGo_Click(object sender, EventArgs e) { int i = comboBoxClasses.SelectedIndex; DataManager.ObjectSpace.ClearTracking(); if (i >= 0) { richTextBoxMessage.Text = ""; string s = comboBoxClasses.Items[i] as string; string strFactoryTypeName = "" System.Type aType = System.Type.GetType(strFactoryTypeName, false, true); if (aType != null) { try { IList list; int FirstRow, NumberOfRows; if (!int.TryParse(textBoxFirstRow.Text, out FirstRow)) FirstRow = 0; if (!int.TryParse(textBoxCount.Text, out NumberOfRows)) NumberOfRows = 1000; DateTime start = DateTime.Now; list = aType.InvokeMember("RetrievePage", BindingFlags.FlattenHierarchy | BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.InvokeMethod, null, null, new object[] { FirstRow, NumberOfRows }) as IList; if (list != null) { DateTime finish = DateTime.Now; TimeSpan elapsed = finish - start; textBoxElapsed.Text = String.Format("{0:F3}", (double)elapsed.TotalMilliseconds / 1000); textBoxStartTime.Text = start.ToShortTimeString(); textBoxEndTime.Text = finish.ToShortTimeString(); textBoxRowsReturned.Text = list.Count.ToString(); textBoxRowsSecond.Text = string.Format("{0:F3}", ((double)list.Count * 1000) / elapsed.TotalMilliseconds).ToString(); } } catch (Exception ex) { richTextBoxMessage.Text = ex.Message; } } } } Here is the RetrievePage code, which appears to load an ObjectSet: public static Collection RetrievePage(int startRowIndex, int maximumRows, string sortClause) { int pageIndex = (int)Math.Floor((double)(startRowIndex-1) / maximumRows) + 1; ObjectQuery query = new ObjectQuery(string.Empty, sortClause, maximumRows, pageIndex, true); ObjectSet pageSet = RetrievePage(query, false); return pageSet; }
4/5/2007 3:04:50 PM Good grief. Posting code didn't work very well -- sorry about that. Anyway, it boils down to an unsorted GetObjectSet(), with the number of rows limited in the query.
4/5/2007 3:21:17 PM

I just tried turning off auto-tracking, and it didn't make much difference.

Performance goes down dramatically with result set size increase. I can retrieve almost 1500-1700 objects / second if I keep the result set size under 2000 objects, but it goes down steadily from there. 10,000 object can be retrieved in a little over 50 seconds (600/second), and down to 300/second for 40,000 rows.

I wonder if there is some sort of O(n) list access going on. I'll wade in a bit deeper.

4/5/2007 3:43:02 PM

Try returning an ObjectReader, just for curiosity's sake.

Travis

4/6/2007 10:13:12 AM

Here's what I know:

1.  My ORMapper was definitely optimized for small to medium resultsets intentionally.  I'm a firm believer that you should use database paging and users should only see small sets.  The cases where you must have more are typically for reports, when DataSets make more sense and will outperform objects anyhow (even NHibernate I would bet from my own tests), or you should be doing the processing on the server if its not reporting.

2.  My ORMapper does not have any of the .NET v2.0 light-weight reflection stuff in it since I've kept the same codebase so far for v1.1 and v2.0, with the except of generics and nullables.  NHibernate probably has added that, which would make a difference, and I know that the results you're seeing were typical of all O/R Mappers in v1.1, including NHibernate, which was again part of my reasoning to optimize towards smaller sets.

Personally, I seldom even both with the IObjectHelper interface at all.  I added it since it makes performance tests better, but in the real world with smaller and more realistic sets of data it just doesn't impact performance the way it can in large theoretical tests -- and my mapper is all about what works best in those real world situations.

Thanks, Paul Wilson

4/6/2007 10:57:54 AM

I don't why my previous post didn't show up about my having identified the difference, but I accounted for the performance observationsn in some addition tests yesterday:

1. My test with the Wilson ORMapper was using an ObjectSet, a richer indexed collection which is slightly more expensive to populate, so it wasn't a fair (e.g. "apples to apples") test.

2. Using a ObjectReader vs. NHibernate with it's reflection optimizer turned off, the Wilson ORMapper is actually slightly faster (within 10%, though).

3. NHibernate gets a 300% performance improvement with the optimizer on versus having it turned off, in case you were interested how much difference using the emitter makes.

4/8/2007 9:17:55 PM

OK, I found it. This really isn't a completely fair comparison after all, since ObjectSet has basically O(1) access by the key value and does does an expensive lookup to determine the index of the object at load time.

This line in ObjectSet_T.cs is the killer:

int index = this.Items.IndexOf(entityObject);

If I comment this line out, then I get object retrieval rates of 2000-2500/second, that don't decline with result set size. This is better.

I'll also try an ObjectReader.

4/20/2007 5:24:52 PM

For the time being, I have replaced the above code with this optimized version, which basically codes in the assumption that the newly added item will always be the last index.  Even with the additional sanity check afterwards, this now retrieves a steady 2000-2200 objects/second with no perceptible roll-off (eliminating the test adds maybe 100 objects/second). I don't know if you want to use this or not, but I thought I would offer it.

public void Add(object objectKey, T entityObject) {

this.Items.Add(entityObject);

int index = this.Items.Count - 1;

if (!entityObject.Equals(this.Items[index]))

      throw new ORMapperException("ObjectSet: Optimized objectkey index retrieval assertion failure");

this.keyValues.Add(objectKey, index);

}

4/21/2007 6:25:56 AM

Regarding the IObjectHelper stuff - if you look at the IL the compiler generates for the switch statement you'll see that the compiler emits code to load a static Dictionary<string, int> with the field names and a jump table index. When evaluating the switch arg, it looks up the string in the dictionary and jumps to the label indexed by the jump table index. So adding your own hashtable would simply replicate the code the compiler already generates for you.

I also found when profiling that when loading new rows from the database, the code calls DateTime.Now at least twice for each field in every row loaded. Since it was a non-trivial chunk of the time, I changed

return Instance.SetField(this.EntityObject, this.instance.Target, member, value, this.context.Provider);
to
return
Instance.SetField(this.entity, this.instance.Target, member, value, this.context.Provider);

in this SetField overload:

private object SetField(EntityMap entityMap, string member, object value)

The other big hitter when running the profiler seemed to be QueryHelper.ChangeType. Since IObjectHelper necessarily has to coerce types anyway, it's superfluous if you have an IObjectHelper implementation. I didn't remove it, but the data suggested it would be the next thing to work on from a performance point of view.

6/27/2007 8:47:52 PM

Thanks, hero without a name :)

I had exactly the same problem (and pinned down the cause to the same function) and your performance fix improved running time by about 20x in a worst case scenario a had here.

The code i have would normally only retrieve a handful records and performance was not a problem in that case, but it had to be prepared to still perform decently when there were several thousands. The original version of Add had a O(n*n) behaviour.

Gunter