# Исследование: Методы борьбы с комбинаторным взрывом в breakshaft ## Содержание 1. [Постановка проблемы](#1-постановка-проблемы) 2. [Анализ текущего состояния](#2-анализ-текущего-состояния) 3. [Варианты решений](#3-варианты-решений) 4. [Сравнительная таблица](#4-сравнительная-таблица) 5. [Рекомендации](#5-рекомендации) --- ## 1. Постановка проблемы ### 1.1. Где происходит комбинаторный взрыв? В `graph_walker.py::explode_callgraph_branches()`: ```python @classmethod def explode_callgraph_branches(cls, g: Callgraph, from_types: frozenset[type]) -> list[CallgraphVariant]: variants = [] for variant in g.variants: # ← Цикл 1 if len(variant.subgraphs) == 0: variants.append(variant) continue subg_combinations = [] for subg in variant.subgraphs: # ← Цикл 2 combinations = cls.explode_callgraph_branches(subg, from_types) # ← Рекурсия! subg_combinations.append(combinations) # ← КОМБИНАТОРНЫЙ ВЗРЫВ ЗДЕСЬ: for combination in all_combinations(subg_combinations): # ← Декартово произведение! # O(n!) вариантов ... ``` ### 1.2. Почему это проблема? | Метрика | Значение | |---------|----------| | **Сложность** | O(n!) в худшем случае | | **20 инжекторов** | ~0.5 сек | | **50 инжекторов** | TIMEOUT (минуты/часы) | | **Память** | Все варианты хранятся в списке | ### 1.3. Пример взрыва ``` Граф преобразований: int → A (3 способа) int → B (2 способа) A,B → C (4 способа) explode_callgraph_branches генерирует: 3 × 2 × 4 = 24 варианта Для 50 инжекторов с 2-3 путями каждый: 2^50 ≈ 10^15 вариантов (петабайты памяти) ``` --- ## 2. Анализ текущего состояния ### 2.1. Существующие оптимизации | Техника | Реализовано? | Эффективность | |---------|--------------|---------------| | **Эвристическая фильтрация** | ✅ Да | Средняя | | **Ограничение глубины** | ❌ Нет | - | | **Кэширование** | ❌ Нет | - | | **Раннее отсечение** | ❌ Нет | - | | **Ленивые вычисления** | ❌ Нет | - | ### 2.2. Bottlenecks 1. **`all_combinations()`** — генерирует ВСЕ варианты сразу 2. **Нет кэширования** — одинаковые подграфы пересчитываются 3. **Нет pruning** — мёртвые ветви не отсекаются рано 4. **Нет ограничения глубины** — рекурсия уходит слишком глубоко --- ## 3. Варианты решений ### Вариант 1: Кэширование подграфов (Memoization) #### Описание Кэшировать результаты `explode_callgraph_branches()` для одинаковых подграфов. #### Реализация ```python from functools import lru_cache class GraphWalker: _cache: dict[int, list[CallgraphVariant]] = {} @classmethod def explode_callgraph_branches(cls, g: Callgraph, from_types: frozenset[type]) -> list[CallgraphVariant]: # Хэш графа для кэширования cache_key = hash((g, from_types)) if cache_key in cls._cache: return cls._cache[cache_key] # Вычисления... result = [...] cls._cache[cache_key] = result return result ``` #### Сильные стороны | + | Описание | |---|----------| | **Прозрачность** | Минимальные изменения кода | | **Эффективность** | До 90% сокращения для повторяющихся подграфов | | **Безопасность** | Не меняет логику, только кэширует | #### Слабые стороны | - | Описание | |---|----------| | **Память** | Кэш растёт линейно с числом уникальных подграфов | | **Инвалидация** | Нужно очищать при изменении инжекторов | | **Не решает взрыв** | Всё ещё генерирует все варианты | #### Оценка - **Сложность**: ⭐ (низкая) - **Эффективность**: ⭐⭐⭐ (средняя) - **Риск**: 🟢 Низкий --- ### Вариант 2: Ленивые итераторы (Lazy Evaluation) #### Описание Генерировать варианты по одному (generator), а не все сразу. #### Реализация ```python @classmethod def explode_callgraph_branches(cls, g: Callgraph, from_types: frozenset[type]) -> Iterator[CallgraphVariant]: for variant in g.variants: if len(variant.subgraphs) == 0: yield variant continue # Ленивое декартово произведение subg_iterators = [ cls.explode_callgraph_branches(subg, from_types) for subg in variant.subgraphs ] for combination in lazy_cartesian_product(*subg_iterators): yield build_variant(variant, combination) ``` #### Сильные стороны | + | Описание | |---|----------| | **Память** | O(1) вместо O(n!) | | **Ранний выход** | Можно остановить после первого подходящего | | **Композиция** | Легко комбинировать с pruning | #### Слабые стороны | - | Описание | |---|----------| | **Сложность** | Требует изменения API (Iterator вместо list) | | **Повторное использование** | Generator одноразовый | | **Отладка** | Сложнее дебажить ленивые вычисления | #### Оценка - **Сложность**: ⭐⭐⭐ (средняя) - **Эффективность**: ⭐⭐⭐⭐ (высокая) - **Риск**: 🟡 Средний --- ### Вариант 3: Эвристическое отсечение (Pruning) #### Описание Отсекать заведомо плохие ветви рано, до полной генерации. #### Реализация ```python @classmethod def explode_callgraph_branches(cls, g: Callgraph, from_types: frozenset[type], max_depth: int = 10, max_branches: int = 100) -> list[CallgraphVariant]: # Раннее отсечение по глубине if g.depth > max_depth: return [] variants = [] for variant in g.variants: # Отсечение по приоритету if variant.injector.priority < PRIORITY_THRESHOLD: continue # Отсечение по consumed_types if len(variant.consumed_from_types) == 0: continue # Рекурсия с ограничением subg_combinations = [] for subg in variant.subgraphs: combinations = cls.explode_callgraph_branches( subg, from_types, max_depth=max_depth - 1, max_branches=max_branches // len(variant.subgraphs) ) subg_combinations.append(combinations[:max_branches]) # ← Ограничение! # ... генерация комбинаций ``` #### Сильные стороны | + | Описание | |---|----------| | **Эффективность** | До 99% сокращения для больших графов | | **Контроль** | Явные лимиты (depth, branches) | | **Гибкость** | Настраиваемые эвристики | #### Слабые стороны | - | Описание | |---|----------| | **Потеря оптимальности** | Может отсечь лучший путь | | **Настройка** | Нужно подбирать пороги | | **Непредсказуемость** | Разное поведение на разных графах | #### Оценка - **Сложность**: ⭐⭐ (низкая) - **Эффективность**: ⭐⭐⭐⭐⭐ (очень высокая) - **Риск**: 🟡 Средний --- ### Вариант 4: Ограничение числа путей (Top-K Selection) #### Описание Генерировать только K лучших путей вместо всех. #### Реализация ```python @classmethod def explode_callgraph_branches(cls, g: Callgraph, from_types: frozenset[type], top_k: int = 10) -> list[CallgraphVariant]: variants = [] for variant in g.variants: if len(variant.subgraphs) == 0: variants.append(variant) continue # Рекурсия для подграфов subg_results = [] for subg in variant.subgraphs: subg_variants = cls.explode_callgraph_branches(subg, from_types, top_k) subg_results.append(subg_variants[:top_k]) # ← Top-K для каждого подграфа! # Генерация комбинаций for combination in all_combinations(subg_results): new_variant = build_variant(variant, combination) variants.append(new_variant) # Раннее ограничение if len(variants) > top_k * 10: # Буфер variants.sort(key=priority_key, reverse=True) variants = variants[:top_k * 10] # Финальный Top-K variants.sort(key=priority_key, reverse=True) return variants[:top_k] ``` #### Сильные стороны | + | Описание | |---|----------| | **Гарантированная сложность** | O(k × n) вместо O(n!) | | **Простота** | Минимальные изменения | | **Предсказуемость** | Контролируемый лимит | #### Слабые стороны | - | Описание | |---|----------| | **Потеря путей** | Может потерять валидные пути | | **Выбор k** | Нужно подбирать значение | | **Сортировка** | overhead на сортировку | #### Оценка - **Сложность**: ⭐⭐ (низкая) - **Эффективность**: ⭐⭐⭐⭐ (высокая) - **Риск**: 🟢 Низкий --- ### Вариант 5: Комбинированный подход (Hybrid) #### Описание Комбинация кэширования + lazy evaluation + pruning + top-k. #### Реализация ```python @classmethod def explode_callgraph_branches( cls, g: Callgraph, from_types: frozenset[type], max_depth: int = 10, top_k: int = 100, use_cache: bool = True, use_pruning: bool = True ) -> Iterator[CallgraphVariant]: # Кэш if use_cache: cache_key = hash((g, from_types, max_depth, top_k)) if cache_key in cls._cache: yield from cls._cache[cache_key] return # Pruning if use_pruning and g.depth > max_depth: return # Lazy генерация results = [] for variant in cls._generate_variants_lazy(g, from_types, max_depth, top_k): results.append(variant) if len(results) >= top_k: break # Кэширование if use_cache: cls._cache[cache_key] = results yield from results ``` #### Сильные стороны | + | Описание | |---|----------| | **Максимальная эффективность** | Все оптимизации работают вместе | | **Гибкость** | Настраиваемые параметры | | **Масштабируемость** | Работает с большими графами | #### Слабые стороны | - | Описание | |---|----------| | **Сложность** | Значительные изменения кода | | **Тестирование** | Нужно много тестов | | **Отладка** | Сложно понять какая оптимизация сработала | #### Оценка - **Сложность**: ⭐⭐⭐⭐⭐ (высокая) - **Эффективность**: ⭐⭐⭐⭐⭐ (очень высокая) - **Риск**: 🔴 Высокий --- ### Вариант 6: Сжатие графа (Graph Compression) #### Описание Группировать одинаковые комбинации и вычислять их один раз. #### Реализация ```python @dataclass class CompressedVariant: variant: CallgraphVariant count: int # Сколько раз встречается equivalent_paths: list[CallgraphVariant] @classmethod def explode_callgraph_branches(cls, g: Callgraph, from_types: frozenset[type]) -> list[CompressedVariant]: # Группировка по signature signature_map: dict[tuple, list[CallgraphVariant]] = defaultdict(list) for variant in g.variants: signature = compute_signature(variant) # Хэш структуры signature_map[signature].append(variant) # Сжатие compressed = [] for signature, variants in signature_map.items(): compressed.append(CompressedVariant( variant=variants[0], # Представитель count=len(variants), equivalent_paths=variants )) # Вычисления на сжатых данных return compress_and_solve(compressed) ``` #### Сильные стороны | + | Описание | |---|----------| | **Эффективность** | До 95% сокращения для симметричных графов | | **Точность** | Не теряет информацию | | **Инновационность** | Современный подход (NTT 2025) | #### Слабые стороны | - | Описание | |---|----------| | **Сложность** | Значительная переработка | | **Overhead** | Вычисление signature | | **Не универсально** | Эффективно только для симметричных графов | #### Оценка - **Сложность**: ⭐⭐⭐⭐ (высокая) - **Эффективность**: ⭐⭐⭐ (средняя, зависит от графа) - **Риск**: 🔴 Высокий --- ### Вариант 7: A* с эвристикой (Heuristic Search) #### Описание Использовать A* поиск вместо полного перебора. #### Реализация ```python import heapq @classmethod def explode_callgraph_branches(cls, g: Callgraph, from_types: frozenset[type]) -> list[CallgraphVariant]: # Priority queue: (priority, variant) queue = [(0, initial_variant)] visited = set() results = [] while queue and len(results) < MAX_RESULTS: priority, variant = heapq.heappop(queue) variant_id = hash(variant) if variant_id in visited: continue visited.add(variant_id) if is_goal(variant): results.append(variant) continue # Расширение с эвристикой for next_variant in expand(variant): heuristic_priority = estimate_distance_to_goal(next_variant) heapq.heappush(queue, (heuristic_priority, next_variant)) return results ``` #### Сильные стороны | + | Описание | |---|----------| | **Оптимальность** | Находит лучший путь первым | | **Эффективность** | Не генерирует все варианты | | **Гибкость** | Настраиваемая эвристика | #### Слабые стороны | - | Описание | |---|----------| | **Эвристика** | Нужно разработать хорошую | | **Сложность** | Значительная переработка | | **Память** | Priority queue может расти | #### Оценка - **Сложность**: ⭐⭐⭐⭐ (высокая) - **Эффективность**: ⭐⭐⭐⭐ (высокая) - **Риск**: 🟡 Средний --- ## 4. Сравнительная таблица | Вариант | Сложность | Эффективность | Память | Риск | Рекомендация | |---------|-----------|---------------|--------|------|--------------| | **1. Кэширование** | ⭐ | ⭐⭐⭐ | O(n) | 🟢 | ✅ Начать с этого | | **2. Lazy** | ⭐⭐⭐ | ⭐⭐⭐⭐ | O(1) | 🟡 | ✅ Для больших графов | | **3. Pruning** | ⭐⭐ | ⭐⭐⭐⭐⭐ | O(n) | 🟡 | ✅ Обязательно | | **4. Top-K** | ⭐⭐ | ⭐⭐⭐⭐ | O(k) | 🟢 | ✅ Для production | | **5. Hybrid** | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | O(k) | 🔴 | ⭐ Лучший выбор | | **6. Compression** | ⭐⭐⭐⭐ | ⭐⭐⭐ | O(n) | 🔴 | Для симметричных графов | | **7. A*** | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | O(n) | 🟡 | Для оптимальности | --- ## 5. Рекомендации ### 5.1. Краткосрочные решения (быстрая победа) **Вариант 1 + Вариант 4**: Кэширование + Top-K ```python # Минимальные изменения @lru_cache(maxsize=1000) def explode_callgraph_branches(cls, g: Callgraph, from_types: frozenset[type], top_k: int = 100): # ... существующий код с ограничением variants.sort(key=priority_key, reverse=True) return variants[:top_k] ``` **Преимущества:** - ~50 строк кода - Низкий риск - 10-100x ускорение ### 5.2. Среднесрочные решения (баланс) **Вариант 3 + Вариант 4**: Pruning + Top-K ```python repo = ConvRepo( max_depth=10, # Ограничение глубины top_k_paths=50, # Максимум путей prune_low_priority=True # Отсечение по приоритету ) ``` **Преимущества:** - Контролируемая сложность - Предсказуемая производительность - Хорошее качество путей ### 5.3. Долгосрочные решения (полное решение) **Вариант 5 (Hybrid)**: Кэширование + Lazy + Pruning + Top-K ```python repo = HybridConvRepo( cache_size=10000, max_depth=15, top_k=100, use_lazy=True, use_pruning=True, priority_threshold=0.1 ) ``` **Преимущества:** - Масштабируемость до 1000+ инжекторов - Гибкая настройка - Оптимальная производительность ### 5.4. Дорожная карта ``` Фаза 1 (1 неделя): ├── Кэширование (lru_cache) ├── Top-K ограничение └── Тесты производительности Фаза 2 (2 недели): ├── Pruning эвристики ├── Lazy итераторы └── Бенчмарки Фаза 3 (4 недели): ├── Hybrid подход ├── A* с эвристикой ├── Полное тестирование └── Документация ``` --- ## 6. Заключение ### 6.1. Выводы 1. **Нет серебряной пули** — каждый вариант имеет компромиссы 2. **Кэширование + Top-K** — лучший старт (минимум риска) 3. **Pruning** — обязателен для больших графов 4. **Hybrid** — финальная цель для production ### 6.2. Риски | Риск | Вероятность | Влияние | Митигация | |------|-------------|---------|-----------| | Потеря оптимальных путей | Средняя | Высокое | Настройка top_k, pruning thresholds | | Усложнение кода | Высокая | Среднее | Хорошая документация, тесты | | Проблемы с памятью | Низкая | Высокое | Ограничение cache_size | | Непредсказуемость | Средняя | Среднее | Бенчмарки на разных графах | ### 6.3. Следующие шаги 1. **Выбрать подход** для Фазы 1 (кэширование + Top-K) 2. **Создать PR** с минимальными изменениями 3. **Собрать бенчмарки** до/после 4. **Итеративно улучшать** --- *Документ создан для breakshaft v0.1.6* *Дата: 2026-03-28* *Источники: arXiv:2512.12243v2, NTT Review 2025, EmergentMind*