• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Giorgi / DuckDB.NET / 39571241390

21 Oct 2026 08:41AM UTC coverage: 92.79% (-0.01%) from 92.801%
39571241390

push

github

Giorgi
Decode a GetChars value once and count its length without decoding

GetChars decoded the value rune by rune from its start on every call, so
reading a large value in chunks took time that grows with the square of
its length. It now decodes the value once with GetString, keeps it for
later calls on the same row, and copies from it. A null buffer counts
the UTF-16 length from the UTF-8 bytes without decoding them.

Measured on the benchmark VM (net10.0, 1M characters, one read from
nothing kept): chunked reads in 4096 characters went from 113 ms to
0.2 ms (ASCII) and from 184 ms to 2.8 ms (mixed widths). A single whole
read of mixed-width text is 1.38 times slower, because GetString builds
the string the old code did not.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F9b3CGWkCMzmhoxge1QiZ8

2313 of 2564 branches covered (90.21%)

Branch coverage included in aggregate %.

38 of 42 new or added lines in 1 file covered. (90.48%)

2 existing lines in 1 file now uncovered.

4032 of 4274 relevant lines covered (94.34%)

970826.32 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.36
/DuckDB.NET.Data/DataChunk/Reader/StringVectorDataReader.cs
1
using System.IO;
2
using System.Runtime.CompilerServices;
3
using System.Runtime.InteropServices;
4
using System.Runtime.Intrinsics;
5
using System.Text;
6

7
namespace DuckDB.NET.Data.DataChunk.Reader;
8

