Performance of ODB vs C# ORMs

April 6th, 2011

You might have heard the story of Stack Overflow switching to their own C# ORM framework because all the existing ones are too slow. Yesterday this framework was named Dapper and published as an open-source project. Dapper also includes a simple performance benchmark and the results for pretty much all the popular C# ORMs. This caught my attention and I thought, why not implement the same benchmark for ODB C++ ORM and see how it stacks up against the C# crowd?

In a nutshell, the idea of the benchmark is to measure the time it takes to pull 500 random post objects from the database. The post object here is meant to simulate a Stack Overflow question. Its C++ version is shown below:

#pragma db object
class post
{
public:
  #pragma db id
  unsigned long id;
 
  std::string text;
 
  boost::posix_time::ptime creation_date;
  boost::posix_time::ptime last_change_date;
 
  int counter1;
  int counter2;
  int counter3;
  int counter4;
  int counter5;
  int counter6;
  int counter7;
  int counter8;
  int counter9;
};

Once the above class is compiled with the ODB compiler, the resulting database schema looks like this, which is identical to the one used in Dapper’s benchmark:

CREATE TABLE post (
  id BIGINT UNSIGNED NOT NULL PRIMARY KEY,
  text TEXT NOT NULL,
  creation_date DATETIME,
  last_change_date DATETIME,
  counter1 INT NOT NULL,
  counter2 INT NOT NULL,
  counter3 INT NOT NULL,
  counter4 INT NOT NULL,
  counter5 INT NOT NULL,
  counter6 INT NOT NULL,
  counter7 INT NOT NULL,
  counter8 INT NOT NULL,
  counter9 INT NOT NULL)

First, the ODB benchmark loads 10000 objects into the database:

transaction t (db->begin ());
 
for (unsigned long i (0); i < total_objects; ++i)
{
  post p;
 
  p.id = i;
  p.text = text;
  p.creation_date = second_clock::local_time () -
    time_duration (i, 0, 0);
  p.last_change_date = second_clock::local_time ();
 
  p.counter1 = i + 1;
  p.counter2 = i + 2;
  p.counter3 = i + 3;
  p.counter4 = i + 4;
  p.counter5 = i + 5;
  p.counter6 = i + 6;
  p.counter7 = i + 7;
  p.counter8 = i + 8;
  p.counter9 = i + 9;
 
  db->persist (p);
}
 
t.commit ();

Then it runs the following iteration a couple of hundred times while measuring the time:

void
test (database& db)
{
  post p;
 
  transaction t (db.begin ());
 
  for (unsigned long i (0); i < 500; ++i)
  {
    unsigned long id (rand () % total_objects);
    db.load (id, p);
  }
 
  t.commit ();
}

