wave16: dart-0001/2/3 + octave-0002 + php-0003/4 + cassandra-0005 + erlang-0003 + chef-0001 — 542/240

This commit is contained in:
russell@unturf.com 2026-03-27 19:36:35 -04:00
parent 31850d5ef6
commit d816d3c74c
18 changed files with 1902 additions and 6 deletions

View file

@ -0,0 +1,119 @@
# octave-0002: load_path::add — O(D²) directory-presence scan on path initialization
## Summary
| Field | Value |
|-------------|-------|
| ID | octave-0002 |
| Severity | MEDIUM |
| CWE | CWE-407 (Algorithmic Complexity) |
| File | `libinterp/corefcn/load-path.cc` |
| Functions | `load_path::find_dir_info`, `load_path::add` |
| Lines | 10211055, 11051155 |
| Speedup | ~500x op-count at D=1000 dirs |
## Defect
Every call to `load_path::add(dir, ...)` calls `find_dir_info(dir)` (lines 1119, 1151),
which is an O(D) linear scan over the `m_dir_info_list`:
```cpp
// load-path.cc:1029
while (retval != m_dir_info_list.cend ())
{
if (retval->m_dir_name == dir) // O(D) string compare per call
break;
retval++;
}
```
`load_path::set()` (called at startup from `initialize()`) loops over all N
path elements and calls `append()``add()` for each one:
```cpp
// load-path.cc:346
for (const auto& elt : elts)
append (elt, warn); // each append → add → find_dir_info (O(D))
```
Since `m_dir_info_list` grows by 1 per iteration, the total cost is:
`1 + 2 + ... + N = O(N²)`.
Additionally, each `add()` call issues a **second** `find_dir_info(".")` call at
line 1151 to keep "." at the front — doubling the quadratic work.
A standard Octave installation ships with ~200+ path directories via
`OCTAVE_PATH` and `restoredefaultpath`. Toolbox-heavy environments (Octave Forge,
biomedical packages) can exceed 500+ directories.
## Root Cause
`m_dir_info_list` is a `std::list<dir_info>` with no parallel index.
Membership is checked by linear scan every time a directory is added.
This is O(N²) overall for N-directory path initialization.
## Fix
Maintain a `std::unordered_set<std::string> m_dir_name_set` in `load_path`
alongside `m_dir_info_list`. Replace the `find_dir_info` loop with O(1)
set membership, and maintain the set in sync with the list on add/remove.
```diff
--- a/libinterp/corefcn/load-path.h
+++ b/libinterp/corefcn/load-path.h
@@ -... @@
+ std::unordered_set<std::string> m_dir_name_set;
--- a/libinterp/corefcn/load-path.cc
+++ b/libinterp/corefcn/load-path.cc
@@ -1021,10 +1021,12 @@
load_path::find_dir_info (const std::string& dir_arg) const
{
std::string dir = ...;
- auto retval = m_dir_info_list.cbegin ();
- while (retval != m_dir_info_list.cend ())
- {
- if (retval->m_dir_name == dir)
- break;
- retval++;
- }
- return retval;
+ // O(1) fast path: check set membership before walking the list
+ if (m_dir_name_set.find (dir) == m_dir_name_set.end ())
+ return m_dir_info_list.cend ();
+ // Only walk list to get iterator when dir is actually present (rare)
+ auto retval = m_dir_info_list.cbegin ();
+ while (retval != m_dir_info_list.cend ())
+ {
+ if (retval->m_dir_name == dir) break;
+ retval++;
+ }
+ return retval;
}
@@ -1119 +1121 @@
+ m_dir_name_set.insert (di.m_dir_name);
if (at_end)
m_dir_info_list.push_back (di);
@@ -remove @@
+ m_dir_name_set.erase (di.m_dir_name);
m_dir_info_list.erase (...);
```
The fix eliminates the O(D) scan for the common case (dir not yet present).
Total path initialization cost drops from O(D²) to O(D).
## Measurement
Simulated with `OctaveLoadPathAlgorithm.java`:
| D | slow ops | fast ops | ratio |
|------|----------|----------|-------|
| 100 | 10,200 | 200 | 51× |
| 500 | 251,000 | 1,000 | 251× |
| 1000 | 1,002,000| 2,000 | 501× |
## References
- `libinterp/corefcn/load-path.cc` lines 10211055 (`find_dir_info`)
- `libinterp/corefcn/load-path.cc` lines 11051155 (`add`)
- `libinterp/corefcn/load-path.cc` lines 346347 (`set` loop)