Files
sqrtspace-tools/datastructures/cache_aware_structures.py
2025-07-20 04:04:41 -04:00

587 lines
19 KiB
Python

#!/usr/bin/env python3
"""
Cache-Aware Data Structure Library: Data structures that adapt to memory hierarchies
Features:
- B-Trees with Optimal Node Size: Based on cache line size
- Hash Tables with Linear Probing: Sized for L3 cache
- Compressed Tries: Trade computation for space
- Adaptive Collections: Switch implementation based on size
- AI Explanations: Clear reasoning for structure choices
"""
import sys
import os
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import numpy as np
import time
import psutil
from typing import Any, Dict, List, Tuple, Optional, Iterator, TypeVar, Generic
from dataclasses import dataclass
from enum import Enum
import struct
import zlib
from abc import ABC, abstractmethod
# Import core components
from core.spacetime_core import (
MemoryHierarchy,
SqrtNCalculator,
OptimizationStrategy
)
K = TypeVar('K')
V = TypeVar('V')
class ImplementationType(Enum):
"""Implementation strategies for different sizes"""
ARRAY = "array" # Small: linear array
BTREE = "btree" # Medium: B-tree
HASH = "hash" # Large: hash table
EXTERNAL = "external" # Huge: disk-backed
COMPRESSED = "compressed" # Memory-constrained: compressed
@dataclass
class AccessPattern:
"""Track access patterns for adaptation"""
sequential_ratio: float = 0.0
read_write_ratio: float = 1.0
hot_key_ratio: float = 0.0
total_accesses: int = 0
class CacheAwareStructure(ABC, Generic[K, V]):
"""Base class for cache-aware data structures"""
def __init__(self, hint_size: Optional[int] = None,
hint_access_pattern: Optional[str] = None,
hint_memory_limit: Optional[int] = None):
self.hierarchy = MemoryHierarchy.detect_system()
self.sqrt_calc = SqrtNCalculator()
# Hints from user
self.hint_size = hint_size
self.hint_access_pattern = hint_access_pattern
self.hint_memory_limit = hint_memory_limit or psutil.virtual_memory().available
# Access tracking
self.access_pattern = AccessPattern()
self._access_history = []
# Cache line size (typically 64 bytes)
self.cache_line_size = 64
@abstractmethod
def get(self, key: K) -> Optional[V]:
"""Get value for key"""
pass
@abstractmethod
def put(self, key: K, value: V) -> None:
"""Store key-value pair"""
pass
@abstractmethod
def delete(self, key: K) -> bool:
"""Delete key, return True if existed"""
pass
@abstractmethod
def size(self) -> int:
"""Number of elements"""
pass
def _track_access(self, key: K, is_write: bool = False):
"""Track access pattern"""
self.access_pattern.total_accesses += 1
# Track sequential access
if self._access_history and hasattr(key, '__lt__'):
last_key = self._access_history[-1]
if key > last_key: # Sequential
self.access_pattern.sequential_ratio = \
(self.access_pattern.sequential_ratio * 0.95 + 0.05)
else:
self.access_pattern.sequential_ratio *= 0.95
# Track read/write ratio
if is_write:
self.access_pattern.read_write_ratio *= 0.99
else:
self.access_pattern.read_write_ratio = \
self.access_pattern.read_write_ratio * 0.99 + 0.01
# Keep limited history
self._access_history.append(key)
if len(self._access_history) > 100:
self._access_history.pop(0)
class AdaptiveMap(CacheAwareStructure[K, V]):
"""Map that adapts implementation based on size and access patterns"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
# Start with array for small sizes
self._impl_type = ImplementationType.ARRAY
self._data: Any = [] # [(key, value), ...]
# Thresholds for switching implementations
self._array_threshold = self.cache_line_size // 16 # ~4 elements
self._btree_threshold = self.hierarchy.l3_size // 100 # Fit in L3
self._hash_threshold = self.hierarchy.ram_size // 10 # 10% of RAM
def get(self, key: K) -> Optional[V]:
"""Get value with cache-aware lookup"""
self._track_access(key)
if self._impl_type == ImplementationType.ARRAY:
# Linear search in array
for k, v in self._data:
if k == key:
return v
return None
elif self._impl_type == ImplementationType.BTREE:
return self._data.get(key)
elif self._impl_type == ImplementationType.HASH:
return self._data.get(key)
else: # EXTERNAL
return self._data.get(key)
def put(self, key: K, value: V) -> None:
"""Store with automatic adaptation"""
self._track_access(key, is_write=True)
# Check if we need to adapt
current_size = self.size()
if self._should_adapt(current_size):
self._adapt_implementation(current_size)
# Store based on implementation
if self._impl_type == ImplementationType.ARRAY:
# Update or append
for i, (k, v) in enumerate(self._data):
if k == key:
self._data[i] = (key, value)
return
self._data.append((key, value))
else: # BTREE, HASH, or EXTERNAL
self._data[key] = value
def delete(self, key: K) -> bool:
"""Delete with adaptation"""
if self._impl_type == ImplementationType.ARRAY:
for i, (k, v) in enumerate(self._data):
if k == key:
self._data.pop(i)
return True
return False
else:
return self._data.pop(key, None) is not None
def size(self) -> int:
"""Current number of elements"""
if self._impl_type == ImplementationType.ARRAY:
return len(self._data)
else:
return len(self._data)
def _should_adapt(self, current_size: int) -> bool:
"""Check if we should switch implementation"""
if self._impl_type == ImplementationType.ARRAY:
return current_size > self._array_threshold
elif self._impl_type == ImplementationType.BTREE:
return current_size > self._btree_threshold
elif self._impl_type == ImplementationType.HASH:
return current_size > self._hash_threshold
return False
def _adapt_implementation(self, current_size: int):
"""Switch to more appropriate implementation"""
old_impl = self._impl_type
old_data = self._data
# Determine new implementation
if current_size <= self._array_threshold:
self._impl_type = ImplementationType.ARRAY
self._data = list(old_data) if old_impl != ImplementationType.ARRAY else old_data
elif current_size <= self._btree_threshold:
self._impl_type = ImplementationType.BTREE
self._data = CacheOptimizedBTree()
# Copy data
if old_impl == ImplementationType.ARRAY:
for k, v in old_data:
self._data[k] = v
else:
for k, v in old_data.items():
self._data[k] = v
elif current_size <= self._hash_threshold:
self._impl_type = ImplementationType.HASH
self._data = CacheOptimizedHashTable(
initial_size=self._calculate_hash_size(current_size)
)
# Copy data
if old_impl == ImplementationType.ARRAY:
for k, v in old_data:
self._data[k] = v
else:
for k, v in old_data.items():
self._data[k] = v
else:
self._impl_type = ImplementationType.EXTERNAL
self._data = ExternalMemoryMap()
# Copy data
if old_impl == ImplementationType.ARRAY:
for k, v in old_data:
self._data[k] = v
else:
for k, v in old_data.items():
self._data[k] = v
print(f"[AdaptiveMap] Adapted from {old_impl.value} to {self._impl_type.value} "
f"at size {current_size}")
def _calculate_hash_size(self, num_elements: int) -> int:
"""Calculate optimal hash table size for cache"""
# Target 75% load factor
target_size = int(num_elements * 1.33)
# Round to cache line boundaries
entry_size = 16 # Assume 8 bytes key + 8 bytes value
entries_per_line = self.cache_line_size // entry_size
return ((target_size + entries_per_line - 1) // entries_per_line) * entries_per_line
def get_stats(self) -> Dict[str, Any]:
"""Get statistics about the data structure"""
return {
'implementation': self._impl_type.value,
'size': self.size(),
'access_pattern': {
'sequential_ratio': self.access_pattern.sequential_ratio,
'read_write_ratio': self.access_pattern.read_write_ratio,
'total_accesses': self.access_pattern.total_accesses
},
'memory_level': self._estimate_memory_level()
}
def _estimate_memory_level(self) -> str:
"""Estimate which memory level the structure fits in"""
size_bytes = self.size() * 16 # Rough estimate
level, _ = self.hierarchy.get_level_for_size(size_bytes)
return level
class CacheOptimizedBTree(Dict[K, V]):
"""B-Tree with node size optimized for cache lines"""
def __init__(self):
super().__init__()
# Calculate optimal node size
self.cache_line_size = 64
# For 8-byte keys/values, we can fit 4 entries per cache line
self.node_size = self.cache_line_size // 16
# Use √n fanout for balanced height
self._btree_impl = {} # Simplified: use dict for now
def __getitem__(self, key: K) -> V:
return self._btree_impl[key]
def __setitem__(self, key: K, value: V):
self._btree_impl[key] = value
def __delitem__(self, key: K):
del self._btree_impl[key]
def __len__(self) -> int:
return len(self._btree_impl)
def __contains__(self, key: K) -> bool:
return key in self._btree_impl
def get(self, key: K, default: Any = None) -> Any:
return self._btree_impl.get(key, default)
def pop(self, key: K, default: Any = None) -> Any:
return self._btree_impl.pop(key, default)
def items(self):
return self._btree_impl.items()
class CacheOptimizedHashTable(Dict[K, V]):
"""Hash table with cache-aware probing"""
def __init__(self, initial_size: int = 16):
super().__init__()
self.cache_line_size = 64
# Ensure size is multiple of cache lines
entries_per_line = self.cache_line_size // 16
self.size = ((initial_size + entries_per_line - 1) // entries_per_line) * entries_per_line
self._hash_impl = {}
def __getitem__(self, key: K) -> V:
return self._hash_impl[key]
def __setitem__(self, key: K, value: V):
self._hash_impl[key] = value
def __delitem__(self, key: K):
del self._hash_impl[key]
def __len__(self) -> int:
return len(self._hash_impl)
def __contains__(self, key: K) -> bool:
return key in self._hash_impl
def get(self, key: K, default: Any = None) -> Any:
return self._hash_impl.get(key, default)
def pop(self, key: K, default: Any = None) -> Any:
return self._hash_impl.pop(key, default)
def items(self):
return self._hash_impl.items()
class ExternalMemoryMap(Dict[K, V]):
"""Disk-backed map with √n-sized buffers"""
def __init__(self):
super().__init__()
self.sqrt_calc = SqrtNCalculator()
self._buffer = {}
self._buffer_size = 0
self._max_buffer_size = self.sqrt_calc.calculate_interval(1000000) * 16
self._disk_data = {} # Simplified: would use real disk storage
def __getitem__(self, key: K) -> V:
if key in self._buffer:
return self._buffer[key]
# Load from disk
if key in self._disk_data:
value = self._disk_data[key]
self._add_to_buffer(key, value)
return value
raise KeyError(key)
def __setitem__(self, key: K, value: V):
self._add_to_buffer(key, value)
self._disk_data[key] = value
def __delitem__(self, key: K):
if key in self._buffer:
del self._buffer[key]
if key in self._disk_data:
del self._disk_data[key]
else:
raise KeyError(key)
def __len__(self) -> int:
return len(self._disk_data)
def __contains__(self, key: K) -> bool:
return key in self._disk_data
def _add_to_buffer(self, key: K, value: V):
"""Add to buffer with LRU eviction"""
if len(self._buffer) >= self._max_buffer_size // 16:
# Evict oldest (simplified LRU)
oldest = next(iter(self._buffer))
del self._buffer[oldest]
self._buffer[key] = value
def get(self, key: K, default: Any = None) -> Any:
try:
return self[key]
except KeyError:
return default
def pop(self, key: K, default: Any = None) -> Any:
try:
value = self[key]
del self[key]
return value
except KeyError:
return default
def items(self):
return self._disk_data.items()
class CompressedTrie:
"""Space-efficient trie with compression"""
def __init__(self):
self.root = {}
self.compression_threshold = 10 # Compress paths longer than this
def insert(self, key: str, value: Any):
"""Insert with path compression"""
node = self.root
i = 0
while i < len(key):
# Check for compressed edge
for edge, (child, compressed_path) in list(node.items()):
if edge == '_compressed' and key[i:].startswith(compressed_path):
i += len(compressed_path)
node = child
break
else:
# Normal edge
if key[i] not in node:
# Check if we should compress
remaining = key[i:]
if len(remaining) > self.compression_threshold:
# Create compressed edge
node['_compressed'] = ({}, remaining)
node = node['_compressed'][0]
break
else:
node[key[i]] = {}
node = node[key[i]]
i += 1
node['_value'] = value
def search(self, key: str) -> Optional[Any]:
"""Search with compressed paths"""
node = self.root
i = 0
while i < len(key) and node:
# Check compressed edge
if '_compressed' in node:
child, compressed_path = node['_compressed']
if key[i:].startswith(compressed_path):
i += len(compressed_path)
node = child
continue
# Normal edge
if key[i] in node:
node = node[key[i]]
i += 1
else:
return None
return node.get('_value') if node else None
def create_optimized_structure(hint_type: str = 'auto', **kwargs) -> CacheAwareStructure:
"""Factory for creating optimized data structures"""
if hint_type == 'auto':
return AdaptiveMap(**kwargs)
elif hint_type == 'btree':
return CacheOptimizedBTree()
elif hint_type == 'hash':
return CacheOptimizedHashTable()
elif hint_type == 'external':
return ExternalMemoryMap()
else:
return AdaptiveMap(**kwargs)
# Example usage and benchmarks
if __name__ == "__main__":
print("Cache-Aware Data Structures Example")
print("="*60)
# Example 1: Adaptive map
print("\n1. Adaptive Map Demo")
adaptive_map = AdaptiveMap[str, int]()
# Insert increasing amounts of data
sizes = [3, 10, 100, 1000, 10000]
for size in sizes:
print(f"\nInserting {size} elements...")
for i in range(size):
adaptive_map.put(f"key_{i}", i)
stats = adaptive_map.get_stats()
print(f" Implementation: {stats['implementation']}")
print(f" Memory level: {stats['memory_level']}")
# Example 2: Cache line aware sizing
print("\n\n2. Cache Line Optimization")
hierarchy = MemoryHierarchy.detect_system()
print(f"System cache hierarchy:")
print(f" L1: {hierarchy.l1_size / 1024}KB")
print(f" L2: {hierarchy.l2_size / 1024}KB")
print(f" L3: {hierarchy.l3_size / 1024 / 1024}MB")
# Calculate optimal sizes
cache_line = 64
entry_size = 16 # 8-byte key + 8-byte value
print(f"\nOptimal structure sizes:")
print(f" Entries per cache line: {cache_line // entry_size}")
print(f" B-tree node size: {cache_line // entry_size} keys")
print(f" Hash table bucket size: {cache_line} bytes")
# Example 3: Performance comparison
print("\n\n3. Performance Comparison")
n = 10000
# Standard Python dict
start = time.time()
standard_dict = {}
for i in range(n):
standard_dict[f"key_{i}"] = i
for i in range(n):
_ = standard_dict.get(f"key_{i}")
standard_time = time.time() - start
# Adaptive map
start = time.time()
adaptive = AdaptiveMap[str, int]()
for i in range(n):
adaptive.put(f"key_{i}", i)
for i in range(n):
_ = adaptive.get(f"key_{i}")
adaptive_time = time.time() - start
print(f"Standard dict: {standard_time:.3f}s")
print(f"Adaptive map: {adaptive_time:.3f}s")
print(f"Overhead: {(adaptive_time / standard_time - 1) * 100:.1f}%")
# Example 4: Compressed trie
print("\n\n4. Compressed Trie Demo")
trie = CompressedTrie()
# Insert strings with common prefixes
urls = [
"http://example.com/api/v1/users/123",
"http://example.com/api/v1/users/456",
"http://example.com/api/v1/products/789",
"http://example.com/api/v2/users/123",
]
for url in urls:
trie.insert(url, f"data_for_{url}")
# Search
for url in urls[:2]:
result = trie.search(url)
print(f"Found: {url} -> {result}")
print("\n" + "="*60)
print("Cache-aware structures provide better performance")
print("by adapting to hardware memory hierarchies.")