diff --git a/DuckDB.NET.Benchmarks/AppenderBenchmark.cs b/DuckDB.NET.Benchmarks/AppenderBenchmark.cs new file mode 100644 index 0000000..36f918e --- /dev/null +++ b/DuckDB.NET.Benchmarks/AppenderBenchmark.cs @@ -0,0 +1,50 @@ +using BenchmarkDotNet.Attributes; +using DuckDB.NET.Data; + +namespace DuckDB.NET.Benchmarks; + +[MemoryDiagnoser] +public class AppenderBenchmark +{ + private DuckDBConnection connection = null!; + + [Params(1_000_000)] + public int RowCount { get; set; } + + [GlobalSetup] + public void Setup() + { + connection = new DuckDBConnection("DataSource=:memory:"); + connection.Open(); + } + + [GlobalCleanup] + public void Cleanup() + { + connection.Dispose(); + } + + [IterationSetup] + public void IterationSetup() + { + using var command = connection.CreateCommand(); + command.CommandText = "DROP TABLE IF EXISTS bench; CREATE TABLE bench (a INTEGER, b BIGINT, c DOUBLE, d BOOLEAN);"; + command.ExecuteNonQuery(); + } + + [Benchmark] + public void AppendRows() + { + using var appender = connection.CreateAppender("bench"); + + for (var i = 0; i < RowCount; i++) + { + appender.CreateRow() + .AppendValue(i) + .AppendValue((long)i) + .AppendValue((double)i) + .AppendValue(i % 2 == 0) + .EndRow(); + } + } +} diff --git a/DuckDB.NET.Benchmarks/Benchmarks.csproj b/DuckDB.NET.Benchmarks/Benchmarks.csproj new file mode 100644 index 0000000..319ca74 --- /dev/null +++ b/DuckDB.NET.Benchmarks/Benchmarks.csproj @@ -0,0 +1,34 @@ + + + + Exe + net10.0 + net10.0 + enable + enable + true + true + Full + false + + + + + + + + + + + + + + + false + PreserveNewest + runtimes\%(RecursiveDir)\%(FileName)%(Extension) + + + + diff --git a/DuckDB.NET.Benchmarks/NativeLibraryLoader.cs b/DuckDB.NET.Benchmarks/NativeLibraryLoader.cs new file mode 100644 index 0000000..a253796 --- /dev/null +++ b/DuckDB.NET.Benchmarks/NativeLibraryLoader.cs @@ -0,0 +1,45 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace DuckDB.NET.Benchmarks; + +internal static class NativeLibraryLoader +{ + [ModuleInitializer] + public static void Init() + { + if (GetRid() is not { } rid) + { + return; + } + + _ = NativeLibrary.TryLoad(Path.Join("runtimes", rid, "native", "duckdb"), Assembly.GetExecutingAssembly(), DllImportSearchPath.AssemblyDirectory, out _) || + NativeLibrary.TryLoad(Path.Join("runtimes", rid, "native", "libduckdb"), Assembly.GetExecutingAssembly(), DllImportSearchPath.AssemblyDirectory, out _); + } + + private static string? GetRid() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return Environment.Is64BitProcess ? "win-x64" : "win-x86"; + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + return RuntimeInformation.ProcessArchitecture switch + { + Architecture.X64 => "linux-x64", + Architecture.Arm64 => "linux-arm64", + _ => null, + }; + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + return "osx"; + } + + return null; + } +} diff --git a/DuckDB.NET.Benchmarks/Program.cs b/DuckDB.NET.Benchmarks/Program.cs new file mode 100644 index 0000000..4e31a4d --- /dev/null +++ b/DuckDB.NET.Benchmarks/Program.cs @@ -0,0 +1,15 @@ +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Jobs; +using BenchmarkDotNet.Running; +using BenchmarkDotNet.Toolchains.InProcess.Emit; +using DuckDB.NET.Benchmarks; + +// The repo's Directory.Build.props renames the output assembly to DuckDB.NET.Benchmarks +// while the project file stays Benchmarks.csproj, so BenchmarkDotNet's default toolchain +// can't locate the csproj. Run in-process to avoid the separate build/spawn step. +var config = DefaultConfig.Instance + .AddJob(Job.Default.WithToolchain(InProcessEmitToolchain.Instance)); + +BenchmarkSwitcher + .FromTypes([typeof(AppenderBenchmark)]) + .Run(args, config); diff --git a/DuckDB.NET.Data/DuckDBAppender.cs b/DuckDB.NET.Data/DuckDBAppender.cs index d32e3ca..720d225 100644 --- a/DuckDB.NET.Data/DuckDBAppender.cs +++ b/DuckDB.NET.Data/DuckDBAppender.cs @@ -17,6 +17,7 @@ public class DuckDBAppender : IDisposable private readonly DuckDBLogicalType[] logicalTypes; private readonly DuckDBDataChunk dataChunk; private readonly VectorDataWriterBase[] vectorWriters; + private DuckDBAppenderRow? currentRow; internal DuckDBAppender(Native.DuckDBAppender appender, string qualifiedTableName) { @@ -60,7 +61,19 @@ public IDuckDBAppenderRow CreateRow() } rowCount++; - return new DuckDBAppenderRow(qualifiedTableName, vectorWriters, rowCount - 1, dataChunk, nativeAppender); + + // A single row is filled and ended before the next CreateRow() call, so reuse one instance + // instead of allocating a DuckDBAppenderRow per row. + if (currentRow is null) + { + currentRow = new DuckDBAppenderRow(qualifiedTableName, vectorWriters, rowCount - 1, dataChunk, nativeAppender); + } + else + { + currentRow.Reset(rowCount - 1); + } + + return currentRow; } public void Clear() diff --git a/DuckDB.NET.Data/DuckDBAppenderRow.cs b/DuckDB.NET.Data/DuckDBAppenderRow.cs index 186ab69..1881cbe 100644 --- a/DuckDB.NET.Data/DuckDBAppenderRow.cs +++ b/DuckDB.NET.Data/DuckDBAppenderRow.cs @@ -7,7 +7,7 @@ public class DuckDBAppenderRow : IDuckDBAppenderRow private int columnIndex = 0; private readonly string qualifiedTableName; private readonly VectorDataWriterBase[] vectorWriters; - private readonly ulong rowIndex; + private ulong rowIndex; private readonly DuckDBDataChunk dataChunk; private readonly Native.DuckDBAppender nativeAppender; @@ -21,6 +21,18 @@ internal DuckDBAppenderRow(string qualifiedTableName, VectorDataWriterBase[] vec this.nativeAppender = nativeAppender; } + /// + /// Re-targets this row instance at a new row index so the appender can reuse a single + /// instead of allocating one per row. The table name, vector + /// writers, data chunk and native appender are stable for the lifetime of the appender, so only + /// the row index and column cursor need to be reset. + /// + internal void Reset(ulong rowIndex) + { + this.rowIndex = rowIndex; + columnIndex = 0; + } + public void EndRow() { if (columnIndex < vectorWriters.Length) diff --git a/DuckDB.NET.slnx b/DuckDB.NET.slnx index 715e640..b40a6d6 100644 --- a/DuckDB.NET.slnx +++ b/DuckDB.NET.slnx @@ -1,4 +1,5 @@ +