220 lines
9.2 KiB
Java
220 lines
9.2 KiB
Java
package unit;
|
||
|
||
import java.util.*;
|
||
|
||
/**
|
||
* BulletTest — Java analogs of three Bullet Physics CWE-407 defects.
|
||
*
|
||
* Defect 1 (bullet-0001): btGhostObject — findLinearSearch on m_overlappingObjects
|
||
* in addOverlappingObjectInternal / removeOverlappingObjectInternal.
|
||
* Called per broadphase pair per simulation step.
|
||
* Slow: ArrayList.contains / indexOf — O(N) per call.
|
||
* Fast: HashMap with index — O(1) per call.
|
||
*
|
||
* Defect 2 (bullet-0002): btCollisionObject::checkCollideWithOverride —
|
||
* findLinearSearch on m_objectsWithoutCollisionCheck.
|
||
* Called inside needsCollision() for every pair in processAllOverlappingPairs.
|
||
* Slow: ArrayList.contains — O(E) per pair.
|
||
* Fast: HashSet.contains — O(1) per pair.
|
||
*
|
||
* Defect 3 (bullet-0003): btSortedOverlappingPairCache::findPair /
|
||
* removeOverlappingPair — findLinearSearch on m_overlappingPairArray.
|
||
* Called during broadphase pair removal phase.
|
||
* Slow: ArrayList.indexOf — O(P) per removal.
|
||
* Fast: HashMap<key, index> — O(1) per removal.
|
||
*/
|
||
public class BulletTest {
|
||
|
||
static void bench(String label, Runnable slow, Runnable fast, long sOps, long fOps) {
|
||
// warmup
|
||
slow.run();
|
||
fast.run();
|
||
long t0 = System.nanoTime();
|
||
slow.run();
|
||
long sMs = (System.nanoTime() - t0) / 1_000_000;
|
||
long t1 = System.nanoTime();
|
||
fast.run();
|
||
long fMs = (System.nanoTime() - t1) / 1_000_000;
|
||
double ratio = fOps > 0 ? (double) sOps / fOps : 0;
|
||
System.out.printf(" %-56s slow:%5dms (%,d ops) fast:%5dms (%,d ops) speedup:%.0fx%n",
|
||
label, sMs, sOps, fMs, fOps, ratio);
|
||
}
|
||
|
||
// -----------------------------------------------------------------------
|
||
// bullet-0001: btGhostObject overlapping-object add/remove per broadphase step
|
||
// -----------------------------------------------------------------------
|
||
static void benchGhostOverlapping(int P, int steps) {
|
||
// P objects start overlapping a ghost, then all leave — simulated over steps
|
||
|
||
// SLOW: ArrayList membership check — O(P) per add and remove
|
||
Runnable slow = () -> {
|
||
List<Object> overlapping = new ArrayList<>(P);
|
||
Object[] bodies = new Object[P];
|
||
for (int i = 0; i < P; i++) bodies[i] = new Object();
|
||
|
||
for (int s = 0; s < steps; s++) {
|
||
// add phase (each new overlap checks if already present)
|
||
overlapping.clear();
|
||
for (int i = 0; i < P; i++) {
|
||
if (!overlapping.contains(bodies[i])) // O(P)
|
||
overlapping.add(bodies[i]);
|
||
}
|
||
// remove phase
|
||
for (int i = 0; i < P; i++) {
|
||
int idx = overlapping.indexOf(bodies[i]); // O(P)
|
||
if (idx >= 0) {
|
||
overlapping.set(idx, overlapping.get(overlapping.size() - 1));
|
||
overlapping.remove(overlapping.size() - 1);
|
||
}
|
||
}
|
||
}
|
||
};
|
||
|
||
// FAST: HashMap<Object, Integer> index — O(1) per add and remove
|
||
Runnable fast = () -> {
|
||
List<Object> overlapping = new ArrayList<>(P);
|
||
Map<Object, Integer> index = new HashMap<>(P * 2);
|
||
Object[] bodies = new Object[P];
|
||
for (int i = 0; i < P; i++) bodies[i] = new Object();
|
||
|
||
for (int s = 0; s < steps; s++) {
|
||
// add phase
|
||
overlapping.clear();
|
||
index.clear();
|
||
for (int i = 0; i < P; i++) {
|
||
if (!index.containsKey(bodies[i])) { // O(1)
|
||
index.put(bodies[i], overlapping.size());
|
||
overlapping.add(bodies[i]);
|
||
}
|
||
}
|
||
// remove phase
|
||
for (int i = P - 1; i >= 0; i--) {
|
||
Integer pos = index.remove(bodies[i]); // O(1)
|
||
if (pos != null) {
|
||
int last = overlapping.size() - 1;
|
||
if (pos != last) {
|
||
Object moved = overlapping.get(last);
|
||
overlapping.set(pos, moved);
|
||
index.put(moved, pos);
|
||
}
|
||
overlapping.remove(last);
|
||
}
|
||
}
|
||
}
|
||
};
|
||
|
||
long sOps = (long) steps * P * P; // P adds × O(P) + P removes × O(P)
|
||
long fOps = (long) steps * P;
|
||
bench(String.format("bullet-0001 btGhostObject overlapping P=%d steps=%d", P, steps),
|
||
slow, fast, sOps, fOps);
|
||
}
|
||
|
||
// -----------------------------------------------------------------------
|
||
// bullet-0002: btCollisionObject::checkCollideWithOverride per pair per step
|
||
// -----------------------------------------------------------------------
|
||
static void benchCheckCollideWith(int M, int E, int steps) {
|
||
// M total pairs, E exclusions per object
|
||
Object[] colliders = new Object[E + 1];
|
||
for (int i = 0; i <= E; i++) colliders[i] = new Object();
|
||
|
||
// SLOW: ArrayList.contains — O(E) per pair
|
||
Runnable slow = () -> {
|
||
List<Object> exclusions = new ArrayList<>(Arrays.asList(colliders).subList(1, E + 1));
|
||
long dummy = 0;
|
||
for (int s = 0; s < steps; s++) {
|
||
for (int p = 0; p < M; p++) {
|
||
Object candidate = colliders[p % (E + 1)];
|
||
if (!exclusions.contains(candidate)) // O(E)
|
||
dummy++;
|
||
}
|
||
}
|
||
if (dummy < 0) System.out.println("never");
|
||
};
|
||
|
||
// FAST: HashSet.contains — O(1) per pair
|
||
Runnable fast = () -> {
|
||
Set<Object> exclusions = new HashSet<>(Arrays.asList(colliders).subList(1, E + 1));
|
||
long dummy = 0;
|
||
for (int s = 0; s < steps; s++) {
|
||
for (int p = 0; p < M; p++) {
|
||
Object candidate = colliders[p % (E + 1)];
|
||
if (!exclusions.contains(candidate)) // O(1)
|
||
dummy++;
|
||
}
|
||
}
|
||
if (dummy < 0) System.out.println("never");
|
||
};
|
||
|
||
long sOps = (long) steps * M * E;
|
||
long fOps = (long) steps * M;
|
||
bench(String.format("bullet-0002 checkCollideWith M=%d E=%d steps=%d", M, E, steps),
|
||
slow, fast, sOps, fOps);
|
||
}
|
||
|
||
// -----------------------------------------------------------------------
|
||
// bullet-0003: btSortedOverlappingPairCache::removeOverlappingPair — O(P) per remove
|
||
// -----------------------------------------------------------------------
|
||
static void benchSortedPairCacheRemove(int P) {
|
||
// P pairs, remove all of them (broadphase pair removal phase)
|
||
Integer[] pairs = new Integer[P];
|
||
for (int i = 0; i < P; i++) pairs[i] = i;
|
||
|
||
// SLOW: ArrayList.indexOf + remove — O(P) per removal
|
||
Runnable slow = () -> {
|
||
List<Integer> pairArray = new ArrayList<>(Arrays.asList(pairs));
|
||
|
||
for (int i = 0; i < P; i++) {
|
||
int idx = pairArray.indexOf(pairs[i]); // O(P)
|
||
if (idx >= 0) {
|
||
pairArray.set(idx, pairArray.get(pairArray.size() - 1));
|
||
pairArray.remove(pairArray.size() - 1);
|
||
}
|
||
}
|
||
};
|
||
|
||
// FAST: HashMap<key, index> — O(1) per removal
|
||
Runnable fast = () -> {
|
||
List<Integer> pairArray = new ArrayList<>(Arrays.asList(pairs));
|
||
Map<Integer, Integer> pairIndex = new HashMap<>(P * 2);
|
||
for (int i = 0; i < P; i++) pairIndex.put(pairs[i], i);
|
||
|
||
for (int i = 0; i < P; i++) {
|
||
Integer pos = pairIndex.remove(pairs[i]); // O(1)
|
||
if (pos != null) {
|
||
int last = pairArray.size() - 1;
|
||
if (pos != last) {
|
||
Integer moved = pairArray.get(last);
|
||
pairArray.set(pos, moved);
|
||
pairIndex.put(moved, pos);
|
||
}
|
||
pairArray.remove(last);
|
||
}
|
||
}
|
||
};
|
||
|
||
long sOps = (long) P * P / 2; // average scan P/2 × P removals
|
||
long fOps = P;
|
||
bench(String.format("bullet-0003 btSortedPairCache removeOverlappingPair P=%d", P),
|
||
slow, fast, sOps, fOps);
|
||
}
|
||
|
||
public static void main(String[] args) {
|
||
System.out.println("Bullet Physics CWE-407 defect benchmarks");
|
||
System.out.println("=".repeat(100));
|
||
|
||
System.out.println("\n[bullet-0001] btGhostObject::addOverlappingObjectInternal/removeOverlappingObjectInternal");
|
||
benchGhostOverlapping(50, 100);
|
||
benchGhostOverlapping(200, 50);
|
||
benchGhostOverlapping(500, 20);
|
||
|
||
System.out.println("\n[bullet-0002] btCollisionObject::checkCollideWithOverride per pair per step");
|
||
benchCheckCollideWith(500, 10, 100);
|
||
benchCheckCollideWith(1_000, 20, 50);
|
||
benchCheckCollideWith(2_000, 50, 20);
|
||
|
||
System.out.println("\n[bullet-0003] btSortedOverlappingPairCache::removeOverlappingPair");
|
||
benchSortedPairCacheRemove(500);
|
||
benchSortedPairCacheRemove(2_000);
|
||
benchSortedPairCacheRemove(10_000);
|
||
}
|
||
}
|