feat: мемоизация (кэширование) explode_callgraph_branches
Реализовано кэширование результатов explode_callgraph_branches: - GraphWalker._explode_cache: dict для хранения результатов - Ключ кэша: (hash(g), hash(from_types)) - Очистка кэша при добавлении инжекторов (GraphWalker.clear_cache()) - Инвалидация через add_injector() Результаты: - Повторный explode: 0.015ms -> 0.002ms (7.5x быстрее) - Все 114 тестов проходят Файлы: - graph_walker.py: добавлен кэш и clear_cache() - convertor.py: очистка кэша при add_injector() - test_memoization.py: 5 тестов на кэширование Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
595
COMBINATORIAL_EXPLOSION_RESEARCH.md
Normal file
595
COMBINATORIAL_EXPLOSION_RESEARCH.md
Normal file
@@ -0,0 +1,595 @@
|
|||||||
|
# Исследование: Методы борьбы с комбинаторным взрывом в 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*
|
||||||
@@ -107,11 +107,15 @@ class ConvRepo:
|
|||||||
cps = ConversionPoint.from_fn(func, rettype=rettype, type_remap=type_remap)
|
cps = ConversionPoint.from_fn(func, rettype=rettype, type_remap=type_remap)
|
||||||
# Применяем приоритет ко всем ConversionPoint (может быть несколько для Union/tuple)
|
# Применяем приоритет ко всем ConversionPoint (может быть несколько для Union/tuple)
|
||||||
prioritized_cps = [cp.copy_with(priority=priority) for cp in cps]
|
prioritized_cps = [cp.copy_with(priority=priority) for cp in cps]
|
||||||
|
|
||||||
# Удаляем существующие инжекторы для этой функции (если есть)
|
# Удаляем существующие инжекторы для этой функции (если есть)
|
||||||
self._convertor_set = {cp for cp in self._convertor_set if cp.fn is not func}
|
self._convertor_set = {cp for cp in self._convertor_set if cp.fn is not func}
|
||||||
|
|
||||||
self.add_conversion_points(prioritized_cps)
|
self.add_conversion_points(prioritized_cps)
|
||||||
|
|
||||||
|
# Очищаем кэш graph_walker при изменении инжекторов
|
||||||
|
from .graph_walker import GraphWalker
|
||||||
|
GraphWalker.clear_cache()
|
||||||
|
|
||||||
def _callseq_from_callgraph(self, cg: Callgraph) -> list[ConversionPoint]:
|
def _callseq_from_callgraph(self, cg: Callgraph) -> list[ConversionPoint]:
|
||||||
if len(cg.variants) == 0:
|
if len(cg.variants) == 0:
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import collections.abc
|
|||||||
import typing
|
import typing
|
||||||
from types import NoneType
|
from types import NoneType
|
||||||
from typing import Callable, Optional
|
from typing import Callable, Optional
|
||||||
|
from functools import lru_cache
|
||||||
|
|
||||||
from .models import ConversionPoint, Callgraph, CallgraphVariant, TransformationPoint, CompositionDirection
|
from .models import ConversionPoint, Callgraph, CallgraphVariant, TransformationPoint, CompositionDirection
|
||||||
from .util import extract_func_argtypes, all_combinations, extract_func_argtypes_seq, extract_return_type, universal_qualname
|
from .util import extract_func_argtypes, all_combinations, extract_func_argtypes_seq, extract_return_type, universal_qualname
|
||||||
@@ -10,6 +11,15 @@ from typing import Iterable
|
|||||||
|
|
||||||
|
|
||||||
class GraphWalker:
|
class GraphWalker:
|
||||||
|
# Кэш для explode_callgraph_branches
|
||||||
|
# Ключ: (hash(g), hash(from_types))
|
||||||
|
# Значение: list[CallgraphVariant]
|
||||||
|
_explode_cache: dict[tuple[int, int], list[CallgraphVariant]] = {}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def clear_cache(cls):
|
||||||
|
"""Очистить кэш explode_callgraph_branches."""
|
||||||
|
cls._explode_cache.clear()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def generate_callgraph(cls,
|
def generate_callgraph(cls,
|
||||||
@@ -102,6 +112,16 @@ class GraphWalker:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def explode_callgraph_branches(cls, g: Callgraph, from_types: frozenset[type]) -> list[CallgraphVariant]:
|
def explode_callgraph_branches(cls, g: Callgraph, from_types: frozenset[type]) -> list[CallgraphVariant]:
|
||||||
|
# Кэширование: создаём хэш графа
|
||||||
|
# Хэш графа = хэш всех вариантов
|
||||||
|
g_hash = hash(frozenset(g.variants)) if g.variants else 0
|
||||||
|
cache_key = (g_hash, hash(from_types))
|
||||||
|
|
||||||
|
# Проверяем кэш
|
||||||
|
if cache_key in cls._explode_cache:
|
||||||
|
return cls._explode_cache[cache_key]
|
||||||
|
|
||||||
|
# Вычисляем
|
||||||
variants = []
|
variants = []
|
||||||
for variant in g.variants:
|
for variant in g.variants:
|
||||||
if len(variant.subgraphs) == 0:
|
if len(variant.subgraphs) == 0:
|
||||||
@@ -127,7 +147,10 @@ class GraphWalker:
|
|||||||
variants.append(
|
variants.append(
|
||||||
CallgraphVariant(variant.injector, cum_cmb,
|
CallgraphVariant(variant.injector, cum_cmb,
|
||||||
variant.consumed_from_types | cons))
|
variant.consumed_from_types | cons))
|
||||||
|
|
||||||
|
# Сохраняем в кэш
|
||||||
|
cls._explode_cache[cache_key] = variants
|
||||||
|
|
||||||
return variants
|
return variants
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
310
tests/test_benchmarks.py
Normal file
310
tests/test_benchmarks.py
Normal file
@@ -0,0 +1,310 @@
|
|||||||
|
"""
|
||||||
|
Бенчмарки производительности для breakshaft.
|
||||||
|
|
||||||
|
Измеряет время построения графа преобразований для разного числа инжекторов.
|
||||||
|
|
||||||
|
Использование:
|
||||||
|
uv run pytest tests/test_benchmarks.py -v -s
|
||||||
|
"""
|
||||||
|
|
||||||
|
import time
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from breakshaft import ConvRepo
|
||||||
|
from breakshaft.graph_walker import GraphWalker
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TypeN:
|
||||||
|
"""Базовый тип для бенчмарков."""
|
||||||
|
n: int
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Бенчмарки: Базовая производительность
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
class TestBenchmarkBasic:
|
||||||
|
"""Базовые бенчмарки производительности."""
|
||||||
|
|
||||||
|
@pytest.mark.benchmark
|
||||||
|
def test_benchmark_chain_10(self):
|
||||||
|
"""Бенчмарк: цепочка 10 инжекторов."""
|
||||||
|
repo = ConvRepo()
|
||||||
|
|
||||||
|
# Создаём цепочку Type0 -> Type1 -> ... -> Type10
|
||||||
|
for i in range(10):
|
||||||
|
def make_injector(idx):
|
||||||
|
def injector(value: TypeN) -> TypeN:
|
||||||
|
return TypeN(value.n + 1)
|
||||||
|
injector.__name__ = f'type_{idx}_to_type_{idx+1}'
|
||||||
|
injector.__qualname__ = injector.__name__
|
||||||
|
return injector
|
||||||
|
|
||||||
|
repo.add_injector(make_injector(i), priority=i)
|
||||||
|
|
||||||
|
start = time.perf_counter()
|
||||||
|
|
||||||
|
def consumer(value: TypeN) -> int:
|
||||||
|
return value.n
|
||||||
|
|
||||||
|
fn = repo.get_conversion((TypeN,), consumer, force_commutative=False)
|
||||||
|
|
||||||
|
elapsed = time.perf_counter() - start
|
||||||
|
print(f"\nChain 10: {elapsed*1000:.2f}ms")
|
||||||
|
assert elapsed < 1.0
|
||||||
|
|
||||||
|
@pytest.mark.benchmark
|
||||||
|
def test_benchmark_chain_20(self):
|
||||||
|
"""Бенчмарк: цепочка 20 инжекторов."""
|
||||||
|
repo = ConvRepo()
|
||||||
|
|
||||||
|
for i in range(20):
|
||||||
|
def make_injector(idx):
|
||||||
|
def injector(value: TypeN) -> TypeN:
|
||||||
|
return TypeN(value.n + 1)
|
||||||
|
injector.__name__ = f'type_{idx}_to_type_{idx+1}'
|
||||||
|
return injector
|
||||||
|
|
||||||
|
repo.add_injector(make_injector(i))
|
||||||
|
|
||||||
|
start = time.perf_counter()
|
||||||
|
|
||||||
|
def consumer(value: TypeN) -> int:
|
||||||
|
return value.n
|
||||||
|
|
||||||
|
fn = repo.get_conversion((TypeN,), consumer, force_commutative=False)
|
||||||
|
|
||||||
|
elapsed = time.perf_counter() - start
|
||||||
|
print(f"\nChain 20: {elapsed*1000:.2f}ms")
|
||||||
|
assert elapsed < 5.0
|
||||||
|
|
||||||
|
@pytest.mark.benchmark
|
||||||
|
def test_benchmark_chain_50(self):
|
||||||
|
"""Бенчмарк: цепочка 50 инжекторов."""
|
||||||
|
repo = ConvRepo()
|
||||||
|
|
||||||
|
for i in range(50):
|
||||||
|
def make_injector(idx):
|
||||||
|
def injector(value: TypeN) -> TypeN:
|
||||||
|
return TypeN(value.n + 1)
|
||||||
|
injector.__name__ = f'type_{idx}_to_type_{idx+1}'
|
||||||
|
return injector
|
||||||
|
|
||||||
|
repo.add_injector(make_injector(i))
|
||||||
|
|
||||||
|
start = time.perf_counter()
|
||||||
|
|
||||||
|
def consumer(value: TypeN) -> int:
|
||||||
|
return value.n
|
||||||
|
|
||||||
|
fn = repo.get_conversion((TypeN,), consumer, force_commutative=False)
|
||||||
|
|
||||||
|
elapsed = time.perf_counter() - start
|
||||||
|
print(f"\nChain 50: {elapsed*1000:.2f}ms")
|
||||||
|
|
||||||
|
@pytest.mark.benchmark
|
||||||
|
def test_benchmark_fan_10(self):
|
||||||
|
"""Бенчмарк: веер 10 инжекторов (int -> TypeN)."""
|
||||||
|
repo = ConvRepo()
|
||||||
|
|
||||||
|
for i in range(10):
|
||||||
|
def make_injector(idx):
|
||||||
|
def injector(value: int) -> TypeN:
|
||||||
|
return TypeN(idx)
|
||||||
|
injector.__name__ = f'int_to_type_{idx}'
|
||||||
|
return injector
|
||||||
|
|
||||||
|
repo.add_injector(make_injector(i))
|
||||||
|
|
||||||
|
start = time.perf_counter()
|
||||||
|
|
||||||
|
def consumer(value: TypeN) -> int:
|
||||||
|
return value.n
|
||||||
|
|
||||||
|
fn = repo.get_conversion((int,), consumer, force_commutative=False)
|
||||||
|
|
||||||
|
elapsed = time.perf_counter() - start
|
||||||
|
print(f"\nFan 10: {elapsed*1000:.2f}ms")
|
||||||
|
|
||||||
|
@pytest.mark.benchmark
|
||||||
|
def test_benchmark_fan_20(self):
|
||||||
|
"""Бенчмарк: веер 20 инжекторов."""
|
||||||
|
repo = ConvRepo()
|
||||||
|
|
||||||
|
for i in range(20):
|
||||||
|
def make_injector(idx):
|
||||||
|
def injector(value: int) -> TypeN:
|
||||||
|
return TypeN(idx)
|
||||||
|
injector.__name__ = f'int_to_type_{idx}'
|
||||||
|
return injector
|
||||||
|
|
||||||
|
repo.add_injector(make_injector(i))
|
||||||
|
|
||||||
|
start = time.perf_counter()
|
||||||
|
|
||||||
|
def consumer(value: TypeN) -> int:
|
||||||
|
return value.n
|
||||||
|
|
||||||
|
fn = repo.get_conversion((int,), consumer, force_commutative=False)
|
||||||
|
|
||||||
|
elapsed = time.perf_counter() - start
|
||||||
|
print(f"\nFan 20: {elapsed*1000:.2f}ms")
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Бенчмарки: explode_callgraph_branches
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
class TestBenchmarkExplode:
|
||||||
|
"""Бенчмарки для explode_callgraph_branches."""
|
||||||
|
|
||||||
|
@pytest.mark.benchmark
|
||||||
|
def test_benchmark_explode_simple(self):
|
||||||
|
"""Бенчмарк: explode на простом графе."""
|
||||||
|
repo = ConvRepo()
|
||||||
|
|
||||||
|
@repo.mark_injector()
|
||||||
|
def int_to_a(i: int) -> TypeN:
|
||||||
|
return TypeN(i)
|
||||||
|
|
||||||
|
@repo.mark_injector()
|
||||||
|
def a_to_b(a: TypeN) -> TypeN:
|
||||||
|
return TypeN(a.n + 1)
|
||||||
|
|
||||||
|
walker = GraphWalker()
|
||||||
|
|
||||||
|
def consumer(dep: TypeN) -> int:
|
||||||
|
return dep.n
|
||||||
|
|
||||||
|
start = time.perf_counter()
|
||||||
|
|
||||||
|
cg = walker.generate_callgraph(repo.convertor_set, frozenset({int}), consumer)
|
||||||
|
exploded = walker.explode_callgraph_branches(cg, frozenset({int}))
|
||||||
|
|
||||||
|
elapsed = time.perf_counter() - start
|
||||||
|
print(f"\nexplode (simple): {elapsed*1000:.2f}ms, variants: {len(exploded)}")
|
||||||
|
assert len(exploded) > 0
|
||||||
|
|
||||||
|
@pytest.mark.benchmark
|
||||||
|
def test_benchmark_explode_fan(self):
|
||||||
|
"""Бенчмарк: explode на веерном графе."""
|
||||||
|
repo = ConvRepo()
|
||||||
|
|
||||||
|
for i in range(10):
|
||||||
|
def make_injector(idx):
|
||||||
|
def injector(value: int) -> TypeN:
|
||||||
|
return TypeN(idx)
|
||||||
|
injector.__name__ = f'int_to_type_{idx}'
|
||||||
|
return injector
|
||||||
|
|
||||||
|
repo.add_injector(make_injector(i))
|
||||||
|
|
||||||
|
walker = GraphWalker()
|
||||||
|
|
||||||
|
def consumer(dep: TypeN) -> int:
|
||||||
|
return dep.n
|
||||||
|
|
||||||
|
start = time.perf_counter()
|
||||||
|
|
||||||
|
cg = walker.generate_callgraph(repo.convertor_set, frozenset({int}), consumer)
|
||||||
|
if cg:
|
||||||
|
exploded = walker.explode_callgraph_branches(cg, frozenset({int}))
|
||||||
|
elapsed = time.perf_counter() - start
|
||||||
|
print(f"\nexplode (fan 10): {elapsed*1000:.2f}ms, variants: {len(exploded)}")
|
||||||
|
else:
|
||||||
|
elapsed = time.perf_counter() - start
|
||||||
|
print(f"\nexplode (fan 10): no graph in {elapsed*1000:.2f}ms")
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Бенчмарки: Сравнение сценариев
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
class TestBenchmarkScenarios:
|
||||||
|
"""Бенчмарки различных сценариев использования."""
|
||||||
|
|
||||||
|
@pytest.mark.benchmark
|
||||||
|
def test_benchmark_repeated_calls(self):
|
||||||
|
"""Бенчмарк: Повторные вызовы get_conversion."""
|
||||||
|
repo = ConvRepo()
|
||||||
|
|
||||||
|
@repo.mark_injector()
|
||||||
|
def int_to_a(i: int) -> TypeN:
|
||||||
|
return TypeN(i)
|
||||||
|
|
||||||
|
@repo.mark_injector()
|
||||||
|
def a_to_b(a: TypeN) -> TypeN:
|
||||||
|
return TypeN(a.n + 1)
|
||||||
|
|
||||||
|
def consumer(dep: TypeN) -> int:
|
||||||
|
return dep.n
|
||||||
|
|
||||||
|
# Первый вызов
|
||||||
|
start = time.perf_counter()
|
||||||
|
fn1 = repo.get_conversion((int,), consumer, force_commutative=False)
|
||||||
|
elapsed1 = time.perf_counter() - start
|
||||||
|
|
||||||
|
# Второй вызов
|
||||||
|
start = time.perf_counter()
|
||||||
|
fn2 = repo.get_conversion((int,), consumer, force_commutative=False)
|
||||||
|
elapsed2 = time.perf_counter() - start
|
||||||
|
|
||||||
|
print(f"\nRepeated calls: {elapsed1*1000:.2f}ms -> {elapsed2*1000:.2f}ms")
|
||||||
|
if elapsed2 > 0:
|
||||||
|
print(f"Speedup: {elapsed1/elapsed2:.2f}x")
|
||||||
|
|
||||||
|
@pytest.mark.benchmark
|
||||||
|
def test_benchmark_pipeline(self):
|
||||||
|
"""Бенчмарк: create_pipeline."""
|
||||||
|
repo = ConvRepo()
|
||||||
|
|
||||||
|
@repo.mark_injector()
|
||||||
|
def int_to_a(i: int) -> TypeN:
|
||||||
|
return TypeN(i)
|
||||||
|
|
||||||
|
@repo.mark_injector()
|
||||||
|
def a_to_b(a: TypeN) -> TypeN:
|
||||||
|
return TypeN(a.n + 1)
|
||||||
|
|
||||||
|
def consumer1(dep: TypeN) -> TypeN:
|
||||||
|
return dep
|
||||||
|
|
||||||
|
def consumer2(dep: TypeN) -> int:
|
||||||
|
return dep.n
|
||||||
|
|
||||||
|
start = time.perf_counter()
|
||||||
|
|
||||||
|
pipeline = repo.create_pipeline(
|
||||||
|
(int,),
|
||||||
|
[consumer1, consumer2],
|
||||||
|
force_commutative=False
|
||||||
|
)
|
||||||
|
|
||||||
|
elapsed = time.perf_counter() - start
|
||||||
|
print(f"\nPipeline: {elapsed*1000:.2f}ms")
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Утилиты
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
def run_benchmark_suite():
|
||||||
|
"""Запустить полный набор бенчмарков."""
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
result = subprocess.run([
|
||||||
|
sys.executable, '-m', 'pytest',
|
||||||
|
'tests/test_benchmarks.py',
|
||||||
|
'-v', '--tb=short',
|
||||||
|
'-s'
|
||||||
|
])
|
||||||
|
|
||||||
|
return result.returncode
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
exit(run_benchmark_suite())
|
||||||
174
tests/test_memoization.py
Normal file
174
tests/test_memoization.py
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
"""
|
||||||
|
Тесты мемоизации (кэширования) для breakshaft.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from breakshaft import ConvRepo
|
||||||
|
from breakshaft.graph_walker import GraphWalker
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TypeN:
|
||||||
|
n: int
|
||||||
|
|
||||||
|
|
||||||
|
class TestMemoization:
|
||||||
|
"""Тесты кэширования explode_callgraph_branches."""
|
||||||
|
|
||||||
|
def test_cache_hit(self):
|
||||||
|
"""Кэш должен возвращать тот же результат для одинаковых графов."""
|
||||||
|
repo = ConvRepo()
|
||||||
|
|
||||||
|
@repo.mark_injector()
|
||||||
|
def int_to_a(i: int) -> TypeN:
|
||||||
|
return TypeN(i)
|
||||||
|
|
||||||
|
@repo.mark_injector()
|
||||||
|
def a_to_b(a: TypeN) -> TypeN:
|
||||||
|
return TypeN(a.n + 1)
|
||||||
|
|
||||||
|
walker = GraphWalker()
|
||||||
|
|
||||||
|
def consumer(dep: TypeN) -> int:
|
||||||
|
return dep.n
|
||||||
|
|
||||||
|
# Первый вызов (кэш пуст)
|
||||||
|
cg = walker.generate_callgraph(repo.convertor_set, frozenset({int}), consumer)
|
||||||
|
result1 = walker.explode_callgraph_branches(cg, frozenset({int}))
|
||||||
|
|
||||||
|
# Второй вызов (должен быть из кэша)
|
||||||
|
result2 = walker.explode_callgraph_branches(cg, frozenset({int}))
|
||||||
|
|
||||||
|
# Результаты должны быть одинаковыми
|
||||||
|
assert len(result1) == len(result2)
|
||||||
|
|
||||||
|
# Кэш должен содержать запись
|
||||||
|
assert len(walker._explode_cache) > 0
|
||||||
|
|
||||||
|
def test_cache_invalidated_on_add_injector(self):
|
||||||
|
"""Кэш должен очищаться при добавлении инжектора."""
|
||||||
|
repo = ConvRepo()
|
||||||
|
|
||||||
|
@repo.mark_injector()
|
||||||
|
def int_to_a(i: int) -> TypeN:
|
||||||
|
return TypeN(i)
|
||||||
|
|
||||||
|
walker = GraphWalker()
|
||||||
|
|
||||||
|
def consumer(dep: TypeN) -> int:
|
||||||
|
return dep.n
|
||||||
|
|
||||||
|
# Первый вызов
|
||||||
|
cg = walker.generate_callgraph(repo.convertor_set, frozenset({int}), consumer)
|
||||||
|
walker.explode_callgraph_branches(cg, frozenset({int}))
|
||||||
|
|
||||||
|
cache_size_after_first = len(walker._explode_cache)
|
||||||
|
|
||||||
|
# Добавляем инжектор
|
||||||
|
@repo.mark_injector()
|
||||||
|
def a_to_b(a: TypeN) -> TypeN:
|
||||||
|
return TypeN(a.n + 1)
|
||||||
|
|
||||||
|
# Кэш должен очиститься
|
||||||
|
assert len(walker._explode_cache) == 0
|
||||||
|
|
||||||
|
def test_cache_different_from_types(self):
|
||||||
|
"""Кэш должен различать разные from_types."""
|
||||||
|
repo = ConvRepo()
|
||||||
|
|
||||||
|
@repo.mark_injector()
|
||||||
|
def int_to_a(i: int) -> TypeN:
|
||||||
|
return TypeN(i)
|
||||||
|
|
||||||
|
@repo.mark_injector()
|
||||||
|
def float_to_a(f: float) -> TypeN:
|
||||||
|
return TypeN(int(f))
|
||||||
|
|
||||||
|
walker = GraphWalker()
|
||||||
|
|
||||||
|
def consumer(dep: TypeN) -> int:
|
||||||
|
return dep.n
|
||||||
|
|
||||||
|
# Очищаем кэш перед тестом
|
||||||
|
walker.clear_cache()
|
||||||
|
|
||||||
|
# Вызов с int
|
||||||
|
cg1 = walker.generate_callgraph(repo.convertor_set, frozenset({int}), consumer)
|
||||||
|
result1 = walker.explode_callgraph_branches(cg1, frozenset({int}))
|
||||||
|
|
||||||
|
cache_after_int = len(walker._explode_cache)
|
||||||
|
|
||||||
|
# Вызов с float
|
||||||
|
cg2 = walker.generate_callgraph(repo.convertor_set, frozenset({float}), consumer)
|
||||||
|
result2 = walker.explode_callgraph_branches(cg2, frozenset({float}))
|
||||||
|
|
||||||
|
cache_after_float = len(walker._explode_cache)
|
||||||
|
|
||||||
|
# Кэш должен вырасти (как минимум 2 разные записи)
|
||||||
|
assert cache_after_float > cache_after_int
|
||||||
|
|
||||||
|
def test_cache_clear_method(self):
|
||||||
|
"""Метод clear_cache() должен очищать кэш."""
|
||||||
|
repo = ConvRepo()
|
||||||
|
|
||||||
|
@repo.mark_injector()
|
||||||
|
def int_to_a(i: int) -> TypeN:
|
||||||
|
return TypeN(i)
|
||||||
|
|
||||||
|
walker = GraphWalker()
|
||||||
|
|
||||||
|
def consumer(dep: TypeN) -> int:
|
||||||
|
return dep.n
|
||||||
|
|
||||||
|
# Заполняем кэш
|
||||||
|
cg = walker.generate_callgraph(repo.convertor_set, frozenset({int}), consumer)
|
||||||
|
walker.explode_callgraph_branches(cg, frozenset({int}))
|
||||||
|
|
||||||
|
assert len(walker._explode_cache) > 0
|
||||||
|
|
||||||
|
# Очищаем
|
||||||
|
walker.clear_cache()
|
||||||
|
|
||||||
|
assert len(walker._explode_cache) == 0
|
||||||
|
|
||||||
|
|
||||||
|
class TestMemoizationPerformance:
|
||||||
|
"""Бенчмарки кэширования."""
|
||||||
|
|
||||||
|
def test_repeated_explode_faster(self):
|
||||||
|
"""Повторный explode должен быть быстрее благодаря кэшу."""
|
||||||
|
import time
|
||||||
|
|
||||||
|
repo = ConvRepo()
|
||||||
|
|
||||||
|
@repo.mark_injector()
|
||||||
|
def int_to_a(i: int) -> TypeN:
|
||||||
|
return TypeN(i)
|
||||||
|
|
||||||
|
@repo.mark_injector()
|
||||||
|
def a_to_b(a: TypeN) -> TypeN:
|
||||||
|
return TypeN(a.n + 1)
|
||||||
|
|
||||||
|
walker = GraphWalker()
|
||||||
|
|
||||||
|
def consumer(dep: TypeN) -> int:
|
||||||
|
return dep.n
|
||||||
|
|
||||||
|
cg = walker.generate_callgraph(repo.convertor_set, frozenset({int}), consumer)
|
||||||
|
|
||||||
|
# Первый вызов
|
||||||
|
start1 = time.perf_counter()
|
||||||
|
walker.explode_callgraph_branches(cg, frozenset({int}))
|
||||||
|
elapsed1 = time.perf_counter() - start1
|
||||||
|
|
||||||
|
# Второй вызов (из кэша)
|
||||||
|
start2 = time.perf_counter()
|
||||||
|
walker.explode_callgraph_branches(cg, frozenset({int}))
|
||||||
|
elapsed2 = time.perf_counter() - start2
|
||||||
|
|
||||||
|
# Второй должен быть значительно быстрее
|
||||||
|
print(f"\nexplode: {elapsed1*1000:.3f}ms -> {elapsed2*1000:.3f}ms (cache)")
|
||||||
|
assert elapsed2 < elapsed1 * 0.5 # Хотя бы 2x быстрее
|
||||||
Reference in New Issue
Block a user