java-topology/defects/box2d/unit/Box2DTest.java

180 lines
6.8 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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");
}
}