I used the MySQL database server to run this benchmark (the C# test uses Microsoft SQL Server). In my case, both the database and the benchmark were running on the same multi-core, 64-bit Linux machine. As you can see on Dapper’s web site, the best performance one can get with C# is 47ms (hand-coded) per 500 iterations with Dapper coming second at 49ms. ODB does it in 24ms. Half the time of the hand-coded C# version is not bad! As a bonus, I also ran the ODB test with SQLite (just had to recompile with different options — no source code changes required). The same benchmark using SQLite takes 7ms (14μs per object)! Now, that is fast.

UPDATE: As was pointed out by a reader, it would be useful to know the hardware that was used for the benchmarks. Unfortunately, Dapper’s results don’t mention the test hardware.ODB results are for a Xeon E5520 2.27GHz machine.

ODB 1.3.0 released

April 6th, 2011

ODB 1.3.0 was released today.

In case you are not familiar with ODB, it is an object-relational mapping (ORM) system for C++. It allows you to persist C++ objects to a relational database without having to deal with tables, columns, or SQL, or manually writing any of the mapping code.

As usual, for the complete list of changes see the official ODB 1.3.0 announcement. But the most important new feature in this release is no doubt support for the SQLite embedded database. Below I am going to examine this in more detail.

Support for SQLite is provided by the libodb-sqlite runtime library. Pretty much all the standard ODB functionality is available to you when using SQLite, including support for containers, object relationships, queries, date-time types in the Boost profile, etc. There are a few limitations, however, that are discussed in Chapter 11, “SQLite Database” in the ODB Manual.

If you request the generation of the database schema (--generate-schema ODB compiler option), then for SQLite, by default, the schema is embedded into the generated C++ code since most applications that use SQLite probably don’t want to carry a .sql file around. See the schema/embedded example in the odb-examples package for more information on how to use embedded schemas.

If you have used SQLite before, you are probably aware of the peculiar ways in which it manages access to the same database from multiple threads and processes. While more traditional database systems will make concurrent access pretty much transparent to the user (except for an occasional deadlock or timeout), SQLite simply returns an error if the database is used by someone else. There are some advanced mechanisms available, such as the shared cache mode and unlock notifications, which allow the user to implement more traditional (i.e., blocking) concurrent access to the database from within a multi-threaded program. But it is still quite a lot of low-level, non-trivial code that one has to write.

The good news is that ODB takes care of all these details and allows you to access the same database from multiple threads in the same way as you would with any other database system. For connection management, ODB provides three built-in connection factories (you can also provide your own if so desired): single_conection_factory, new_conection_factory, and conection_pool_factory.

The single connection factory shares a single connection among all the callers. So if one thread is using the connection, all the others requesting a connection will be blocked until it is done.

The new connection factory creates a new connection whenever one is requested. Once the connection is no longer needed, it is closed.

The connection pool factory maintains a pool of connections and you can specify the min and max connection counts for each pool created. This factory is the default choice when creating a database instance.

The new and pool factories are the best options for multi-threaded applications. By default they enable the SQLite shared cache mode and use the unlock notifications to aid concurrency.

Another SQLite-specific feature worth mentioning is support for starting immediate and exclusive SQLite transactions. This is accomplished with two additional odb::sqlite::database functions: begin_immediate() and begin_exclusive(). These functions are primarily useful for avoiding deadlocks.

If you would like to learn more about SQLite support in ODB, the best place to start is Chapter 11, “SQLite Database” in the ODB Manual.

ODB 1.2.0 released

March 16th, 2011

ODB 1.2.0 was released today. In case you are not familiar with ODB, it is an object-relational mapping (ORM) system for C++. It allows you to persist C++ objects to a relational database without having to deal with tables, columns, or SQL, or manually writing any of the mapping code.

This version includes a number of major new features, small improvements, and bug fixes. For an exhaustive list of changes see the official ODB 1.2.0 release announcement. In this post I am going to examine the most notable new features in more detail.

But before we move to the technical matters, I would like to make a different kind of announcement: Constantin Michael has joined the ODB development team. For this release he focused on the Boost profile, discussed next.

Boost profile

You might have noticed that the previous release added profiles as a new feature. We didn’t discuss it much then because there were no profile implementations available at that time. It was just the ODB compiler infrastructure that we were preparing for the first profile implementation. Well, the 1.2.0 release adds the first profile implementation to ODB: the Boost profile.

ODB profiles are a generic mechanism for integrating ODB with widely-used C++ frameworks and libraries. A profile provides glue code which allows us to seamlessly persist various components, such as smart pointers, containers, and value types found in these frameworks and libraries.

In this initial release the Boost profile covers the most commonly used types from the smart_ptr, unordered, and date_time Boost libraries. For example, now we can write:

#pragma db object
class employee
{
  ...
 
  boost::gregorian::date born_;
  boost::unordered_set<std::string> emails_;
  boost::shared_ptr<employer> employer_;
};

As is evident from the code fragment above, we don’t need to do anything special to use Boost types in our persistent classes. Are there any other actions that we need to perform for the above code to work?

As mentioned above, ODB profiles are a generic mechanism that can be used to integrate ODB with a type-system of any third-party library. For example, we are currently working on another profile, this time for the Qt framework. So the ODB compiler and its runtime library don’t know anything about, say, Boost or Qt. Rather, they provide general integration support. The code that is necessary to implement a profile is packaged into a separate library called a profile library. The Boost profile implementation is provided by the libodb-boost profile library.

Now, back to our question. What do we need to do to be able to write the above code in an application that uses ODB? There are three simple steps:

  1. Download and build the profile library.
  2. Specify the profile you would like to use when invoking the ODB compiler. For example:
    odb -d mysql --profile boost employee.hxx
    
  3. Link the profile library to your application.

And that’s it. That’s all we need to do. For more detailed information on ODB profiles in general refer to Chapter 11, “Profiles Introduction” in the ODB Manual. For more information on the Boost profile see Chapter 12, “Boost Profile”.

Embedded database schema

ODB now supports embedding the database schema into the generated C++ code in addition to generating the schema as a standalone SQL file. The new ODB compiler option that allows you to select between the two approaches is --schema-format. For example:

odb -d mysql --generate-schema --schema-format embedded ...

The API usage for creating the database schema from within the application looks like this:

#include <odb/schema-catalog.hxx>
 
database& db = ...
 
transaction t (db.begin ());
schema_catalog::create_schema (db);
t.commit ();

For more details refer to Section 3.3, “Database” in the ODB Manual as well as the schema/embedded example in the odb-examples package.

There are also other important features in this release, including, transparent database reconnection, recoverable exceptions (connection_lost, timeout, and deadlock), and support for the default ODB compiler options file which can be used for installation-wide customizations. Refer to the official ODB 1.2.0 release announcement for more details on these and other features.