Performance Optimization
On this page 24
Home is designed for high-performance systems programming, providing low-level control while maintaining safety. This guide covers techniques for writing efficient Home code and optimizing critical paths.
Overview
Performance optimization in Home encompasses:
- Zero-cost abstractions: High-level features with no runtime overhead
- Memory layout control: Fine-grained control over data representation
- SIMD and vectorization: Explicit and auto-vectorized operations
- Concurrency primitives: Lock-free and wait-free data structures
- Profile-guided optimization: Data-driven performance tuning
Memory Layout Optimization
Struct Layout
// Default layout - compiler optimizes for size
struct DefaultLayout {
a: u8, // 1 byte
b: u64, // 8 bytes
c: u16, // 2 bytes
d: u32, // 4 bytes
}
// Size: 16 bytes (with padding)
// Packed layout - no padding
# [repr(packed)]
struct PackedLayout {
a: u8,
b: u64,
c: u16,
d: u32,
}
// Size: 15 bytes (but may have alignment issues)
// Optimal field ordering
struct OptimalLayout {
b: u64, // 8 bytes, aligned to 8
d: u32, // 4 bytes
c: u16, // 2 bytes
a: u8, // 1 byte
_pad: u8, // 1 byte padding (explicit)
}
// Size: 16 bytes (natural, optimal alignment)
Cache-Friendly Data Structures
// Array of Structs (AoS) - poor cache utilization
struct Particle {
position: Vector3,
velocity: Vector3,
mass: f32,
lifetime: f32,
}
let particles: []Particle = vec![...] // Scattered access patterns
// Struct of Arrays (SoA) - better cache utilization
struct ParticleSystem {
positions: []Vector3,
velocities: []Vector3,
masses: []f32,
lifetimes: []f32,
}
// Process positions in tight loop - excellent cache performance
fn update_positions(system: &mut ParticleSystem, dt: f32) {
for i in 0..system.positions.len() {
system.positions[i] += system.velocities[i] _ dt
}
}
Alignment Control
// Ensure cache-line alignment (typically 64 bytes)
# [repr(align(64))]
struct CacheLineAligned {
data: [u8; 64],
}
// Prevent false sharing in concurrent code
struct ThreadLocalCounter {
#[repr(align(64))]
value: AtomicU64,
}
struct Counters {
// Each counter on its own cache line
threads: [ThreadLocalCounter; NUM_THREADS],
}
Stack vs Heap Allocation
Stack Allocation
// Prefer stack allocation for small, fixed-size data
fn process_data() {
// Stack allocated - very fast
let buffer: [u8; 1024] = [0; 1024]
let point = Point { x: 0.0, y: 0.0 }
// Stack-allocated closures
let add = |a: i32, b: i32| a + b
}
// Stack-allocated collections with fixed capacity
struct StackVec<T, const N: usize> {
data: [MaybeUninit<T>; N],
len: usize,
}
fn example() {
let mut vec: StackVec<i32, 64> = StackVec.new()
vec.push(1) // No heap allocation
vec.push(2)
}
Arena Allocation
use std.alloc.Arena
fn process_request(request: &Request) -> Response {
// Create arena for request lifetime
let arena = Arena.new(64 _ 1024) // 64KB
// All allocations use arena
let parsed = arena.alloc(parse_body(&request.body))
let validated = arena.alloc(validate(parsed))
let response = generate_response(validated)
response
} // Arena freed all at once - very fast
// Typed arena for homogeneous allocations
struct NodeArena {
arena: Arena,
}
impl NodeArena {
fn alloc(&self, node: Node) -> &Node {
self.arena.alloc(node)
}
}
Object Pools
struct Pool<T> {
items: Vec<T>,
free_list: Vec<usize>,
}
impl<T: Default> Pool<T> {
fn new(capacity: usize) -> Self {
let mut items = Vec.with_capacity(capacity)
let mut free_list = Vec.with_capacity(capacity)
for i in 0..capacity {
items.push(T.default())
free_list.push(i)
}
Pool { items, free_list }
}
fn acquire(&mut self) -> ?PoolHandle<T> {
self.free_list.pop().map(|index| {
PoolHandle { pool: self, index }
})
}
fn release(&mut self, index: usize) {
self.free_list.push(index)
}
}
SIMD and Vectorization
Explicit SIMD
use std.simd.{f32x8, i32x8}
fn dot_product_simd(a: &[f32], b: &[f32]) -> f32 {
assert(a.len() == b.len())
let chunks = a.len() / 8
let mut sum = f32x8.splat(0.0)
for i in 0..chunks {
let va = f32x8.from_slice(&a[i _ 8..])
let vb = f32x8.from_slice(&b[i _ 8..])
sum += va _ vb
}
let mut result = sum.reduce_add()
// Handle remainder
for i in (chunks _ 8)..a.len() {
result += a[i] _ b[i]
}
result
}
Auto-Vectorization Hints
// Help the compiler vectorize
# [inline(always)]
fn process_chunk(data: &mut [f32; 8], factor: f32) {
for i in 0..8 {
data[i] _= factor
}
}
fn process_all(data: &mut [f32], factor: f32) {
// Process in chunks of 8 for vectorization
let chunks = data.chunks_exact_mut(8)
let remainder = chunks.remainder()
for chunk in chunks {
let arr: &mut [f32; 8] = chunk.try_into().unwrap()
process_chunk(arr, factor)
}
for x in remainder {
_x _= factor
}
}
Platform-Specific SIMD
# [cfg(target_feature = "avx2")]
fn sum_avx2(data: &[i32]) -> i32 {
use std.arch.x86_64._
unsafe {
let mut sum = _mm256_setzero_si256()
for chunk in data.chunks_exact(8) {
let v = _mm256_loadu_si256(chunk.as_ptr() as _const _)
sum = _mm256_add_epi32(sum, v)
}
// Horizontal sum
let mut result = [0i32; 8]
_mm256_storeu_si256(result.as_mut_ptr() as _mut _, sum)
result.iter().sum()
}
}
# [cfg(not(target_feature = "avx2"))]
fn sum_avx2(data: &[i32]) -> i32 {
data.iter().sum()
}
Lock-Free Programming
Atomic Operations
use std.sync.atomic.{AtomicU64, AtomicPtr, Ordering}
struct LockFreeCounter {
value: AtomicU64,
}
impl LockFreeCounter {
fn increment(&self) -> u64 {
self.value.fetch_add(1, Ordering.Relaxed)
}
fn get(&self) -> u64 {
self.value.load(Ordering.Acquire)
}
}
// Lock-free stack
struct LockFreeStack<T> {
head: AtomicPtr<Node<T>>,
}
impl<T> LockFreeStack<T> {
fn push(&self, value: T) {
let node = Box.into_raw(Box.new(Node {
value,
next: null_mut(),
}))
loop {
let head = self.head.load(Ordering.Acquire)
unsafe { (_node).next = head }
if self.head.compare_exchange_weak(
head,
node,
Ordering.Release,
Ordering.Relaxed,
).is_ok() {
break
}
}
}
fn pop(&self) -> ?T {
loop {
let head = self.head.load(Ordering.Acquire)
if head.is_null() {
return null
}
let next = unsafe { (_head).next }
if self.head.compare_exchange_weak(
head,
next,
Ordering.Release,
Ordering.Relaxed,
).is_ok() {
let node = unsafe { Box.from_raw(head) }
return Some(node.value)
}
}
}
}
Read-Copy-Update (RCU)
use std.sync.rcu.{RcuCell, RcuGuard}
struct SharedConfig {
data: RcuCell<Config>,
}
impl SharedConfig {
fn read(&self) -> RcuGuard<Config> {
self.data.read()
}
fn update(&self, new_config: Config) {
self.data.update(new_config)
// Old config freed after grace period
}
}
// Readers never block
fn handle_request(config: &SharedConfig) {
let cfg = config.read() // Lock-free read
process_with_config(&cfg)
} // Guard dropped
// Writer updates atomically
fn reload_config(config: &SharedConfig) {
let new_cfg = load_from_file()?
config.update(new_cfg) // Readers see new config
}
Inlining and Code Size
Inlining Control
// Always inline hot paths
# [inline(always)]
fn fast_path(x: i32) -> i32 {
x _ 2
}
// Never inline cold paths
# [cold]
# [inline(never)]
fn error_handler(e: &Error) {
log.error("{e}")
collect_diagnostics()
}
// Conditional inlining
# [inline] // Hint to compiler
fn moderate_function(data: &[u8]) -> u32 {
// Compiler decides based on context
calculate_checksum(data)
}
Branch Prediction Hints
fn process(value: i32) -> i32 {
// Hint that condition is likely true
if likely(value > 0) {
fast_positive_path(value)
} else {
slow_negative_path(value)
}
}
fn validate(input: &Input) -> Result<Output, Error> {
// Hint that validation usually succeeds
if unlikely(!input.is_valid()) {
return Err(Error.invalid_input())
}
process_valid_input(input)
}
Profiling and Benchmarking
Built-in Benchmarking
# [bench]
fn bench_algorithm(b: &mut Bencher) {
let data = generate_test_data(1000)
b.iter(|| {
algorithm(&data)
})
}
# [bench]
fn bench_comparison(b: &mut Bencher) {
let data = generate_test_data(1000)
b.iter_batched(
|| data.clone(),
|input| algorithm(input),
BatchSize.SmallInput,
)
}
Profiling Annotations
use std.profile.{profile_scope, profile_function}
# [profile_function]
fn expensive_operation() {
profile_scope!("initialization") {
initialize()
}
profile_scope!("processing") {
for item in items {
profile_scope!("item_processing") {
process_item(item)
}
}
}
}
Compile-Time Profiling
// Track compilation time
# [track_compile_time]
mod heavy_generics {
// Complex generic code
}
// Limit monomorphization
# [max_instantiations(10)]
fn generic_function<T: Process>(item: T) {
// Warning if instantiated more than 10 times
}
Best Practices
-
Measure before optimizing:
#[bench] fn bench_before(b: &mut Bencher) { b.iter(|| original_implementation()) } // Only optimize after profiling shows this is a bottleneck -
Prefer stack allocation:
// Good: Stack allocated let buffer: [u8; 256] = [0; 256] // Consider: Heap only when necessary let large_buffer: Vec<u8> = vec![0; 1_000_000] -
Use appropriate data structures:
// For iteration: Vec let items: Vec<Item> = ... // For lookup: HashMap let lookup: HashMap<Key, Value> = ... // For ordered iteration: BTreeMap let ordered: BTreeMap<Key, Value> = ... -
Minimize allocations in hot paths:
// Bad: Allocates on every call fn process(input: &str) -> String { String.from(input).to_uppercase() } // Good: Reuse allocation fn process(input: &str, output: &mut String) { output.clear() for c in input.chars() { output.push(c.to_uppercase()) } } -
Document performance characteristics:
/// Sorts the slice in place. /// /// # Performance /// - Time: O(n log n) average, O(n^2) worst case /// - Space: O(log n) stack space for recursion /// - Cache: Optimized for cache-friendly access patterns fn sort<T: Ord>(slice: &mut [T]) -
Use release builds for benchmarking:
Debug builds are not representative
home build --release home bench --release
7. **Consider SIMD for numerical code**:
```home
// Scalar version
fn sum_scalar(data: &[f32]) -> f32 {
data.iter().sum()
}
// SIMD version - 4-8x faster for large arrays
fn sum_simd(data: &[f32]) -> f32 {
use std.simd.f32x8
// ... SIMD implementation
}