netpanzer-0001: UnitInterface::removeUnit std::find on PlayerUnitList vector O(D*U) during mass destruction. Fix: unordered_map index + swap-and-pop O(1) removal. MEDIUM, 3.7x at U=2000 D=1000. netpanzer-0002: UnitBucketArray::getUnitBucketIndex scans all buckets O(B*U) per misplaced unit in sortBucketArray fallback path. Fix: unordered_map<UnitID, bucket_index> for O(1) lookup. HIGH, 9.4x at B=200 U=2000 M=500. MOAD 0003 (ThreadLocal): CLEAN, no thread_local patterns MOAD 0004 (Logged Secret): CLEAN, passwords not logged verbatim MOAD 0005 (Thundering Herd): CLEAN, PathCache is single-threaded
208 lines
6.8 KiB
C++
208 lines
6.8 KiB
C++
// Unit test for netpanzer-0002: UnitBucketArray::getUnitBucketIndex O(B*U) -> O(1)
|
|
// CWE-407: Algorithmic Complexity — full bucket array scan for unit lookup
|
|
//
|
|
// Defect: getUnitBucketIndex() scans ALL buckets (B) and traverses every linked
|
|
// list within each bucket to find which bucket a unit belongs to. This is
|
|
// O(total_units) per call. Called from moveUnit() fallback when a unit is not
|
|
// in its expected bucket. sortBucketArray() calls moveUnit() for each misplaced
|
|
// unit, yielding O(M * total_units) = O(N^2) when M units are misplaced after
|
|
// heavy movement (common in tank battles with many units moving simultaneously).
|
|
//
|
|
// Fix: maintain std::unordered_map<UnitID, unsigned long> mapping unit IDs to
|
|
// bucket indices. Updated on add, move, and delete. getUnitBucketIndex becomes O(1).
|
|
|
|
#include <algorithm>
|
|
#include <cassert>
|
|
#include <chrono>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <list>
|
|
#include <unordered_map>
|
|
#include <vector>
|
|
|
|
using UnitID = unsigned short;
|
|
|
|
// ========== DEFECTIVE VERSION: linear scan of all buckets ==========
|
|
struct DefectiveBucketArray {
|
|
int num_buckets;
|
|
std::vector<std::list<UnitID>> buckets;
|
|
|
|
DefectiveBucketArray(int n) : num_buckets(n), buckets(n) {}
|
|
|
|
void addUnit(UnitID id, int bucket) {
|
|
buckets[bucket].push_back(id);
|
|
}
|
|
|
|
// O(total_units) full scan
|
|
int getUnitBucketIndex(UnitID id) {
|
|
for (int b = 0; b < num_buckets; b++) {
|
|
for (auto& uid : buckets[b]) {
|
|
if (uid == id) return b;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
bool moveUnit(UnitID id, int from, int to) {
|
|
for (auto it = buckets[from].begin(); it != buckets[from].end(); ++it) {
|
|
if (*it == id) {
|
|
buckets[from].erase(it);
|
|
buckets[to].push_back(id);
|
|
return true;
|
|
}
|
|
}
|
|
// Fallback: full scan
|
|
int actual = getUnitBucketIndex(id);
|
|
if (actual >= 0) {
|
|
return moveUnit(id, actual, to);
|
|
}
|
|
return false;
|
|
}
|
|
};
|
|
|
|
// ========== FIXED VERSION: hash map for O(1) lookup ==========
|
|
struct FixedBucketArray {
|
|
int num_buckets;
|
|
std::vector<std::list<UnitID>> buckets;
|
|
std::unordered_map<UnitID, int> unitBucketMap;
|
|
|
|
FixedBucketArray(int n) : num_buckets(n), buckets(n) {}
|
|
|
|
void addUnit(UnitID id, int bucket) {
|
|
buckets[bucket].push_back(id);
|
|
unitBucketMap[id] = bucket;
|
|
}
|
|
|
|
// O(1) lookup
|
|
int getUnitBucketIndex(UnitID id) {
|
|
auto it = unitBucketMap.find(id);
|
|
return it != unitBucketMap.end() ? it->second : -1;
|
|
}
|
|
|
|
bool moveUnit(UnitID id, int from, int to) {
|
|
for (auto it = buckets[from].begin(); it != buckets[from].end(); ++it) {
|
|
if (*it == id) {
|
|
buckets[from].erase(it);
|
|
buckets[to].push_back(id);
|
|
unitBucketMap[id] = to;
|
|
return true;
|
|
}
|
|
}
|
|
// Fallback: O(1) lookup instead of full scan
|
|
int actual = getUnitBucketIndex(id);
|
|
if (actual >= 0) {
|
|
return moveUnit(id, actual, to);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void removeUnit(UnitID id, int bucket) {
|
|
buckets[bucket].remove(id);
|
|
unitBucketMap.erase(id);
|
|
}
|
|
};
|
|
|
|
static long long now_ns() {
|
|
return std::chrono::high_resolution_clock::now().time_since_epoch().count();
|
|
}
|
|
|
|
int main() {
|
|
// Test correctness
|
|
{
|
|
FixedBucketArray fixed(100);
|
|
for (UnitID i = 0; i < 200; i++) {
|
|
fixed.addUnit(i, i % 100);
|
|
}
|
|
|
|
// Verify lookups
|
|
for (UnitID i = 0; i < 200; i++) {
|
|
assert(fixed.getUnitBucketIndex(i) == (int)(i % 100));
|
|
}
|
|
|
|
// Move units to wrong buckets, then look them up
|
|
for (UnitID i = 0; i < 50; i++) {
|
|
fixed.moveUnit(i, i % 100, (i + 50) % 100);
|
|
}
|
|
|
|
for (UnitID i = 0; i < 50; i++) {
|
|
assert(fixed.getUnitBucketIndex(i) == (int)((i + 50) % 100));
|
|
}
|
|
|
|
// Remove and verify
|
|
fixed.removeUnit(0, 50);
|
|
assert(fixed.getUnitBucketIndex(0) == -1);
|
|
}
|
|
printf("PASS correctness\n");
|
|
|
|
// Benchmark: simulate sortBucketArray with misplaced units
|
|
// This triggers getUnitBucketIndex fallback in defective version
|
|
const int BUCKETS = 200;
|
|
const int UNITS = 2000;
|
|
const int MISPLACED = 500; // units that moved to wrong bucket
|
|
const int TRIALS = 50;
|
|
|
|
// Benchmark defective: misplaced units trigger fallback scan
|
|
long long defective_ns = 0;
|
|
for (int t = 0; t < TRIALS; t++) {
|
|
DefectiveBucketArray defective(BUCKETS);
|
|
for (UnitID i = 0; i < UNITS; i++) {
|
|
defective.addUnit(i, i % BUCKETS);
|
|
}
|
|
|
|
// Move MISPLACED units to wrong bucket (simulates movement between frames)
|
|
// Then try to move them from their "expected" bucket (will fail, trigger fallback)
|
|
for (UnitID i = 0; i < MISPLACED; i++) {
|
|
// Silently move unit to a different bucket (simulating stale bucket info)
|
|
int expected = i % BUCKETS;
|
|
int actual = (i + 1) % BUCKETS;
|
|
defective.buckets[expected].remove(i);
|
|
defective.buckets[actual].push_back(i);
|
|
}
|
|
|
|
long long start = now_ns();
|
|
for (UnitID i = 0; i < MISPLACED; i++) {
|
|
int expected = i % BUCKETS;
|
|
int target = (i + 2) % BUCKETS;
|
|
// This will fail at expected bucket, triggering full scan fallback
|
|
defective.moveUnit(i, expected, target);
|
|
}
|
|
defective_ns += now_ns() - start;
|
|
}
|
|
|
|
// Benchmark fixed
|
|
long long fixed_ns = 0;
|
|
for (int t = 0; t < TRIALS; t++) {
|
|
FixedBucketArray fixed(BUCKETS);
|
|
for (UnitID i = 0; i < UNITS; i++) {
|
|
fixed.addUnit(i, i % BUCKETS);
|
|
}
|
|
|
|
// Same setup: move units to wrong bucket
|
|
for (UnitID i = 0; i < MISPLACED; i++) {
|
|
int expected = i % BUCKETS;
|
|
int actual = (i + 1) % BUCKETS;
|
|
fixed.buckets[expected].remove(i);
|
|
fixed.buckets[actual].push_back(i);
|
|
fixed.unitBucketMap[i] = actual; // map stays correct
|
|
}
|
|
|
|
long long start = now_ns();
|
|
for (UnitID i = 0; i < MISPLACED; i++) {
|
|
int expected = i % BUCKETS;
|
|
int target = (i + 2) % BUCKETS;
|
|
fixed.moveUnit(i, expected, target);
|
|
}
|
|
fixed_ns += now_ns() - start;
|
|
}
|
|
|
|
double ratio = (double)defective_ns / (double)fixed_ns;
|
|
printf("Defective: %lld ns total (%d trials)\n", defective_ns, TRIALS);
|
|
printf("Fixed: %lld ns total (%d trials)\n", fixed_ns, TRIALS);
|
|
printf("Ratio: %.1fx speedup\n", ratio);
|
|
printf("B=%d buckets, U=%d units, M=%d misplaced\n", BUCKETS, UNITS, MISPLACED);
|
|
|
|
assert(ratio > 2.0 && "Fixed version should be at least 2x faster");
|
|
printf("PASS performance (%.1fx)\n", ratio);
|
|
|
|
return 0;
|
|
}
|