Skip to content

Commit d2dfe56

Browse files
committed
Improve performance when fetching Decls.
The current implementation in clangsharp_Cursor_getDecl to get a Decl is an N^2 algorithm; getting a Decl for index X means looping through all the previous indices 0-(X-1) first. This is rather slow when dealing with hundreds of thousands of Decl instances, here's a screenshot from Instruments showing 74% of the time spent in clangsharp_Cursor_getDecl: <img width="921" height="348" alt="Screenshot 2026-01-07 at 12 47 26" src="https://github.com/user-attachments/assets/c96d5611-683e-4f0d-9878-cc551ba60c71" /> I had a few observations: 1. In my use case, I'll always iterate over all the Decls (once I start iterating). 2. There doesn't seem to be a way in native code to get the Decl given an index. 3. Storing the C++ iterator in C# to keep iterating on it is beyond my C++ knowledge (in particular any iterator lifetime management didn't look trivial). So I implemented a way to get all the Decls and store them into a C# array directly, so now the algorithm is 2N instead (iterate once to count the number of Decls, iterate again to get them). After this change, getting the Decls now takes up 0.0% (!): <img width="930" height="400" alt="Screenshot 2026-01-07 at 12 47 14" src="https://github.com/user-attachments/assets/b40ac6d3-abb3-47c7-977c-884015b617ce" /> Also note the total time of the profiling session: 1.30 min vs 3.38 min.
1 parent dedb818 commit d2dfe56

9 files changed

Lines changed: 79 additions & 23 deletions

File tree

sources/ClangSharp.Interop/Extensions/CXCursor.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1834,6 +1834,8 @@ public static void DisposeOverriddenCursors(ReadOnlySpan<CXCursor> overridden)
18341834

18351835
public readonly CXCursor GetDecl(uint index) => clangsharp.Cursor_getDecl(this, index);
18361836

