|
| 1 | +# CrossHair Issue #115: Trace Callback Performance Optimization |
| 2 | + |
| 3 | +## Summary |
| 4 | + |
| 5 | +This implementation addresses [Issue #115](https://github.com/pschanely/CrossHair/issues/115) by optimizing the trace callback performance in CrossHair. The issue noted that CrossHair spends a lot of execution time in pure-Python `sys.settrace` handlers, and moving more logic to C could significantly speed up CrossHair. |
| 6 | + |
| 7 | +## Changes Made |
| 8 | + |
| 9 | +### 1. C Extension Optimizations (`crosshair/_tracers.c`) |
| 10 | + |
| 11 | +#### Opcode-to-Handler Caching |
| 12 | +- **Problem**: For every traced opcode, the C code would iterate through all handler tables and call into Python, even when there's only one handler. |
| 13 | +- **Solution**: Added a simple cache that remembers the last opcode and its handler for single-handler scenarios. |
| 14 | +- **Implementation**: |
| 15 | + - Added `last_opcode` and `last_handler` fields to the CTracer struct |
| 16 | + - When there's only one handler table, check the cache first before iterating |
| 17 | + - Cache is invalidated when modules are pushed/popped |
| 18 | + |
| 19 | +#### Streamlined Post-Op Callback Processing |
| 20 | +- **Problem**: Post-op callback processing had nested conditionals and multiple exit points. |
| 21 | +- **Solution**: Refactored into a separate inline function `process_postop_callbacks()` with early returns for the common no-callback case. |
| 22 | + |
| 23 | +#### Cache Statistics |
| 24 | +- Added `cache_hits` and `cache_misses` counters for debugging |
| 25 | +- Added `get_cache_stats()` method to expose statistics to Python |
| 26 | +- Helps verify that optimizations are working effectively |
| 27 | + |
| 28 | +#### Handler Table Iteration Order |
| 29 | +- **Optimization**: Changed from forward iteration to reverse iteration |
| 30 | +- **Rationale**: More recently added handlers are likely to be more active; reverse iteration improves cache locality |
| 31 | + |
| 32 | +### 2. Header File Updates (`crosshair/_tracers.h`) |
| 33 | + |
| 34 | +Added new fields to the `CTracer` struct: |
| 35 | +```c |
| 36 | +int last_opcode; /* Cache: last opcode processed */ |
| 37 | +PyObject* last_handler; /* Cache: handler for last_opcode */ |
| 38 | +long cache_hits; /* Statistics: cache hits */ |
| 39 | +long cache_misses; /* Statistics: cache misses */ |
| 40 | +``` |
| 41 | + |
| 42 | +### 3. Python-Level Optimizations (`crosshair/tracers.py`) |
| 43 | + |
| 44 | +#### Fast-Path for Primitive Types |
| 45 | +- **Problem**: The `trace_op` method performs expensive attribute lookups for every target, even for primitive types that will never need tracing. |
| 46 | +- **Solution**: Added an early exit check for common primitive types: |
| 47 | +```python |
| 48 | +fast_path_types = (type(None), int, float, str, bool, list, dict, tuple, set) |
| 49 | +if target_type in fast_path_types: |
| 50 | + return None |
| 51 | +``` |
| 52 | + |
| 53 | +#### Optimized Exception Handling |
| 54 | +- **Problem**: Stack reads were wrapped in try/except blocks that always ran. |
| 55 | +- **Solution**: Combined operations that can fail into single try/except blocks to reduce overhead. |
| 56 | + |
| 57 | +#### Inline Attribute Lookups |
| 58 | +- **Problem**: Multiple `__getattribute__` calls for the same object. |
| 59 | +- **Solution**: Inlined the lookups to reduce function call overhead. |
| 60 | + |
| 61 | +### 4. Testing |
| 62 | + |
| 63 | +Created two test files: |
| 64 | + |
| 65 | +#### `tracers_performance_test.py` |
| 66 | +- Unit tests for the new fast-path behavior |
| 67 | +- Tests for cache statistics functionality |
| 68 | +- Ensures optimizations don't break existing functionality |
| 69 | + |
| 70 | +#### `tracers_performance_benchmark.py` |
| 71 | +- Benchmarks for various tracing scenarios |
| 72 | +- Measures cache hit rates |
| 73 | +- Demonstrates the impact of optimizations |
| 74 | + |
| 75 | +### 5. Documentation |
| 76 | + |
| 77 | +#### Updated `doc/source/changelog.rst` |
| 78 | +- Added entry for the next version documenting the performance improvements |
| 79 | + |
| 80 | +#### Added inline documentation |
| 81 | +- C code includes detailed comments explaining the optimizations |
| 82 | +- Python docstrings explain the fast-path behavior |
| 83 | + |
| 84 | +## Performance Impact |
| 85 | + |
| 86 | +### Expected Improvements |
| 87 | + |
| 88 | +1. **Reduced Python-to-C Transitions**: The opcode-to-handler cache avoids Python function calls for the common single-handler case. |
| 89 | + |
| 90 | +2. **Fewer Attribute Lookups**: The primitive type fast-path in Python eliminates expensive `__getattribute__` calls for common types. |
| 91 | + |
| 92 | +3. **Better Cache Locality**: Reverse iteration of handler tables improves CPU cache utilization. |
| 93 | + |
| 94 | +4. **Streamlined Processing**: The refactored post-op callback processing has fewer branches and better branch prediction. |
| 95 | + |
| 96 | +### Benchmarking |
| 97 | + |
| 98 | +Run the benchmark script to measure improvements: |
| 99 | +```bash |
| 100 | +python crosshair/tracers_performance_benchmark.py |
| 101 | +``` |
| 102 | + |
| 103 | +The benchmark tests: |
| 104 | +- Simple function calls |
| 105 | +- Nested function calls |
| 106 | +- Method calls |
| 107 | +- Loops with function calls |
| 108 | + |
| 109 | +## Technical Details |
| 110 | + |
| 111 | +### Cache Invalidation Strategy |
| 112 | + |
| 113 | +The opcode-to-handler cache is invalidated when: |
| 114 | +1. A new module is pushed (`CTracer_push_module`) |
| 115 | +2. A module is popped (`CTracer_pop_module`) |
| 116 | + |
| 117 | +This ensures that the cache never contains stale data while minimizing invalidation overhead. |
| 118 | + |
| 119 | +### Thread Safety Considerations |
| 120 | + |
| 121 | +The cache is per-CTracer instance, and CTracer instances are not shared between threads. The existing thread safety model of CrossHair is maintained. |
| 122 | + |
| 123 | +### Backward Compatibility |
| 124 | + |
| 125 | +All changes are backward compatible: |
| 126 | +- The `get_cache_stats()` method is new and doesn't affect existing code |
| 127 | +- The fast-path optimizations are transparent to callers |
| 128 | +- All existing tests should continue to pass |
| 129 | + |
| 130 | +## Future Work |
| 131 | + |
| 132 | +Potential additional optimizations: |
| 133 | + |
| 134 | +1. **More Sophisticated Caching**: Could implement an LRU cache for multiple handlers |
| 135 | +2. **Fast-Path for Common Patterns**: Identify and optimize common opcode sequences |
| 136 | +3. **Batch Processing**: Group multiple opcodes before calling into Python |
| 137 | +4. **Profile-Guided Optimization**: Use cache stats to identify the most common paths and optimize them further |
| 138 | + |
| 139 | +## Files Changed |
| 140 | + |
| 141 | +1. `crosshair/_tracers.c` - Core C extension optimizations |
| 142 | +2. `crosshair/_tracers.h` - Header file with new struct fields |
| 143 | +3. `crosshair/tracers.py` - Python-level fast-path optimizations |
| 144 | +4. `doc/source/changelog.rst` - Documentation update |
| 145 | +5. `crosshair/tracers_performance_test.py` - New test file |
| 146 | +6. `crosshair/tracers_performance_benchmark.py` - New benchmark file |
| 147 | + |
| 148 | +## References |
| 149 | + |
| 150 | +- Issue: https://github.com/pschanely/CrossHair/issues/115 |
| 151 | +- Discussion: https://github.com/pschanely/CrossHair/discussions/105#discussioncomment-662381 |
0 commit comments