fbneo-0001: BurnDrvGetIndex O(N) linear scan over 24493 drivers -> O(1) unordered_map, 91x speedup at N=5000. Fix: hash index built at BurnLibInit, cleared at BurnLibExit. mame: confirmed CLEAN on all 5 MOADs (binary search for driver lookup, single-threaded).
139 lines
4.5 KiB
C++
139 lines
4.5 KiB
C++
// Unit test for fbneo-0001: BurnDrvGetIndex O(N) linear scan -> O(1) hash lookup.
|
|
// Models the defect: with 5000+ game drivers, every call to BurnDrvGetIndex
|
|
// (game load, state load, Kaillera/netplay) scanned the full driver array.
|
|
// Fix: build std::unordered_map<string, INT32> at BurnLibInit time, O(1) lookup.
|
|
|
|
#include <cassert>
|
|
#include <cstdio>
|
|
#include <chrono>
|
|
#include <string>
|
|
#include <vector>
|
|
#include <unordered_map>
|
|
#include <cstring>
|
|
|
|
// Minimal driver struct matching FBNeo's BurnDriver
|
|
struct BurnDriver {
|
|
const char* szShortName;
|
|
};
|
|
|
|
// Original: O(N) linear scan
|
|
static int drv_get_index_original(const std::vector<BurnDriver*>& pDriver,
|
|
const char* szName)
|
|
{
|
|
if (!szName) return -1;
|
|
for (int i = 0; i < (int)pDriver.size(); i++) {
|
|
if (strcmp(szName, pDriver[i]->szShortName) == 0)
|
|
return i;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
// Patched: O(1) hash lookup
|
|
struct PatchedIndex {
|
|
std::unordered_map<std::string, int> map;
|
|
|
|
void build(const std::vector<BurnDriver*>& pDriver) {
|
|
map.clear();
|
|
map.reserve(pDriver.size());
|
|
for (int i = 0; i < (int)pDriver.size(); i++)
|
|
map[pDriver[i]->szShortName] = i;
|
|
}
|
|
|
|
int get(const char* szName) const {
|
|
if (!szName) return -1;
|
|
auto it = map.find(szName);
|
|
return (it != map.end()) ? it->second : -1;
|
|
}
|
|
};
|
|
|
|
int main()
|
|
{
|
|
// Build driver pool modeling FBNeo's ~5000 game drivers
|
|
const int N_DRIVERS = 5000;
|
|
std::vector<std::string> names;
|
|
names.reserve(N_DRIVERS);
|
|
for (int i = 0; i < N_DRIVERS; i++)
|
|
names.push_back("drv_" + std::to_string(i));
|
|
|
|
std::vector<BurnDriver> drivers(N_DRIVERS);
|
|
std::vector<BurnDriver*> pDriver(N_DRIVERS);
|
|
for (int i = 0; i < N_DRIVERS; i++) {
|
|
drivers[i].szShortName = names[i].c_str();
|
|
pDriver[i] = &drivers[i];
|
|
}
|
|
|
|
PatchedIndex idx;
|
|
idx.build(pDriver);
|
|
|
|
// Test 1: found at index 0 (best case original, same patched)
|
|
{
|
|
int orig = drv_get_index_original(pDriver, "drv_0");
|
|
int patched = idx.get("drv_0");
|
|
assert(orig == 0);
|
|
assert(patched == 0);
|
|
printf("PASS: lookup first driver\n");
|
|
}
|
|
|
|
// Test 2: found at last index (worst case original)
|
|
{
|
|
int orig = drv_get_index_original(pDriver, "drv_4999");
|
|
int patched = idx.get("drv_4999");
|
|
assert(orig == 4999);
|
|
assert(patched == 4999);
|
|
printf("PASS: lookup last driver\n");
|
|
}
|
|
|
|
// Test 3: not found returns -1
|
|
{
|
|
int orig = drv_get_index_original(pDriver, "no_such_game");
|
|
int patched = idx.get("no_such_game");
|
|
assert(orig == -1);
|
|
assert(patched == -1);
|
|
printf("PASS: missing driver returns -1\n");
|
|
}
|
|
|
|
// Test 4: null returns -1
|
|
{
|
|
int orig = drv_get_index_original(pDriver, nullptr);
|
|
int patched = idx.get(nullptr);
|
|
assert(orig == -1);
|
|
assert(patched == -1);
|
|
printf("PASS: null name returns -1\n");
|
|
}
|
|
|
|
// Test 5: correctness across all drivers
|
|
{
|
|
for (int i = 0; i < N_DRIVERS; i += 100) {
|
|
assert(drv_get_index_original(pDriver, names[i].c_str()) == i);
|
|
assert(idx.get(names[i].c_str()) == i);
|
|
}
|
|
printf("PASS: correctness across all %d drivers\n", N_DRIVERS);
|
|
}
|
|
|
|
// Test 6: performance -- lookup all N drivers once (covers entire range, no elision)
|
|
{
|
|
volatile int sink = 0;
|
|
|
|
auto t0 = std::chrono::high_resolution_clock::now();
|
|
for (int i = 0; i < N_DRIVERS; i++)
|
|
sink += drv_get_index_original(pDriver, names[i].c_str());
|
|
auto t1 = std::chrono::high_resolution_clock::now();
|
|
for (int i = 0; i < N_DRIVERS; i++)
|
|
sink += idx.get(names[i].c_str());
|
|
auto t2 = std::chrono::high_resolution_clock::now();
|
|
(void)sink;
|
|
|
|
double orig_us = std::chrono::duration_cast<std::chrono::microseconds>(t1 - t0).count();
|
|
double patch_us = std::chrono::duration_cast<std::chrono::microseconds>(t2 - t1).count();
|
|
double ratio = orig_us / (patch_us > 0.0 ? patch_us : 1.0);
|
|
|
|
printf("PASS: performance N=%d all-driver scan: original=%.0fus patched=%.0fus ratio=%.1fx\n",
|
|
N_DRIVERS, orig_us, patch_us, ratio);
|
|
// Scanning all N=5000 drivers takes O(N^2/2) comparisons vs O(N) hash lookups.
|
|
// Even with vectorization the ratio must exceed 5x at N=5000.
|
|
assert(ratio > 5.0);
|
|
}
|
|
|
|
printf("ALL TESTS PASSED\n");
|
|
return 0;
|
|
}
|