1837+
public readonly bool GetDecls(CXCursor* cursors, uint count) => clangsharp.Cursor_getDecls(this, cursors, count);
1838+
18371839
public readonly void GetDefinitionSpellingAndExtent(out string spelling, out uint startLine, out uint startColumn, out uint endLine, out uint endColumn)
18381840
{
18391841
fixed (uint* pStartLine = &startLine)

sources/ClangSharp.Interop/clangsharp/clangsharp.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
namespace ClangSharp.Interop;
88

9-
public static partial class @clangsharp
9+
public static unsafe partial class @clangsharp
1010
{
1111
[DllImport("libClangSharp", CallingConvention = CallingConvention.Cdecl, EntryPoint = "clangsharp_Cursor_getArgument", ExactSpelling = true)]
1212
public static extern CXCursor Cursor_getArgument(CXCursor C, [NativeTypeName("unsigned int")] uint i);
@@ -181,6 +181,9 @@ public static partial class @clangsharp
181181
[DllImport("libClangSharp", CallingConvention = CallingConvention.Cdecl, EntryPoint = "clangsharp_Cursor_getDecl", ExactSpelling = true)]
182182
public static extern CXCursor Cursor_getDecl(CXCursor C, [NativeTypeName("unsigned int")] uint i);
183183

184+
[DllImport("libClangSharp", CallingConvention = CallingConvention.Cdecl, EntryPoint = "clangsharp_Cursor_getDecls", ExactSpelling = true)]
185+
public static extern bool Cursor_getDecls(CXCursor C, CXCursor* decls, [NativeTypeName("unsigned int")] uint count);
186+
184187
[DllImport("libClangSharp", CallingConvention = CallingConvention.Cdecl, EntryPoint = "clangsharp_Cursor_getDeclKind", ExactSpelling = true)]
185188
public static extern CX_DeclKind Cursor_getDeclKind(CXCursor C);
186189

sources/ClangSharp/Cursors/Decls/Decl.cs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,29 @@ private protected Decl(CXCursor handle, CXCursorKind expectedCursorKind, CX_Decl
3535
_attrs = LazyList.Create<Attr>(Handle.NumAttrs, (i) => TranslationUnit.GetOrCreate<Attr>(Handle.GetAttr(unchecked((uint)i))));
3636
_body = new ValueLazy<Stmt?>(() => !Handle.Body.IsNull ? TranslationUnit.GetOrCreate<Stmt>(Handle.Body) : null);
3737
_canonicalDecl = new ValueLazy<Decl>(() => TranslationUnit.GetOrCreate<Decl>(Handle.CanonicalCursor));
38-
_decls = LazyList.Create<Decl>(Handle.NumDecls, (i) => TranslationUnit.GetOrCreate<Decl>(Handle.GetDecl(unchecked((uint)i))));
38+
_decls = LazyList.Create<Decl>(Handle.NumDecls, (i) => TranslationUnit.GetOrCreate<Decl>(Handle.GetDecl(unchecked((uint)i))), (list) => {
39+
var cursors = new CXCursor[list.Length];
40+
bool success;
41+
unsafe {
42+
fixed (CXCursor* first = cursors) {
43+
success = Handle.GetDecls(first, unchecked((uint)list.Length));
44+
}
45+
}
46+
if (success)
47+
{
48+
for (var i = 0; i < list.Length; i++)
49+
{
50+
list[i] = TranslationUnit.GetOrCreate<Decl>(cursors[i]);
51+
}
52+
}
53+
else
54+
{
55+
for (var i = 0; i < list.Length; i++)
56+
{
57+
list[i] = TranslationUnit.GetOrCreate<Decl>(Handle.GetDecl(unchecked((uint)i)));
58+
}
59+
}
60+
});
3961
_describedTemplate = new ValueLazy<TemplateDecl?>(() => {
4062
var describedTemplate = Handle.DescribedTemplate;
4163
return describedTemplate.IsNull ? null : TranslationUnit.GetOrCreate<TemplateDecl>(describedTemplate);

sources/ClangSharp/LazyList.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@ namespace ClangSharp;
77

88
internal static class LazyList
99
{
10-
public static LazyList<T> Create<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] T>(int count, Func<int, T> valueFactory)
10+
public static LazyList<T> Create<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] T>(int count, Func<int, T> valueFactory, Action<T[]>? allValuesFactory = null)
1111
where T : class
1212
{
1313
if (count <= 0)
1414
{
1515
return LazyList<T>.Empty;
1616
}
17-
return new LazyList<T>(count, valueFactory);
17+
return new LazyList<T>(count, valueFactory, allValuesFactory);
1818
}
1919

2020
public static LazyList<T, TBase> Create<T, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TBase>(LazyList<TBase> list, int skip = -1, int take = -1)

sources/ClangSharp/LazyList`1.cs

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@ internal sealed class LazyList<[DynamicallyAccessedMembers(DynamicallyAccessedMe
1212
{
1313
internal readonly T[] _items;
1414
internal readonly Func<int, T> _valueFactory;
15+
internal readonly Action<T[]>? _allValuesFactory;
1516

1617
public static readonly LazyList<T> Empty = new LazyList<T>(0, _ => null!);
1718

18-
public LazyList(int count, Func<int, T> valueFactory)
19+
public LazyList(int count, Func<int, T> valueFactory, Action<T[]>? allValuesFactory = null)
1920
{
2021
_items = (count <= 0) ? [] : new T[count];
2122
_valueFactory = valueFactory;
23+
_allValuesFactory = allValuesFactory;
2224
}
2325

2426
public T this[int index]
@@ -30,8 +32,16 @@ public T this[int index]
3032

3133
if (item is null)
3234
{
33-
item = _valueFactory(index);
34-
items[index] = item;
35+
if (_allValuesFactory is not null)
36+
{
37+
_allValuesFactory(_items);
38+
item = _items[index];
39+
}
40+
else
41+
{
42+
item = _valueFactory(index);
43+
items[index] = item;
44+
}
3545
}
3646

3747
return item;
@@ -56,14 +66,7 @@ public void CopyTo(T[] array, int arrayIndex)
5666

5767
for (var i = 0; i < items.Length; i++)
5868
{
59-
var currentItem = items[i];
60-
61-
if (currentItem is null)
62-
{
63-
currentItem = _valueFactory(i);
64-
items[i] = currentItem;
65-
}
66-
69+
var currentItem = this[i];
6770
array[arrayIndex + i] = currentItem;
6871
}
6972
}
@@ -76,14 +79,7 @@ public int IndexOf(T item)
7679

7780
for (var i = 0; i < items.Length; i++)
7881
{
79-
var currentItem = items[i];
80-
81-
if (currentItem is null)
82-
{
83-
currentItem = _valueFactory(i);
84-
items[i] = currentItem;
85-
}
86-
82+
var currentItem = this[i];
8783
if (EqualityComparer<T>.Default.Equals(currentItem, item))
8884
{
8985
return i;

sources/libClangSharp/ClangSharp.cpp

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -943,6 +943,28 @@ CXType clangsharp_Cursor_getDeclaredReturnType(CXCursor C) {
943943
return MakeCXType(QualType(), getCursorTU(C));
944944
}
945945

946+
bool clangsharp_Cursor_getDecls(CXCursor C, CXCursor* decls, unsigned count) {
947+
if (isDeclOrTU(C.kind)) {
948+
const Decl* D = getCursorDecl(C);
949+
950+
if (const DeclContext* DC = dyn_cast<DeclContext>(D)) {
951+
unsigned n = 0;
952+
953+
for (auto decl : DC->decls()) {
954+
if (n == count) {
955+
return false;
956+
}
957+
decls[n] = MakeCXCursor(decl, getCursorTU(C));
958+
n++;
959+
}
960+
961+
return n == count;
962+
}
963+
}
964+
965+
return false;
966+
}
967+
946968
CXCursor clangsharp_Cursor_getDecl(CXCursor C, unsigned i) {
947969
if (isDeclOrTU(C.kind)) {
948970
const Decl* D = getCursorDecl(C);

sources/libClangSharp/ClangSharp.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,8 @@ CLANGSHARP_LINKAGE CXType clangsharp_Cursor_getDeclaredReturnType(CXCursor C);
341341

342342
CLANGSHARP_LINKAGE CXCursor clangsharp_Cursor_getDecl(CXCursor C, unsigned i);
343343

344+
CLANGSHARP_LINKAGE bool clangsharp_Cursor_getDecls(CXCursor C, CXCursor* decls, unsigned count);
345+
344346
CLANGSHARP_LINKAGE CX_DeclKind clangsharp_Cursor_getDeclKind(CXCursor C);
345347

346348
CLANGSHARP_LINKAGE CXCursor clangsharp_Cursor_getDecomposedDecl(CXCursor C);

tests/ClangSharp.UnitTests/CXCursor.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ public class CXCursorTest : TranslationUnitTest
1010
[Test]
1111
public void AttrKindSpelling()
1212
{
13+
AssertNeedNewClangSharp();
1314

1415
var inputContents =
1516
$$"""

tests/ClangSharp.UnitTests/CursorTests/DeclTest.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ public sealed class DeclTest : TranslationUnitTest
1515
[TestCase("public", CX_CXXPublic)]
1616
public void AccessSpecDeclTest(string accessSpecifier, CX_CXXAccessSpecifier expectedAccessSpecifier)
1717
{
18+
AssertNeedNewClangSharp();
19+
1820
var inputContents = $@"struct MyStruct
1921
{{
2022
{accessSpecifier}:
@@ -32,6 +34,8 @@ public void AccessSpecDeclTest(string accessSpecifier, CX_CXXAccessSpecifier exp
3234
[Test]
3335
public void ClassTemplateDeclTest()
3436
{
37+
AssertNeedNewClangSharp();
38+
3539
var inputContents = $@"template<class T>
3640
class MyClass
3741
{{
@@ -51,6 +55,8 @@ class MyClass
5155
[Test]
5256
public void ClassTemplatePartialSpecializationDeclTest()
5357
{
58+
AssertNeedNewClangSharp();
59+
5460
var inputContents = $@"template<class T, class U>
5561
class MyClass
5662
{{
@@ -78,6 +84,8 @@ class MyClass<int, U>
7884
[Test]
7985
public void TemplateParameterPackTest()
8086
{
87+
AssertNeedNewClangSharp();
88+
8189
var inputContents = $@"template<class... Types>
8290
class tuple;
8391

0 commit comments

Comments
 (0)