From b208fcee2e016d8f800b4c7ff3d2c3060d338059 Mon Sep 17 00:00:00 2001 From: Skuirrels Date: Sat, 11 Jul 2026 00:21:07 +0100 Subject: [PATCH] Skip Convert.ChangeType on the same-type object read path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NumericVectorDataReader.GetValue(offset, targetType) backs the non-generic read surface (GetValue(ordinal), this[ordinal], GetValues). It always ran the boxed value through Convert.ChangeType, even when targetType already matched the column's natural type — the common case, since those APIs read using the column's ClrType. Convert.ChangeType does not early-out on same-type for IConvertible values: it dispatches through ToXxx() and re-boxes the result, so every value was boxed twice. Add a value.GetType() == targetType fast path that returns the already boxed value directly. Benchmark (read 1,000,000 rows x 3 numeric columns via GetValue, .NET 10, M4 Pro): Before: 54.50 ms, 137.3 MB allocated After: 22.73 ms, 68.7 MB allocated -58% time and -50% allocations on the object-returning path (the remaining allocation is the unavoidable single box of the object API). The typed GetFieldValue path is unaffected. Behavior is unchanged: ChangeType returned the same value for same-type, and BigInteger (not IConvertible) already used this same early-out inside ChangeType. --- .../DataChunk/Reader/NumericVectorDataReader.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/DuckDB.NET.Data/DataChunk/Reader/NumericVectorDataReader.cs b/DuckDB.NET.Data/DataChunk/Reader/NumericVectorDataReader.cs index 9da4bea6..caefd9bc 100644 --- a/DuckDB.NET.Data/DataChunk/Reader/NumericVectorDataReader.cs +++ b/DuckDB.NET.Data/DataChunk/Reader/NumericVectorDataReader.cs @@ -71,6 +71,15 @@ internal override object GetValue(ulong offset, Type targetType) _ => base.GetValue(offset, targetType) }; + // Fast path: when the boxed value already has the requested type (the common case for + // GetValue(ordinal)/this[ordinal], which read using the column's natural ClrType), skip the + // Convert.ChangeType machinery entirely. ChangeType does not early-out on same-type for + // IConvertible values — it re-boxes via ToXxx() — so this also removes a redundant box. + if (value.GetType() == targetType) + { + return value; + } + if (targetType.IsNumeric()) { try