With the release of Visual Studio 2008 and the .NET Framework 2.0 SP1, there are some bug fixes and improvements in ADO.NET (A list of the general fixes is at http://support.microsoft.com/kb/945757)

We worked with the CLR team to get improvements like the thread pool improved scalability, and that was important for making sure our SqlReader performance is better in multithreaded scenarios (like your ASP.NET applications under load).

Particularly improved were scenarios like Data set insertion, and multi-threaded scenarios (such as ASP.NET applications) using SqlDataReader (25% better or more, depending on hardware and OS architecture).

For a complex real world web application, like Petshop, the improvement is not so big, since the application does many other things in addition to data access, but still you should get part of the benefits.

A summary of our lab numbers, just as a reference of how much improvement you can get:

Scenario .NET FW 2.0 .NET FW 2.0 SP1 Improvement
SqlReader 14855 18100 27.3%
DataSet insert 9637 12890 40.8%
Pet Shop 4.0(Browse the store) 22.44 24.40 8.72%
Pet Shop 4.0 (Buy some pets) 21.54 23.04 6.99%

* Average throughput, over a set of runs, on 4 proc dual core servers with W2K3 SP1, 16 Gb. of RAM, with a sample average load (not by far peak capacity). Hardware differences, network conditions and the way your scenario is written affect performance, among many other things, so your mileage may vary.

You can find the code for Petshop here, and here I paste a sample of code similar to our main test loop (which is run in multiple threads, by our test harness. Remember that most SqlClient classes are not thread-safe, so your code shouldn't use the same object in two different threads):

public void SqlReader() {
    using (SqlConnection conn = new SqlConnection(_connectionString)) {
        conn.Open();
        SqlCommand cmd = new SqlCommand(_sql, conn);
        using (SqlDataReader reader = cmd.ExecuteReader()) {
            int fields = reader.FieldCount;
            while (reader.Read()) {
                for (int i = 0; i < fields; i++) {
                    object obj = reader.GetValue(i);
                }
            }
        }
    }
}

public void DataSetInsert() {
    SqlConnection conn = new SqlConnection(_connectionString);
    DataSet ds = new DataSet();
    SqlDataAdapter adp = new SqlDataAdapter(_sql, conn);
    adp.Fill(ds, "atable");
}