// Unit test for widelands-0001: FindBobsCallback O(N^2) dedup // Simulates the pattern from src/logic/map.cc FindBobsCallback // Demonstrates O(N^2) with std::find vs O(N) with unordered_set #include #include #include #include #include #include #include // Simulate Bob pointers as unique integers cast to void* using Bob = void; struct FindBobsCallbackBefore { std::vector* list_; uint32_t found_{0U}; void add_bob(Bob* bob) { if (list_ != nullptr) { // DEFECT: O(N) scan per insertion if (std::find(list_->begin(), list_->end(), bob) != list_->end()) { return; } list_->push_back(bob); } ++found_; } }; struct FindBobsCallbackAfter { std::vector* list_; std::unordered_set seen_; uint32_t found_{0U}; void add_bob(Bob* bob) { if (list_ != nullptr) { // FIX: O(1) lookup if (seen_.count(bob) != 0) { return; } list_->push_back(bob); seen_.insert(bob); } ++found_; } }; int main() { // Simulate finding B bobs across fields, with ~50% duplicates const int B = 2000; // Create bob pool (half the total insertions, ensuring duplicates) std::vector bob_pool; for (int i = 0; i < B; ++i) { bob_pool.push_back(reinterpret_cast(static_cast(i + 1))); } // Create insertion sequence: each bob inserted twice (simulating adjacent fields) std::vector insertions; for (int i = 0; i < B; ++i) { insertions.push_back(bob_pool[i]); } for (int i = 0; i < B; ++i) { insertions.push_back(bob_pool[i]); } // --- BEFORE (O(N^2)) --- std::vector list_before; FindBobsCallbackBefore cb_before; cb_before.list_ = &list_before; auto t0 = std::chrono::high_resolution_clock::now(); for (Bob* bob : insertions) { cb_before.add_bob(bob); } auto t1 = std::chrono::high_resolution_clock::now(); double ms_before = std::chrono::duration(t1 - t0).count(); // --- AFTER (O(N)) --- std::vector list_after; FindBobsCallbackAfter cb_after; cb_after.list_ = &list_after; auto t2 = std::chrono::high_resolution_clock::now(); for (Bob* bob : insertions) { cb_after.add_bob(bob); } auto t3 = std::chrono::high_resolution_clock::now(); double ms_after = std::chrono::duration(t3 - t2).count(); // Correctness: same results assert(list_before.size() == list_after.size()); assert(list_before.size() == static_cast(B)); assert(cb_before.found_ == cb_after.found_); // Both lists should contain exactly the same bobs std::sort(list_before.begin(), list_before.end()); std::sort(list_after.begin(), list_after.end()); assert(list_before == list_after); double ratio = ms_before / ms_after; printf("FindBobsCallback dedup (B=%d, %d insertions):\n", B, (int)insertions.size()); printf(" BEFORE (std::find): %.3f ms\n", ms_before); printf(" AFTER (unordered_set): %.3f ms\n", ms_after); printf(" Ratio: %.1fx\n", ratio); // Expect meaningful speedup assert(ratio > 2.0); printf("PASS\n"); return 0; }