180 lines
6.8 KiB
Java
180 lines
6.8 KiB
Java
package unit;
|
||
|
||
import java.util.*;
|
||
|
||
/**
|
||
* Box2D CWE-407 benchmark: b2UnBufferMove linear scan vs O(1) index map.
|
||
*
|
||
* Defect: src/broad_phase.c b2UnBufferMove() lines 77-87
|
||
* Linear scan of moveArray to find the index for removal after
|
||
* O(1) hash-set removal. With N shapes all buffered for movement,
|
||
* destroying all bodies triggers N × N/2 comparisons on average.
|
||
*
|
||
* Fix: Maintain a parallel int[] indexMap[proxyKey] → array position.
|
||
* On RemoveSwap, update the displaced element's entry. O(1) removal.
|
||
*
|
||
* ticket: docs/tickets/box2d-0001-broad-phase-unbuffer-move-linear-scan.md
|
||
*/
|
||
public class Box2DTest {
|
||
|
||
// -----------------------------------------------------------------------
|
||
// SLOW: mirrors b2UnBufferMove — hash set O(1) + linear array scan O(n)
|
||
// -----------------------------------------------------------------------
|
||
static class SlowMoveBuffer {
|
||
Set<Integer> moveSet = new HashSet<>();
|
||
List<Integer> moveArray = new ArrayList<>();
|
||
|
||
void bufferMove(int proxyKey) {
|
||
if (moveSet.add(proxyKey)) {
|
||
moveArray.add(proxyKey);
|
||
}
|
||
}
|
||
|
||
/** Linear search — exact mirror of box2d broad_phase.c lines 77-87. */
|
||
void unBufferMove(int proxyKey) {
|
||
if (moveSet.remove(proxyKey)) {
|
||
// "Purge from move buffer. Linear search."
|
||
for (int i = 0; i < moveArray.size(); i++) {
|
||
if (moveArray.get(i) == proxyKey) {
|
||
// RemoveSwap: replace with last, shrink
|
||
int last = moveArray.size() - 1;
|
||
moveArray.set(i, moveArray.get(last));
|
||
moveArray.remove(last);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// -----------------------------------------------------------------------
|
||
// FAST: O(1) removal via index map (the proposed fix)
|
||
// -----------------------------------------------------------------------
|
||
static class FastMoveBuffer {
|
||
Set<Integer> moveSet = new HashSet<>();
|
||
int[] moveArray = new int[4096];
|
||
int count = 0;
|
||
Map<Integer, Integer> indexMap = new HashMap<>(); // proxyKey → array index
|
||
|
||
void bufferMove(int proxyKey) {
|
||
if (moveSet.add(proxyKey)) {
|
||
if (count == moveArray.length) {
|
||
moveArray = Arrays.copyOf(moveArray, count * 2);
|
||
}
|
||
indexMap.put(proxyKey, count);
|
||
moveArray[count++] = proxyKey;
|
||
}
|
||
}
|
||
|
||
void unBufferMove(int proxyKey) {
|
||
if (moveSet.remove(proxyKey)) {
|
||
int idx = indexMap.remove(proxyKey);
|
||
int last = count - 1;
|
||
if (idx != last) {
|
||
int displaced = moveArray[last];
|
||
moveArray[idx] = displaced;
|
||
indexMap.put(displaced, idx);
|
||
}
|
||
count--;
|
||
}
|
||
}
|
||
}
|
||
|
||
// -----------------------------------------------------------------------
|
||
// Bench harness
|
||
// -----------------------------------------------------------------------
|
||
static void bench(String label, Runnable slow, Runnable fast, long sOps, long fOps) {
|
||
// Warmup
|
||
slow.run(); fast.run();
|
||
// Slow timing
|
||
long t0 = System.nanoTime(); slow.run(); long sMs = (System.nanoTime() - t0) / 1_000_000;
|
||
// Fast timing
|
||
long t1 = System.nanoTime(); fast.run(); long fMs = (System.nanoTime() - t1) / 1_000_000;
|
||
double r = fOps > 0 ? (double) sOps / fOps : 0;
|
||
System.out.printf(" %-52s slow:%4dms (%,d ops) fast:%4dms (%,d ops) speedup:%.0fx%n",
|
||
label, sMs, sOps, fMs, fOps, r);
|
||
}
|
||
|
||
// -----------------------------------------------------------------------
|
||
// Scenarios
|
||
// -----------------------------------------------------------------------
|
||
|
||
/** N shapes all buffered, then all destroyed (body-destroy sweep). */
|
||
static Runnable slowDestroyAll(int n) {
|
||
return () -> {
|
||
SlowMoveBuffer buf = new SlowMoveBuffer();
|
||
for (int i = 0; i < n; i++) buf.bufferMove(i);
|
||
for (int i = 0; i < n; i++) buf.unBufferMove(i);
|
||
};
|
||
}
|
||
|
||
static Runnable fastDestroyAll(int n) {
|
||
return () -> {
|
||
FastMoveBuffer buf = new FastMoveBuffer();
|
||
for (int i = 0; i < n; i++) buf.bufferMove(i);
|
||
for (int i = 0; i < n; i++) buf.unBufferMove(i);
|
||
};
|
||
}
|
||
|
||
/** Interleaved add/remove (shape filter update pattern): N pairs. */
|
||
static Runnable slowInterleaved(int n) {
|
||
return () -> {
|
||
SlowMoveBuffer buf = new SlowMoveBuffer();
|
||
for (int i = 0; i < n; i++) {
|
||
buf.bufferMove(i);
|
||
buf.unBufferMove(i);
|
||
}
|
||
};
|
||
}
|
||
|
||
static Runnable fastInterleaved(int n) {
|
||
return () -> {
|
||
FastMoveBuffer buf = new FastMoveBuffer();
|
||
for (int i = 0; i < n; i++) {
|
||
buf.bufferMove(i);
|
||
buf.unBufferMove(i);
|
||
}
|
||
};
|
||
}
|
||
|
||
/** Fill half, then remove all (ContactManager pattern: some shapes static). */
|
||
static Runnable slowHalfFill(int n) {
|
||
return () -> {
|
||
SlowMoveBuffer buf = new SlowMoveBuffer();
|
||
for (int i = 0; i < n; i++) buf.bufferMove(i);
|
||
// remove only the dynamic half
|
||
for (int i = 0; i < n / 2; i++) buf.unBufferMove(i);
|
||
};
|
||
}
|
||
|
||
static Runnable fastHalfFill(int n) {
|
||
return () -> {
|
||
FastMoveBuffer buf = new FastMoveBuffer();
|
||
for (int i = 0; i < n; i++) buf.bufferMove(i);
|
||
for (int i = 0; i < n / 2; i++) buf.unBufferMove(i);
|
||
};
|
||
}
|
||
|
||
public static void main(String[] args) {
|
||
System.out.println("Box2D CWE-407: b2UnBufferMove linear scan vs O(1) index map");
|
||
System.out.println(" defect: src/broad_phase.c lines 77-87");
|
||
System.out.println();
|
||
|
||
int N = 800;
|
||
long ops = (long) N * N / 2; // approx comparisons in slow path
|
||
|
||
bench(String.format("destroy-all N=%d (body-destroy sweep)", N),
|
||
slowDestroyAll(N), fastDestroyAll(N), ops, N);
|
||
|
||
bench(String.format("interleaved N=%d (shape-filter update)", N),
|
||
slowInterleaved(N), fastInterleaved(N), ops, N);
|
||
|
||
bench(String.format("half-fill N=%d (mixed static/dynamic)", N),
|
||
slowHalfFill(N), fastHalfFill(N), ops, N);
|
||
|
||
System.out.println();
|
||
System.out.println("Fix: maintain HashMap<proxyKey, arrayIndex> alongside moveArray.");
|
||
System.out.println(" On RemoveSwap, update displaced element's index entry.");
|
||
System.out.println(" All operations O(1). See patch box2d-0001-broad-phase-index-map.patch");
|
||
}
|
||
}
|