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

  1. Measure before optimizing:

    #[bench]
    fn bench_before(b: &mut Bencher) {
        b.iter(|| original_implementation())
    }
    
    // Only optimize after profiling shows this is a bottleneck
    
  2. 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]
    
  3. 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> = ...
    
  4. 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())
        }
    }
    
  5. 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])
    
  6. 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
}

Released under the MIT License.