naev-0001: map.c Dijkstra/A* pathfinding uses linked-list open/closed sets with O(V) A_in() membership test and O(V) A_lowest() extract-min per iteration, making full pathfinding O(V^2 + E*V). Fix: array-indexed visited flags for O(1) membership, sorted-insert open list for O(1) extract-min. 102.5x at V=500 (Naev has 538 star systems). HIGH. naev-0002: tech.c tech_addGroupItemPrice() dedup scans growing output array linearly per item O(I*N) when building outfit/ship/commodity lists from tech groups. Fix: hash set for O(1) amortized dedup. 333x at N=1000. MEDIUM. MOAD-0002 (Intertangle): global stacks are standard C game engine pattern, subsystems largely independent. CLEAN. MOAD-0003 (Leaked Context): single thread_local in Rust RNG only. CLEAN. MOAD-0004 (Logged Secret): no credentials in single-player game. CLEAN. MOAD-0005 (Thundering Herd): single-threaded gameplay logic. CLEAN. 2/2 PASS, 2 defects.
148 lines
5.4 KiB
C++
148 lines
5.4 KiB
C++
// Unit test for freeorion-0002: Tech tree DFS cycle detection std::find on stack O(V*E*S)
|
|
// Demonstrates that using an unordered_set shadow of our DFS stack eliminates quadratic scanning.
|
|
//
|
|
// Our test models a "wide prereq" tree: each tech at depth D has all D-1 techs
|
|
// as prereqs. This causes repeated stack membership checks as each prereq is
|
|
// tested against the current stack.
|
|
|
|
#include <cassert>
|
|
#include <chrono>
|
|
#include <iostream>
|
|
#include <string>
|
|
#include <unordered_map>
|
|
#include <unordered_set>
|
|
#include <vector>
|
|
|
|
using TechTree = std::unordered_map<int, std::vector<int>>;
|
|
|
|
// -----------------------------------------------------------------------
|
|
// DEFECT: std::find on vector stack for cycle detection.
|
|
// For each prereq of each tech, scan the entire stack vector.
|
|
// -----------------------------------------------------------------------
|
|
static int __attribute__((noinline)) scan_ops_linear(const TechTree& tree, int num_techs) {
|
|
volatile int scan_ops = 0;
|
|
std::unordered_set<int> checked;
|
|
|
|
for (int t = num_techs - 1; t >= 0; --t) {
|
|
if (checked.count(t)) continue;
|
|
|
|
std::vector<int> stack;
|
|
stack.push_back(t);
|
|
|
|
while (!stack.empty()) {
|
|
int current = stack.back();
|
|
size_t start_size = stack.size();
|
|
|
|
auto it = tree.find(current);
|
|
if (it != tree.end()) {
|
|
for (int prereq : it->second) {
|
|
if (checked.count(prereq)) continue;
|
|
// Linear scan
|
|
bool found = false;
|
|
for (int i = static_cast<int>(stack.size()) - 1; i >= 0; --i) {
|
|
scan_ops = scan_ops + 1;
|
|
if (stack[i] == prereq) { found = true; break; }
|
|
}
|
|
if (!found) {
|
|
stack.push_back(prereq);
|
|
}
|
|
}
|
|
}
|
|
if (start_size == stack.size()) {
|
|
stack.pop_back();
|
|
checked.insert(current);
|
|
}
|
|
}
|
|
}
|
|
return scan_ops;
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// FIX: unordered_set shadow for O(1) membership
|
|
// -----------------------------------------------------------------------
|
|
static int __attribute__((noinline)) scan_ops_hashset(const TechTree& tree, int num_techs) {
|
|
volatile int scan_ops = 0;
|
|
std::unordered_set<int> checked;
|
|
|
|
for (int t = num_techs - 1; t >= 0; --t) {
|
|
if (checked.count(t)) continue;
|
|
|
|
std::vector<int> stack;
|
|
std::unordered_set<int> stack_set;
|
|
stack.push_back(t);
|
|
stack_set.insert(t);
|
|
|
|
while (!stack.empty()) {
|
|
int current = stack.back();
|
|
size_t start_size = stack.size();
|
|
|
|
auto it = tree.find(current);
|
|
if (it != tree.end()) {
|
|
for (int prereq : it->second) {
|
|
if (checked.count(prereq)) continue;
|
|
scan_ops = scan_ops + 1;
|
|
if (!stack_set.count(prereq)) {
|
|
stack.push_back(prereq);
|
|
stack_set.insert(prereq);
|
|
}
|
|
}
|
|
}
|
|
if (start_size == stack.size()) {
|
|
stack.pop_back();
|
|
stack_set.erase(current);
|
|
checked.insert(current);
|
|
}
|
|
}
|
|
}
|
|
return scan_ops;
|
|
}
|
|
|
|
int main() {
|
|
// Build a chain with fan-in: tech i depends on all techs 0..i-1
|
|
// This creates maximum stack depth at each step
|
|
// and forces many linear scans of a growing stack.
|
|
constexpr int N = 500;
|
|
TechTree tree;
|
|
for (int i = 1; i < N; ++i) {
|
|
std::vector<int> prereqs;
|
|
// Each tech depends on the previous few techs (capped to avoid explosion)
|
|
int start = std::max(0, i - 50);
|
|
for (int j = start; j < i; ++j)
|
|
prereqs.push_back(j);
|
|
tree[i] = prereqs;
|
|
}
|
|
|
|
int linear_ops = scan_ops_linear(tree, N);
|
|
int hashset_ops = scan_ops_hashset(tree, N);
|
|
|
|
double op_ratio = (hashset_ops > 0) ? static_cast<double>(linear_ops) / hashset_ops : 999.0;
|
|
|
|
std::cout << "N=" << N << " techs (fan-in DAG)" << std::endl;
|
|
std::cout << "linear scan ops (defect): " << linear_ops << std::endl;
|
|
std::cout << "hashset ops (fix): " << hashset_ops << std::endl;
|
|
std::cout << "op ratio: " << op_ratio << "x" << std::endl;
|
|
|
|
// Timing
|
|
constexpr int ITERS = 50;
|
|
auto t0 = std::chrono::high_resolution_clock::now();
|
|
for (int r = 0; r < ITERS; ++r)
|
|
scan_ops_linear(tree, N);
|
|
auto t1 = std::chrono::high_resolution_clock::now();
|
|
|
|
for (int r = 0; r < ITERS; ++r)
|
|
scan_ops_hashset(tree, N);
|
|
auto t2 = std::chrono::high_resolution_clock::now();
|
|
|
|
auto linear_us = std::chrono::duration_cast<std::chrono::microseconds>(t1 - t0).count();
|
|
auto hashset_us = std::chrono::duration_cast<std::chrono::microseconds>(t2 - t1).count();
|
|
double time_ratio = (hashset_us > 0) ? static_cast<double>(linear_us) / hashset_us : 999.0;
|
|
|
|
std::cout << "linear time (defect): " << linear_us << " us" << std::endl;
|
|
std::cout << "hashset time (fix): " << hashset_us << " us" << std::endl;
|
|
std::cout << "time ratio: " << time_ratio << "x" << std::endl;
|
|
|
|
assert(op_ratio > 5.0 && "Expected significant op-count reduction from hashset membership");
|
|
|
|
std::cout << "PASS" << std::endl;
|
|
return 0;
|
|
}
|