From 300fe15946700a497feb7e7b8534c492935ea3f3 Mon Sep 17 00:00:00 2001 From: James Moschou Date: Wed, 21 Oct 2026 19:08:09 +0200 Subject: [PATCH 1/3] Rename Vector directory to Collection Prepares for adding a set type alongside the vector types. Co-Authored-By: Claude Opus 5.5 --- .../ComputeCxx/{Vector => Collection}/IndirectPointerVector.h | 0 Sources/ComputeCxx/{Vector => Collection}/Vector.h | 0 Sources/ComputeCxx/Comparison/Builder.h | 2 +- Sources/ComputeCxx/Comparison/Compare.h | 2 +- Sources/ComputeCxx/Data/Table.h | 2 +- Sources/ComputeCxx/Debug/DebugServer.h | 2 +- Sources/ComputeCxx/Graph/Graph.h | 2 +- Sources/ComputeCxx/Protobuf/Encoder.h | 2 +- Sources/ComputeCxx/Subgraph/Subgraph.h | 2 +- Sources/ComputeCxx/Swift/ContextDescriptor.h | 2 +- 10 files changed, 8 insertions(+), 8 deletions(-) rename Sources/ComputeCxx/{Vector => Collection}/IndirectPointerVector.h (100%) rename Sources/ComputeCxx/{Vector => Collection}/Vector.h (100%) diff --git a/Sources/ComputeCxx/Vector/IndirectPointerVector.h b/Sources/ComputeCxx/Collection/IndirectPointerVector.h similarity index 100% rename from Sources/ComputeCxx/Vector/IndirectPointerVector.h rename to Sources/ComputeCxx/Collection/IndirectPointerVector.h diff --git a/Sources/ComputeCxx/Vector/Vector.h b/Sources/ComputeCxx/Collection/Vector.h similarity index 100% rename from Sources/ComputeCxx/Vector/Vector.h rename to Sources/ComputeCxx/Collection/Vector.h diff --git a/Sources/ComputeCxx/Comparison/Builder.h b/Sources/ComputeCxx/Comparison/Builder.h index bd559cab..75b0e748 100644 --- a/Sources/ComputeCxx/Comparison/Builder.h +++ b/Sources/ComputeCxx/Comparison/Builder.h @@ -2,11 +2,11 @@ #include +#include "Collection/Vector.h" #include "ComputeCxx/IAGBase.h" #include "ComputeCxx/IAGComparison.h" #include "LayoutDescriptor.h" #include "Swift/MetadataVisitor.h" -#include "Vector/Vector.h" IAG_ASSUME_NONNULL_BEGIN diff --git a/Sources/ComputeCxx/Comparison/Compare.h b/Sources/ComputeCxx/Comparison/Compare.h index cef88104..159ecb20 100644 --- a/Sources/ComputeCxx/Comparison/Compare.h +++ b/Sources/ComputeCxx/Comparison/Compare.h @@ -2,9 +2,9 @@ #include "LayoutDescriptor.h" +#include "Collection/Vector.h" #include "ComputeCxx/IAGBase.h" #include "ComputeCxx/IAGComparison.h" -#include "Vector/Vector.h" IAG_ASSUME_NONNULL_BEGIN diff --git a/Sources/ComputeCxx/Data/Table.h b/Sources/ComputeCxx/Data/Table.h index 9bc4114c..f040b459 100644 --- a/Sources/ComputeCxx/Data/Table.h +++ b/Sources/ComputeCxx/Data/Table.h @@ -8,8 +8,8 @@ #include #include +#include "Collection/Vector.h" #include "ComputeCxx/IAGBase.h" -#include "Vector/Vector.h" IAG_ASSUME_NONNULL_BEGIN diff --git a/Sources/ComputeCxx/Debug/DebugServer.h b/Sources/ComputeCxx/Debug/DebugServer.h index eac8d111..2a0de408 100644 --- a/Sources/ComputeCxx/Debug/DebugServer.h +++ b/Sources/ComputeCxx/Debug/DebugServer.h @@ -10,8 +10,8 @@ #include +#include "Collection/Vector.h" #include "ComputeCxx/IAGDebugServer.h" -#include "Vector/Vector.h" IAG_ASSUME_NONNULL_BEGIN diff --git a/Sources/ComputeCxx/Graph/Graph.h b/Sources/ComputeCxx/Graph/Graph.h index f578fcef..fd15e3fa 100644 --- a/Sources/ComputeCxx/Graph/Graph.h +++ b/Sources/ComputeCxx/Graph/Graph.h @@ -20,10 +20,10 @@ #include "Attribute/AttributeID/AttributeID.h" #include "Attribute/AttributeType/AttributeType.h" #include "Closure/ClosureFunction.h" +#include "Collection/Vector.h" #include "ComputeCxx/IAGGraphProfiling.h" #include "ComputeCxx/IAGGraphTracing.h" #include "Swift/Metadata.h" -#include "Vector/Vector.h" IAG_ASSUME_NONNULL_BEGIN diff --git a/Sources/ComputeCxx/Protobuf/Encoder.h b/Sources/ComputeCxx/Protobuf/Encoder.h index 92dca0fb..6be3cb09 100644 --- a/Sources/ComputeCxx/Protobuf/Encoder.h +++ b/Sources/ComputeCxx/Protobuf/Encoder.h @@ -2,7 +2,7 @@ #include "ComputeCxx/IAGBase.h" -#include "Vector/Vector.h" +#include "Collection/Vector.h" IAG_ASSUME_NONNULL_BEGIN diff --git a/Sources/ComputeCxx/Subgraph/Subgraph.h b/Sources/ComputeCxx/Subgraph/Subgraph.h index b7ca82c3..f31e2c46 100644 --- a/Sources/ComputeCxx/Subgraph/Subgraph.h +++ b/Sources/ComputeCxx/Subgraph/Subgraph.h @@ -6,12 +6,12 @@ #include "Attribute/AttributeID/AttributeID.h" #include "Closure/ClosureFunction.h" +#include "Collection/IndirectPointerVector.h" #include "ComputeCxx/IAGBase.h" #include "ComputeCxx/IAGUniqueID.h" #include "Data/Zone.h" #include "Graph/Graph.h" #include "Graph/Tree/TreeElement.h" -#include "Vector/IndirectPointerVector.h" IAG_ASSUME_NONNULL_BEGIN diff --git a/Sources/ComputeCxx/Swift/ContextDescriptor.h b/Sources/ComputeCxx/Swift/ContextDescriptor.h index 28a24e76..31f8befa 100644 --- a/Sources/ComputeCxx/Swift/ContextDescriptor.h +++ b/Sources/ComputeCxx/Swift/ContextDescriptor.h @@ -3,9 +3,9 @@ #include #include +#include "Collection/Vector.h" #include "ComputeCxx/IAGBase.h" #include "Metadata.h" -#include "Vector/Vector.h" IAG_ASSUME_NONNULL_BEGIN From 6351c9d332de4abbaaa2931227a432b3f0cf5f94 Mon Sep 17 00:00:00 2001 From: James Moschou Date: Wed, 21 Oct 2026 19:08:16 +0200 Subject: [PATCH 2/3] Add IAG::set A set of pointers stored in a vector sorted by address. Erased elements are marked as removed and skipped when iterating, then compacted away once they outnumber the remaining elements, so erasing is amortized O(1) after an O(log n) lookup instead of shifting the rest of the vector. Co-Authored-By: Claude Opus 5.5 --- Sources/ComputeCxx/Collection/Set.h | 92 +++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 Sources/ComputeCxx/Collection/Set.h diff --git a/Sources/ComputeCxx/Collection/Set.h b/Sources/ComputeCxx/Collection/Set.h new file mode 100644 index 00000000..0f3b0fec --- /dev/null +++ b/Sources/ComputeCxx/Collection/Set.h @@ -0,0 +1,92 @@ +#pragma once + +#include +#include + +#include "ComputeCxx/IAGBase.h" +#include "Vector.h" + +IAG_ASSUME_NONNULL_BEGIN + +namespace IAG { + +/// A set of pointers stored in a vector sorted by address. +template + requires std::is_pointer_v && std::unsigned_integral +class set { + private: + static constexpr uintptr_t removed = 0x1; + + vector _slots; + size_type _num_removed = 0; + + uintptr_t *_Nonnull find_slot(const T &value) { + return std::lower_bound(_slots.begin(), _slots.end(), uintptr_t(value), + [](uintptr_t slot, uintptr_t value) { return (slot & ~removed) < value; }); + }; + + public: + class iterator { + private: + const uintptr_t *_Nonnull _slot; + const uintptr_t *_Nonnull _end; + + void skip_removed() { + while (_slot != _end && (*_slot & removed)) { + ++_slot; + } + }; + + public: + iterator(const uintptr_t *_Nonnull slot, const uintptr_t *_Nonnull end) : _slot(slot), _end(end) { + skip_removed(); + }; + + T _Nonnull operator*() const { return reinterpret_cast(*_slot); }; + iterator &operator++() { + ++_slot; + skip_removed(); + return *this; + }; + bool operator==(const iterator &other) const { return _slot == other._slot; }; + }; + + iterator begin() const { return iterator(_slots.begin(), _slots.end()); }; + iterator end() const { return iterator(_slots.end(), _slots.end()); }; + + bool empty() const { return _slots.size() == _num_removed; }; + + std::pair insert(const T &value) { + auto *slot = find_slot(value); + bool inserted = true; + if (slot == _slots.end() || (*slot & ~removed) != uintptr_t(value)) { + slot = _slots.insert(slot, uintptr_t(value)); + } else if (*slot & removed) { + // A new element reused the address of a removed one + *slot = uintptr_t(value); + _num_removed -= 1; + } else { + inserted = false; + } + return {iterator(slot, _slots.end()), inserted}; + }; + + size_type erase(const T &value) { + auto *slot = find_slot(value); + if (slot == _slots.end() || *slot != uintptr_t(value)) { + return 0; + } + *slot |= removed; + _num_removed += 1; + if (_num_removed > _slots.size() / 2) { + _slots.erase(std::remove_if(_slots.begin(), _slots.end(), [](uintptr_t slot) { return slot & removed; }), + _slots.end()); + _num_removed = 0; + } + return 1; + }; +}; + +} // namespace IAG + +IAG_ASSUME_NONNULL_END From 0e2059138447217f61913a4cbf643cc0ab2d0192 Mon Sep 17 00:00:00 2001 From: James Moschou Date: Wed, 21 Oct 2026 19:08:22 +0200 Subject: [PATCH 3/3] Use IAG::set for the graph's subgraphs Removing a subgraph from the graph used std::remove over the sorted vector of subgraphs, which is O(n), so invalidating n subgraphs together (e.g. tearing down a graph) was O(n^2). Erasing from IAG::set is amortized O(1) after a binary search, and iteration order is unchanged. Co-Authored-By: Claude Opus 5.5 --- Sources/ComputeCxx/Graph/Graph.cpp | 6 +- Sources/ComputeCxx/Graph/Graph.h | 5 +- Sources/ComputeCxx/Graph/Graph.mm | 5 +- .../Subgraph/SubgraphLifecycleTests.swift | 83 +++++++++++++++++++ 4 files changed, 91 insertions(+), 8 deletions(-) diff --git a/Sources/ComputeCxx/Graph/Graph.cpp b/Sources/ComputeCxx/Graph/Graph.cpp index 95b624b5..6ef1841b 100644 --- a/Sources/ComputeCxx/Graph/Graph.cpp +++ b/Sources/ComputeCxx/Graph/Graph.cpp @@ -243,16 +243,14 @@ void Graph::call_main_handler(void *context, void (*body)(void *)) { #pragma mark - Subgraphs void Graph::add_subgraph(Subgraph &subgraph) { - auto *pos = std::lower_bound(_subgraphs.begin(), _subgraphs.end(), &subgraph); - _subgraphs.insert(pos, &subgraph); + _subgraphs.insert(&subgraph); _num_subgraphs += 1; _num_subgraphs_total += 1; } void Graph::remove_subgraph(Subgraph &subgraph) { - auto *iter = std::remove(_subgraphs.begin(), _subgraphs.end(), &subgraph); - _subgraphs.erase(iter, _subgraphs.end()); + _subgraphs.erase(&subgraph); if (auto *map = _tree_data_elements_by_subgraph.get()) { auto iter = map->find(&subgraph); diff --git a/Sources/ComputeCxx/Graph/Graph.h b/Sources/ComputeCxx/Graph/Graph.h index fd15e3fa..b0de4f37 100644 --- a/Sources/ComputeCxx/Graph/Graph.h +++ b/Sources/ComputeCxx/Graph/Graph.h @@ -20,6 +20,7 @@ #include "Attribute/AttributeID/AttributeID.h" #include "Attribute/AttributeType/AttributeType.h" #include "Closure/ClosureFunction.h" +#include "Collection/Set.h" #include "Collection/Vector.h" #include "ComputeCxx/IAGGraphProfiling.h" #include "ComputeCxx/IAGGraphTracing.h" @@ -191,7 +192,7 @@ class Graph { std::unique_ptr _keys = nullptr; // Subgraphs - vector _subgraphs; + set _subgraphs; vector _subgraphs_with_cached_nodes; vector _invalidating_subgraphs; bool _deferring_subgraph_invalidation = false; @@ -337,7 +338,7 @@ class Graph { // MARK: Subgraphs - vector &subgraphs() { return _subgraphs; }; + set &subgraphs() { return _subgraphs; }; void add_subgraph(Subgraph &subgraph); void remove_subgraph(Subgraph &subgraph); diff --git a/Sources/ComputeCxx/Graph/Graph.mm b/Sources/ComputeCxx/Graph/Graph.mm index 021cdbac..92ca0bf0 100644 --- a/Sources/ComputeCxx/Graph/Graph.mm +++ b/Sources/ComputeCxx/Graph/Graph.mm @@ -543,8 +543,9 @@ int trap_cycles() { NSMutableArray *subgraph_dicts = [NSMutableArray array]; auto subgraph_indices = std::unordered_map(); - for (uint32_t index = 0, iter = graph->subgraphs().size(); iter > 0; --iter, ++index) { - subgraph_indices.try_emplace(graph->subgraphs()[index], index); + uint64_t subgraph_index = 0; + for (auto subgraph : graph->subgraphs()) { + subgraph_indices.try_emplace(subgraph, subgraph_index++); } for (auto subgraph : graph->subgraphs()) { diff --git a/Tests/ComputeTests/Shared/Subgraph/SubgraphLifecycleTests.swift b/Tests/ComputeTests/Shared/Subgraph/SubgraphLifecycleTests.swift index 3c6951dc..ec2e6bc1 100644 --- a/Tests/ComputeTests/Shared/Subgraph/SubgraphLifecycleTests.swift +++ b/Tests/ComputeTests/Shared/Subgraph/SubgraphLifecycleTests.swift @@ -89,4 +89,87 @@ struct SubgraphLifecycleTests { #expect(graph.counter(for: .createdSubgraphs) == 1) } } + + @Suite + struct SubgraphBatchInvalidateTests { + @Test + func invalidateManySubgraphs() { + let graph = Graph() + + var subgraphs: [Subgraph] = [] + for _ in 0..<10_000 { + var parent = Subgraph(graph: graph) + subgraphs.append(parent) + for _ in 0..<3 { + let child = Subgraph(graph: graph) + parent.addChild(child) + subgraphs.append(child) + parent = child + } + } + #expect(graph.counter(for: .subgraphs) == 40_000) + + graph.withoutSubgraphInvalidation { + for subgraph in subgraphs { + subgraph.invalidate() + } + } + #expect(graph.counter(for: .subgraphs) == 0) + + withExtendedLifetime(subgraphs) {} + } + + #if canImport(Darwin) + @Test(arguments: [1, 3, 5]) + func remainingSubgraphsAfterPartialInvalidate(keepEvery: Int) throws { + let graph = Graph() + + let root = Subgraph(graph: graph) + var subgraphs: [Subgraph] = [] + for _ in 0..<1_000 { + let subgraph = Subgraph(graph: graph) + root.addChild(subgraph) + subgraphs.append(subgraph) + } + + var invalidated: [Subgraph] = [] + var remaining: [Subgraph] = [] + for (index, subgraph) in subgraphs.enumerated() { + if index % (keepEvery + 1) == 0 { + remaining.append(subgraph) + } else { + invalidated.append(subgraph) + } + } + graph.withoutSubgraphInvalidation { + for subgraph in invalidated { + subgraph.invalidate() + } + } + + // New subgraphs may reuse the addresses of invalidated ones + for _ in 0..<100 { + let subgraph = Subgraph(graph: graph) + root.addChild(subgraph) + remaining.append(subgraph) + } + + #expect(graph.counter(for: .subgraphs) == remaining.count + 1) + #expect(root.childCount == remaining.count) + + let description = try #require(graph.dictionaryDescription()) + let graphDescription = try #require(description.graphs.first) + #expect(graphDescription.subgraphs.count == remaining.count + 1) + + let rootIndex = try #require(graphDescription.subgraphs.firstIndex { $0.children != nil }) + let rootChildren = try #require(graphDescription.subgraphs[rootIndex].children) + #expect(Set(rootChildren).count == remaining.count) + for childIndex in rootChildren { + #expect(graphDescription.subgraphs[childIndex].parents == [rootIndex]) + } + + withExtendedLifetime((root, subgraphs, remaining)) {} + } + #endif + } }