Skip to content

Reading large fields asynchronously is extremely slow #245

@roji

Description

@roji

Reading a large (e.g. 5MB) string with SqlDataReader is 10 times slower than doing it synchronously (see BDN benchmark below); at the same time, it allocates much less memory. The same behavior reproduces both with CommandBehavior.Default and CommandBehavior.SequentialAccess, although I suspect there may be some interaction with the command behavior under the hood. Memory-wise, the expectation would be for SequentialAccess to use much less memory memory compared to the default command behavior - regardless of sync/async.

The tests were on run with .NET Core 3.0 on Ubuntu 19.04 against SQL Server running on localhost.

Related links:

Benchmark code:

[MemoryDiagnoser]
public class Benchmarks
{
    const string ConnectionString = "...";

    [Params(CommandBehavior.Default, CommandBehavior.SequentialAccess)]
    public CommandBehavior Behavior { get; set; }

    [GlobalSetup]
    public void Setup()
    {
        using var conn = new SqlConnection(ConnectionString);
        conn.Open();

        using (var cmd = new SqlCommand("IF NOT EXISTS (SELECT * FROM sysobjects WHERE name='TextTable' AND xtype='U') CREATE TABLE [TextTable] ([Text] VARCHAR(MAX))", conn))
            cmd.ExecuteNonQuery();

        using (var cmd = new SqlCommand("INSERT INTO [TextTable] ([Text]) VALUES (@p)", conn))
        {
            cmd.Parameters.AddWithValue("p", new string('x', 1024 * 1024 * 5));
            cmd.ExecuteNonQuery();
        }
    }

    [Benchmark]
    public async ValueTask<int> Async()
    {
        using var conn = new SqlConnection(ConnectionString);
        using var cmd = new SqlCommand("SELECT [Text] FROM [TextTable]", conn);
        await conn.OpenAsync();

        await using var reader = await cmd.ExecuteReaderAsync(Behavior);
        await reader.ReadAsync();
        return (await reader.GetFieldValueAsync<string>(0)).Length;
    }

    [Benchmark]
    public async ValueTask<int> Sync()
    {
        using var conn = new SqlConnection(ConnectionString);
        using var cmd = new SqlCommand("SELECT [Text] FROM [TextTable]", conn);
        conn.Open();

        using var reader = cmd.ExecuteReader(Behavior);
        reader.Read();
        return reader.GetFieldValue<string>(0).Length;
    }
}

Results:

BenchmarkDotNet=v0.11.5, OS=ubuntu 19.04
Intel Core i7-6700HQ CPU 2.60GHz (Skylake), 1 CPU, 8 logical and 4 physical cores
.NET Core SDK=3.0.100
  [Host]     : .NET Core 3.0.0 (CoreCLR 4.700.19.46205, CoreFX 4.700.19.46214), 64bit RyuJIT
  DefaultJob : .NET Core 3.0.0 (CoreCLR 4.700.19.46205, CoreFX 4.700.19.46214), 64bit RyuJIT

Method Behavior Mean Error StdDev Median Gen 0 Gen 1 Gen 2 Allocated
Async Default 839.58 ms 43.2838 ms 127.6233 ms 930.22 ms 98000.0000 97000.0000 96000.0000 11.31 KB
Sync Default 61.07 ms 1.0988 ms 1.0278 ms 60.80 ms 333.3333 222.2222 222.2222 10600.7 KB
Async SequentialAccess 813.80 ms 44.3059 ms 130.6370 ms 767.23 ms 127000.0000 126000.0000 125000.0000 11.31 KB
Sync SequentialAccess 69.54 ms 0.6815 ms 0.6375 ms 69.45 ms 375.0000 250.0000 250.0000 10672.5 KB

Metadata

Metadata

Assignees

No one assigned

    Labels

    Performance 📈Issues that are targeted to performance improvements.

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions