Feature Iteration and Storage Benchmark
This follows an effort to improve the computation speed of the frontend feature tracking module. One small investigation into the (perhaps excessive) use of the Feature and associated FeatureContainer data-structures showed my over eager design has led to pretty significant overhead in several places. The following details the investigation.
Note: Written by Chat-GPT (free) and reviewed by myself.
Background / Initial Problem
The Dynamic SLAM feature-tracking pipeline performs a large number of operations over tracked image features. The existing FeatureContainer stores features in an std::unordered_map<TrackletId, Feature::Ptr>, with each feature accessed through a Feature class interface. This provides useful functionality and flexible lookup semantics, but it is potentially expensive for workloads that repeatedly iterate over many features.
The original question was whether this feature representation and iteration mechanism introduces a significant performance cost compared with a simpler, contiguous data representation such as a structure-of-arrays (SoA). This is particularly relevant for the visual tracking frontend, where feature data may be accessed multiple times per frame and where reducing per-feature overhead could contribute meaningfully to achieving the desired high processing rate.
These benchmarks were therefore designed to separate the cost of the existing container from the cost of the underlying feature representation. Two benchmark suites were used: FeatureIterationBenchmark compared the existing FeatureContainer directly against a simple SoA representation, while FeatureStorageBenchmark progressively replaced the expensive parts of the representation with simpler storage types.
Technical Background
The existing implementation has several sources of per-feature overhead. The primary container is an std::unordered_map, so iteration follows hash-table nodes rather than a contiguous array. Each map entry stores a Feature::Ptr, which introduces another pointer indirection before reaching the feature object. The Feature object itself contains its data behind the class implementation and exposes properties through accessor functions.
In contrast, an SoA stores each property in its own contiguous std::vector. For example, all x coordinates are stored next to one another, all y coordinates are stored next to one another, and so on. When an operation only needs a small subset of feature properties, this layout allows the CPU to traverse compact, contiguous memory with fewer cache misses and less pointer chasing.
A contiguous std::vector<Feature> removes the hash-table traversal and heap/pointer indirection, while retaining the existing Feature object and its interface. A std::vector<Feature::Ptr> removes the hash-table traversal but retains the pointer indirection. Comparing these four representations therefore gives a useful indication of where the observed overhead originates:
unordered_map<TrackletId, Feature::Ptr> — current implementation.
vector<Feature::Ptr> — removes hash-map traversal but retains pointers.
vector<Feature> — contiguous feature objects with the existing feature representation.
- SoA — contiguous arrays containing only the required properties.
The important point is that the SoA comparison is not purely an AoS-versus-SoA comparison. The existing implementation combines a hash map, pointers/heap allocation, the Feature class representation, and its accessors. The benchmark is intended to quantify the practical cost of the complete representation used by the current pipeline.
FeatureIterationBenchmark Results
The first benchmark compared the existing FeatureContainer against a simple SoA representation. Across feature counts from 100 to 20,000, the results were approximately:
| Number of features |
FeatureContainer |
SoA |
Ratio |
| 100 |
209.1 ns/feature |
10.3 ns/feature |
20.3× |
| 500 |
210.1 ns/feature |
9.93 ns/feature |
21.2× |
| 1,000 |
219.4 ns/feature |
10.2 ns/feature |
21.5× |
| 5,000 |
222.6 ns/feature |
10.35 ns/feature |
21.5× |
| 10,000 |
212.9 ns/feature |
10.21 ns/feature |
20.9× |
| 20,000 |
213.8 ns/feature |
10.16 ns/feature |
21.0× |
The important result is that the performance ratio remained approximately constant as the number of features increased. The current container required roughly 210–223 ns per feature, whereas the SoA required roughly 10 ns per feature.
For example, at 10,000 features, one complete traversal corresponds to approximately 2.13 ms for the existing container compared with approximately 0.10 ms for the SoA. This represents approximately 2 ms of additional cost for a single traversal of 10,000 features.
FeatureStorageBenchmark Results
The second benchmark was designed to determine whether this difference was primarily caused by the hash map, pointer indirection, or the Feature representation itself.
The results were:
| Number of features |
Map |
vector<Feature::Ptr> |
vector<Feature> |
SoA |
| 100 |
174.6 ns |
165.5 ns |
148.6 ns |
9.83 ns |
| 500 |
175.6 ns |
164.3 ns |
147.9 ns |
9.78 ns |
| 1,000 |
174.1 ns |
165.0 ns |
148.9 ns |
9.86 ns |
| 5,000 |
174.6 ns |
164.8 ns |
148.4 ns |
9.79 ns |
| 10,000 |
176.1 ns |
169.4 ns |
150.1 ns |
9.89 ns |
| 20,000 |
178.0 ns |
168.2 ns |
150.1 ns |
9.83 ns |
The results show a clear progression as the storage representation becomes simpler.
Replacing the unordered_map with a vector<Feature::Ptr> reduced the measured cost from approximately 175 ns/feature to approximately 165–169 ns/feature. This is only around a 5–6% reduction, indicating that hash-map traversal is not the dominant source of the overhead in this particular benchmark.
Replacing the pointer vector with a vector<Feature> reduced the cost further to approximately 148–150 ns/feature. This suggests that pointer indirection and the associated non-contiguous placement of individual Feature objects contribute additional overhead.
However, the largest difference remains between vector<Feature> and the SoA representation. The contiguous Feature representation still costs approximately 15× more per feature than the SoA. This indicates that the structure and access pattern of the Feature object itself are a substantial part of the measured cost when compared with a representation containing only the small set of scalar properties required by the benchmark.
Interpretation
The benchmarks indicate that the large performance difference is not primarily caused by std::unordered_map. Moving from the map to a vector of feature pointers produced only a relatively small improvement.
The results instead suggest that several smaller costs accumulate in the current representation, including pointer indirection, the memory layout of the Feature object, and accessing properties through the existing feature abstraction. The SoA benefits from having exactly the required data in compact, contiguous arrays, which is particularly favourable for sequential CPU processing and cache utilisation.
The approximately 21× difference observed in the first benchmark should therefore not be interpreted as saying that "SoA is 21× faster than AoS" in general. It is more accurately described as the measured difference between the current FeatureContainer implementation and the simplified SoA representation used in this benchmark.
The benchmark also shows that the performance characteristics are stable over the tested range. There was little change in ns/feature between 100 and 20,000 features, suggesting that the dominant cost is approximately linear in the number of features and that the relative overhead is not caused by a particular large-data threshold within this range.
Relevance to Dynamic SLAM / Feature Tracking
These results are useful for the current optimisation work because feature tracking is typically dominated by operations that repeatedly access feature properties such as keypoints, track IDs, object IDs, ages, and inlier status. If the same collection of features is traversed many times during a frame, the per-feature overhead can accumulate.
For example, using the FeatureIterationBenchmark result at 10,000 features, the measured difference between the current representation and the SoA was approximately 2.0 ms for one traversal. If an equivalent feature traversal were performed five times in a frame, the corresponding difference would be roughly 10 ms; ten such traversals would be roughly 20 ms. These numbers are illustrative extrapolations rather than measurements of the complete pipeline.
This makes the result potentially significant for a high-frequency visual tracking pipeline. At 20 Hz, the total frame budget is 50 ms, so several milliseconds spent repeatedly traversing feature data can become a meaningful fraction of the available budget.
However, these benchmarks do not by themselves establish that the entire feature system should be rewritten as an SoA. The benchmark isolates storage and iteration costs rather than measuring the complete tracking pipeline. The current Feature abstraction may also provide functionality that is valuable for correctness, maintainability, and different access patterns. In addition, the benchmark only measures the particular operations included in these tests.
The results therefore provide evidence that feature storage and iteration are worth considering as an optimisation target, especially if profiling shows that these operations occur repeatedly in the tracking frontend. They do not, on their own, justify replacing the current representation everywhere.
Conclusions
The benchmarks demonstrate that the current feature representation has measurable per-feature overhead compared with a compact SoA representation.
The main observations are:
- The current
FeatureContainer is approximately 20–22× slower per feature than the tested SoA in the first iteration benchmark.
- At 10,000 features, this corresponds to approximately 2.0 ms of additional cost for one traversal in the benchmark.
- Replacing the
unordered_map with vector<Feature::Ptr> only reduced the cost by approximately 5–6%, suggesting that the hash map is not the dominant bottleneck.
- Replacing
vector<Feature::Ptr> with vector<Feature> reduced the cost further, showing that pointer indirection and object placement contribute measurable overhead.
- The SoA remained around 9.8 ns/feature, compared with approximately 150 ns/feature for
vector<Feature>, showing that the feature object's representation/access pattern is also important.
- The measured per-feature costs were largely stable from 100 to 20,000 features, indicating consistent scaling over the tested range.
Overall, the results provide a strong indication that a more data-oriented feature representation could be beneficial for performance-critical sections of the Dynamic SLAM frontend. The next step should be to benchmark the specific feature-access patterns used by the actual tracking pipeline and determine whether an SoA or a hybrid representation can reduce runtime without unnecessarily complicating the rest of the system.
Feature Iteration and Storage Benchmark
This follows an effort to improve the computation speed of the frontend feature tracking module. One small investigation into the (perhaps excessive) use of the
Featureand associatedFeatureContainerdata-structures showed my over eager design has led to pretty significant overhead in several places. The following details the investigation.Background / Initial Problem
The Dynamic SLAM feature-tracking pipeline performs a large number of operations over tracked image features. The existing
FeatureContainerstores features in anstd::unordered_map<TrackletId, Feature::Ptr>, with each feature accessed through aFeatureclass interface. This provides useful functionality and flexible lookup semantics, but it is potentially expensive for workloads that repeatedly iterate over many features.The original question was whether this feature representation and iteration mechanism introduces a significant performance cost compared with a simpler, contiguous data representation such as a structure-of-arrays (SoA). This is particularly relevant for the visual tracking frontend, where feature data may be accessed multiple times per frame and where reducing per-feature overhead could contribute meaningfully to achieving the desired high processing rate.
These benchmarks were therefore designed to separate the cost of the existing container from the cost of the underlying feature representation. Two benchmark suites were used:
FeatureIterationBenchmarkcompared the existingFeatureContainerdirectly against a simple SoA representation, whileFeatureStorageBenchmarkprogressively replaced the expensive parts of the representation with simpler storage types.Technical Background
The existing implementation has several sources of per-feature overhead. The primary container is an
std::unordered_map, so iteration follows hash-table nodes rather than a contiguous array. Each map entry stores aFeature::Ptr, which introduces another pointer indirection before reaching the feature object. TheFeatureobject itself contains its data behind the class implementation and exposes properties through accessor functions.In contrast, an SoA stores each property in its own contiguous
std::vector. For example, all x coordinates are stored next to one another, all y coordinates are stored next to one another, and so on. When an operation only needs a small subset of feature properties, this layout allows the CPU to traverse compact, contiguous memory with fewer cache misses and less pointer chasing.A contiguous
std::vector<Feature>removes the hash-table traversal and heap/pointer indirection, while retaining the existingFeatureobject and its interface. Astd::vector<Feature::Ptr>removes the hash-table traversal but retains the pointer indirection. Comparing these four representations therefore gives a useful indication of where the observed overhead originates:unordered_map<TrackletId, Feature::Ptr>— current implementation.vector<Feature::Ptr>— removes hash-map traversal but retains pointers.vector<Feature>— contiguous feature objects with the existing feature representation.The important point is that the SoA comparison is not purely an AoS-versus-SoA comparison. The existing implementation combines a hash map, pointers/heap allocation, the
Featureclass representation, and its accessors. The benchmark is intended to quantify the practical cost of the complete representation used by the current pipeline.FeatureIterationBenchmarkResultsThe first benchmark compared the existing
FeatureContaineragainst a simple SoA representation. Across feature counts from 100 to 20,000, the results were approximately:FeatureContainerThe important result is that the performance ratio remained approximately constant as the number of features increased. The current container required roughly 210–223 ns per feature, whereas the SoA required roughly 10 ns per feature.
For example, at 10,000 features, one complete traversal corresponds to approximately 2.13 ms for the existing container compared with approximately 0.10 ms for the SoA. This represents approximately 2 ms of additional cost for a single traversal of 10,000 features.
FeatureStorageBenchmarkResultsThe second benchmark was designed to determine whether this difference was primarily caused by the hash map, pointer indirection, or the
Featurerepresentation itself.The results were:
vector<Feature::Ptr>vector<Feature>The results show a clear progression as the storage representation becomes simpler.
Replacing the
unordered_mapwith avector<Feature::Ptr>reduced the measured cost from approximately 175 ns/feature to approximately 165–169 ns/feature. This is only around a 5–6% reduction, indicating that hash-map traversal is not the dominant source of the overhead in this particular benchmark.Replacing the pointer vector with a
vector<Feature>reduced the cost further to approximately 148–150 ns/feature. This suggests that pointer indirection and the associated non-contiguous placement of individualFeatureobjects contribute additional overhead.However, the largest difference remains between
vector<Feature>and the SoA representation. The contiguousFeaturerepresentation still costs approximately 15× more per feature than the SoA. This indicates that the structure and access pattern of theFeatureobject itself are a substantial part of the measured cost when compared with a representation containing only the small set of scalar properties required by the benchmark.Interpretation
The benchmarks indicate that the large performance difference is not primarily caused by
std::unordered_map. Moving from the map to a vector of feature pointers produced only a relatively small improvement.The results instead suggest that several smaller costs accumulate in the current representation, including pointer indirection, the memory layout of the
Featureobject, and accessing properties through the existing feature abstraction. The SoA benefits from having exactly the required data in compact, contiguous arrays, which is particularly favourable for sequential CPU processing and cache utilisation.The approximately 21× difference observed in the first benchmark should therefore not be interpreted as saying that "SoA is 21× faster than AoS" in general. It is more accurately described as the measured difference between the current
FeatureContainerimplementation and the simplified SoA representation used in this benchmark.The benchmark also shows that the performance characteristics are stable over the tested range. There was little change in ns/feature between 100 and 20,000 features, suggesting that the dominant cost is approximately linear in the number of features and that the relative overhead is not caused by a particular large-data threshold within this range.
Relevance to Dynamic SLAM / Feature Tracking
These results are useful for the current optimisation work because feature tracking is typically dominated by operations that repeatedly access feature properties such as keypoints, track IDs, object IDs, ages, and inlier status. If the same collection of features is traversed many times during a frame, the per-feature overhead can accumulate.
For example, using the
FeatureIterationBenchmarkresult at 10,000 features, the measured difference between the current representation and the SoA was approximately 2.0 ms for one traversal. If an equivalent feature traversal were performed five times in a frame, the corresponding difference would be roughly 10 ms; ten such traversals would be roughly 20 ms. These numbers are illustrative extrapolations rather than measurements of the complete pipeline.This makes the result potentially significant for a high-frequency visual tracking pipeline. At 20 Hz, the total frame budget is 50 ms, so several milliseconds spent repeatedly traversing feature data can become a meaningful fraction of the available budget.
However, these benchmarks do not by themselves establish that the entire feature system should be rewritten as an SoA. The benchmark isolates storage and iteration costs rather than measuring the complete tracking pipeline. The current
Featureabstraction may also provide functionality that is valuable for correctness, maintainability, and different access patterns. In addition, the benchmark only measures the particular operations included in these tests.The results therefore provide evidence that feature storage and iteration are worth considering as an optimisation target, especially if profiling shows that these operations occur repeatedly in the tracking frontend. They do not, on their own, justify replacing the current representation everywhere.
Conclusions
The benchmarks demonstrate that the current feature representation has measurable per-feature overhead compared with a compact SoA representation.
The main observations are:
FeatureContaineris approximately 20–22× slower per feature than the tested SoA in the first iteration benchmark.unordered_mapwithvector<Feature::Ptr>only reduced the cost by approximately 5–6%, suggesting that the hash map is not the dominant bottleneck.vector<Feature::Ptr>withvector<Feature>reduced the cost further, showing that pointer indirection and object placement contribute measurable overhead.vector<Feature>, showing that the feature object's representation/access pattern is also important.Overall, the results provide a strong indication that a more data-oriented feature representation could be beneficial for performance-critical sections of the Dynamic SLAM frontend. The next step should be to benchmark the specific feature-access patterns used by the actual tracking pipeline and determine whether an SoA or a hybrid representation can reduce runtime without unnecessarily complicating the rest of the system.