9
internal sealed class StringVectorDataReader : VectorDataReaderBase
10
{
11
    internal unsafe StringVectorDataReader(void* dataPointer, ulong* validityMaskPointer, DuckDBType columnType, string columnName) : base(dataPointer, validityMaskPointer, columnType, columnName)
3,600 ✔
12
    {
13
    }
3,600 ✔
14

15
    internal override string ReadString(ulong offset) => GetValidValue<string>(offset);
177,969 ✔
16
    internal override Stream ReadStream(ulong offset) => GetValidValue<Stream>(offset);
51 ✔
17

18
    protected override T GetValidValue<T>(ulong offset)
19
    {
20
        return DuckDBType switch
178,050 !
21
        {
178,050 ✔
22
            DuckDBType.Bit => GetBitString<T>(offset),
42 ✔
23
            DuckDBType.Blob => (T)(object)GetStream(offset),
51 ✔
24
            DuckDBType.Varchar => (T)(object)GetString(offset),
177,957 ✔
25
            _ => base.GetValidValue<T>(offset)
×
26
        };
178,050 ✔
27
    }
28

29
    internal override object GetValue(ulong offset, Type targetType)
30
    {
31
        return DuckDBType switch
1,002,010 !
32
        {
1,002,010 ✔
33
            DuckDBType.Bit => GetBitString(offset),
39 ✔
34
            DuckDBType.Blob => GetStream(offset),
6 ✔
35
            DuckDBType.Varchar => GetString(offset),
1,001,965 ✔
36
            _ => base.GetValue(offset, targetType)
×
37
        };
1,002,010 ✔
38
    }
39

40
    private T GetBitString<T>(ulong offset)
41
    {
42
        if (typeof(T) == typeof(string))
42 ✔
43
        {
44
            return (T)(object)GetBitString(offset);
12 ✔
45
        }
46

47
        if (typeof(T) == typeof(BitArray))
30 ✔
48
        {
49
            return (T)(object)GetBitStringAsBitArray(offset);
3 ✔
50
        }
51

52
        //Not GetValueStrict<T>: that comes back to this method
53
        return base.GetValidValue<T>(offset);
27 ✔
54
    }
55

56
    private string GetBitString(ulong offset)
57
    {
58
        var bitArray = GetBitStringAsBitArray(offset);
51 ✔
59

60
        return string.Create(bitArray.Length, bitArray, (chars, array) =>
51 ✔
61
        {
51 ✔
62
            for (int index = 0; index < array.Length; index++)
876 ✔
63
            {
51 ✔
64
                chars[index] = array[index] ? '1' : '0';
387 ✔
65
            }
51 ✔
66
        });
102 ✔
67
    }
68

69
    //Copied from https://github.com/duckdb/duckdb/blob/8a17511028d306561d88da9425f9e0e88dedd70c/src/common/types/bit.cpp#L63
70
    private unsafe BitArray GetBitStringAsBitArray(ulong offset)
71
    {
72
        var bits = (DuckDBString*)DataPointer + offset;
54 ✔
73
        var data = bits->Data;
54 ✔
74

75
        var bitLength = (bits->Length - 1) * 8 - *data;
54 ✔
76

77
        var output = new BitArray(bitLength);
54 ✔
78
        var outputIndex = 0;
54 ✔
79

80
        for (var bitIndex = *data; bitIndex < 8; bitIndex++)
522 ✔
81
        {
82
            output[outputIndex++] = (*(data + 1) & (1 << (7 - bitIndex))) > 0;
207 ✔
83
        }
84

85
        for (var byteIndex = 2; byteIndex < bits->Length; byteIndex++)
162 ✔
86
        {
87
            for (var bitIndex = 0; bitIndex < 8; bitIndex++)
486 ✔
88
            {
89
                output[outputIndex++] = (*(data + byteIndex) & (1 << (7 - bitIndex))) > 0;
216 ✔
90
            }
91
        }
92

93
        return output;
54 ✔
94
    }
95

96
    private unsafe string GetString(ulong offset)
97
    {
98
        var data = (DuckDBString*)DataPointer + offset;
1,194,928 ✔
99

100
        // The string constructor that takes an encoding validates more, wraps the pointer in a span and ends in the
101
        // same decoder, so this is the shorter way to it.
102
        return Encoding.UTF8.GetString((byte*)data->Data, data->Length);
1,194,928 ✔
103
    }
104

105
    // The value of the last GetChars call and its row in the chunk, so that reading a value in parts decodes it once.
106
    // Cleared when the reader moves to another chunk.
107
    private string? charsValue;
108
    private ulong charsRow;
109

110
    internal override void Reset(IntPtr vector)
111
    {
112
        charsValue = null;
879 ✔
113
        base.Reset(vector);
879 ✔
114
    }
879 ✔
115

116
    internal unsafe long GetChars(ulong offset, long dataOffset, char[]? buffer, int bufferOffset, int length)
117
    {
118
        if (DuckDBType != DuckDBType.Varchar)
118,818 !
119
        {
NEW
120
            throw GetCharsNotSupported();
×
121
        }
122

123
        if (!IsValid(offset))
118,818 !
124
        {
NEW
125
            throw ValueIsNull();
×
126
        }
127

128
        if (buffer is null)
118,818 ✔
129
        {
130
            if (charsValue is not null && charsRow == offset)
30,006 ✔
131
            {
132
                return charsValue.Length;
15,000 ✔
133
            }
134

135
            // The length alone is counted from the UTF-8 bytes, without decoding them.
136
            var data = (DuckDBString*)DataPointer + offset;
15,006 ✔
137
            return CountUtf16Chars(new ReadOnlySpan<byte>((byte*)data->Data, data->Length));
15,006 ✔
138
        }
139

140
        if (charsValue is null || charsRow != offset)
88,812 ✔
141
        {
142
            charsValue = GetString(offset);
15,006 ✔
143
            charsRow = offset;
15,006 ✔
144
        }
145

146
        if (dataOffset >= charsValue.Length)
88,812 ✔
147
        {
148
            return 0;
15,003 ✔
149
        }
150

151
        var count = (int)Math.Min(length, charsValue.Length - dataOffset);
73,809 ✔
152
        charsValue.CopyTo((int)dataOffset, buffer, bufferOffset, count);
73,809 ✔
153
        return count;
73,809 ✔
154
    }
155

156
    // The UTF-16 length of valid UTF-8, which DuckDB guarantees for VARCHAR. ASCII bytes are one char each; otherwise
157
    // see CountNonAsciiUtf16Chars.
158
    internal static long CountUtf16Chars(ReadOnlySpan<byte> bytes)
159
    {
160
        // Ascii.IsValid stops at the first block with a non-ASCII byte.
161
        if (Ascii.IsValid(bytes))
3,504,201 ✔
162
        {
163
            return bytes.Length;
9,618 ✔
164
        }
165

166
        return CountNonAsciiUtf16Chars(bytes);
3,494,583 ✔
167
    }
168

169
    // One char for every byte that is not a continuation byte (0x80-0xBF), and a second one for every lead byte of a
170
    // four-byte sequence (0xF0-0xF4), which becomes a surrogate pair. As signed bytes, continuation bytes are -128..-65
171
    // and four-byte lead bytes -16..-12; -16..-1 is tested, because 0xF5-0xFF never occur in valid UTF-8.
172
    private static long CountNonAsciiUtf16Chars(ReadOnlySpan<byte> bytes)
173
    {
174
        long count = bytes.Length;
3,494,583 ✔
175
        var index = 0;
3,494,583 ✔
176

177
        if (Vector128.IsHardwareAccelerated)
3,494,583 !
178
        {
179
            ref var start = ref MemoryMarshal.GetReference(bytes);
3,494,583 ✔
180
            var continuationMax = Vector128.Create((sbyte)-65);
3,494,583 ✔
181
            var fourByteLeadMin = Vector128.Create((sbyte)-16);
3,494,583 ✔
182

183
            for (; index <= bytes.Length - Vector128<byte>.Count; index += Vector128<byte>.Count)
8,079,729 ✔
184
            {
185
                var block = Vector128.LoadUnsafe(ref start, (nuint)index).AsSByte();
2,292,573 ✔
186
                var continuation = Vector128.LessThanOrEqual(block, continuationMax);
2,292,573 ✔
187
                var fourByteLead = Vector128.GreaterThanOrEqual(block, fourByteLeadMin) & Vector128.LessThan(block, Vector128<sbyte>.Zero);
2,292,573 ✔
188

189
                count -= BitOperations.PopCount(continuation.ExtractMostSignificantBits());
2,292,573 ✔
190
                count += BitOperations.PopCount(fourByteLead.ExtractMostSignificantBits());
2,292,573 ✔
191
            }
192
        }
193

194
        for (; index < bytes.Length; index++)
32,272,101 ✔
195
        {
196
            var value = bytes[index];
14,388,759 ✔
197
            if ((value & 0xC0) == 0x80)
14,388,759 ✔
198
            {
199
                count--;
10,613,715 ✔
200
            }
201
            else if (value >= 0xF0)
3,775,044 ✔
202
            {
203
                count++;
3,248,979 ✔
204
            }
205
        }
206

207
        return count;
3,494,583 ✔
208
    }
209

210
    [MethodImpl(MethodImplOptions.NoInlining)]
NEW
211
    private NotSupportedException GetCharsNotSupported() => new($"GetChars is not supported for {DuckDBType} columns");
×
212

213
    [MethodImpl(MethodImplOptions.NoInlining)]
NEW
214
    private InvalidCastException ValueIsNull() => new($"Column '{ColumnName}' value is null");
×
215

216
    private unsafe Stream GetStream(ulong offset)
217
    {
218
        var data = (DuckDBString*)DataPointer + offset;
57 ✔
219

220
        return new UnmanagedMemoryStream((byte*)data->Data, data->Length, data->Length, FileAccess.Read);
57 ✔
221
    }
222
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc