--- title: "Performance Optimization Guide" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Performance Optimization Guide} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) ``` # pladdrr Performance Optimization Guide This guide explains the **3-tier performance API** in pladdrr and how to choose the right level for your needs. ## The Three Performance Tiers pladdrr provides three API tiers, each trading interface simplicity for lower per-call overhead: ``` ┌─────────────────────────────────────────────────────────┐ │ TIER 1: High-Level API (Sound, Pitch, Formant, etc.) │ │ - Full object-oriented interface │ │ - Method chaining: sound$to_pitch()$get_mean() │ │ - Best for: Interactive analysis, small datasets │ └─────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────┐ │ TIER 2: Direct API (*_direct functions) │ │ - Accept XPtr directly, skip object dispatch │ │ - Best for: Tight loops, repeated queries │ │ - Reduces per-call R dispatch overhead │ └─────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────┐ │ TIER 3: Batch/Parallel API (*_batch, *_parallel) │ │ - Designed for bulk operations │ │ - Best for: Large datasets, production workflows │ │ - Replaces many R->C crossings with one per call │ └─────────────────────────────────────────────────────────┘ ``` --- ## When do the tiers actually matter? It depends entirely on how much work each call does relative to the R-to-C++ boundary it crosses. - **Querying in a tight loop** — thousands of cheap point queries — is where the batch and direct APIs are worth reaching for, because the per-call dispatch overhead is a larger share of the total cost. Going from one crossing per query to one crossing per vector changes the shape of that cost. - **A single analysis call** — `to_pitch()`, `to_formant_burg()`, `calculate_cpps_fast()` — is dominated by the underlying DSP work, and the tier you call it through makes little practical difference. The different CPPS entry points (`sound$to_powercepstrogram()$get_cpps()`, `calculate_cpps_fast()`, `calculate_cpps_ultra()`) all compute the same value using the same core algorithm. Rule of thumb: reach for a batch or direct function when you are about to write a loop over query points or files. Switching analysis entry points for a single call is not expected to help — measure your own workload with `system.time()` if you're unsure. --- ## Tier 1: High-Level API (Best for Most Users) **When to use:** Interactive analysis, exploration, small datasets (<100 files) **Characteristics:** - Intuitive object-oriented interface - Method chaining - Full error checking and validation - Easy to read and maintain ```{r tier1} library(pladdrr) wav_file <- system.file("extdata", "test.wav", package = "pladdrr") # Load sound sound <- Sound(wav_file) # Extract pitch (returns Pitch object) pitch <- sound$to_pitch(pitch_floor = 75, pitch_ceiling = 300) # Query statistics mean_f0 <- pitch$get_mean(0, 0, "hertz") sd_f0 <- pitch$get_standard_deviation(0, 0, "hertz") # Method chaining mean_f1 <- sound$to_formant()$get_mean(1, 0, 0, "hertz") ``` Adequate for most use cases. Only reach for Tier 2/3 if profiling your own code shows a bottleneck. --- ## Tier 2: Direct API **When to use:** Processing >100 files, tight loops, production code **Characteristics:** - Functions accept external pointers (XPtr) directly - Skip R6 object dispatch overhead - Return raw values or pointers - Require manual pointer extraction ```{r tier2} # Extract pointer once sound_ptr <- sound$.xptr # Direct conversion (returns XPtr, not Pitch object) pitch_ptr <- to_pitch_direct(sound_ptr, time_step = 0, pitch_floor = 75, pitch_ceiling = 300) # Get statistics in one call stats <- get_pitch_stats_direct(pitch_ptr) # Returns: list(min, max, mean, stdev, median, q25, q75, count_voiced) # Get F1-F4 in one call formant_ptr <- to_formant_direct(sound_ptr) formants <- get_formants_direct(formant_ptr, time = 0.5) # Returns: c(F1 = 500, F2 = 1500, F3 = 2500, F4 = 3500) ``` ### Fewer R→C boundary crossings ```{r tier2_comparison} # Tier 1 approach: one boundary crossing per query min_f0 <- pitch$get_minimum(0, 0, "hertz") max_f0 <- pitch$get_maximum(0, 0, "hertz") mean_f0 <- pitch$get_mean(0, 0, "hertz") # ... 5 more calls # Tier 2 approach: one boundary crossing for all statistics stats <- get_pitch_stats_direct(pitch) ``` ### Available Direct Functions **Conversion:** - `to_pitch_direct()` - Create Pitch - `to_formant_direct()` - Create Formant - `to_intensity_direct()` - Create Intensity - `to_harmonicity_direct()` - Create Harmonicity - `to_spectrum_direct()` - Create Spectrum - `to_spectrogram_direct()` - Create Spectrogram - `to_ltas_direct()` - Create LTAS - `to_point_process_direct()` - Create PointProcess **Queries:** - `get_pitch_stats_direct()` - All pitch statistics at once - `get_formants_direct()` - F1-F4 at time point - `get_pitch_value_direct()` - Single pitch value - `get_intensity_value_direct()` - Single intensity value - `get_formant_value_direct()` - Single formant value --- ## Tier 3: Batch & Parallel API **When to use:** Large datasets (>100 files), production pipelines ### Batch Operations Process multiple sounds in a single C++ call: ```{r tier3_batch} # In practice `files` would be a vector of paths to distinct recordings, e.g. # list.files("audio/", pattern = "\\.wav$", full.names = TRUE). This example # repeats the bundled test file to keep the vignette self-contained. files <- rep(wav_file, 3) # Load multiple sounds sounds <- lapply(files, Sound) # Tier 1 approach: one R->C crossing per sound pitches <- lapply(sounds, function(s) s$to_pitch()) # Tier 3 approach: one R->C crossing for the whole list pitches <- sound_to_pitch_batch(sounds) ``` **Available batch functions:** - `sound_to_pitch_batch()` - Batch pitch extraction - `sound_to_pitch_ac_batch()` - Batch autocorrelation pitch - `sound_to_pitch_cc_batch()` - Batch cross-correlation pitch - `sound_to_formant_batch()` - Batch formant extraction - `sound_to_intensity_batch()` - Batch intensity extraction ### Vectorized Queries Extract values at multiple time points in one call: ```{r vectorized} # Tier 1 approach: one R->C crossing per time point times <- seq(0.1, 1.0, by = 0.01) f0_values <- vapply(times, function(t) { pitch$get_value_at_time(t, "hertz") }, numeric(1)) # Tier 3 approach: one R->C crossing for the whole vector f0_values <- get_pitch_at_times(pitch, times) ``` **Vectorized query functions:** - `get_pitch_at_times()` - Batch pitch queries - `get_formants_at_times()` - Batch F1-F4 queries - `get_intensity_at_times()` - Batch intensity queries ### Parallel Processing Distribute file-level work across multiple CPU cores: ```{r parallel} # Analyze files in parallel. In practice `files` is a vector of paths to # distinct recordings, e.g. list.files("audio/", pattern = "\\.wav$", # full.names = TRUE); this example repeats the bundled test file. files <- rep(wav_file, 4) # Simple parallel analysis results <- analyze_files_parallel(files, function(sound) { pitch <- sound$to_pitch() list( mean_f0 = pitch$get_mean(0, 0, "hertz"), sd_f0 = pitch$get_standard_deviation(0, 0, "hertz") ) }, n_cores = 2) # Convenience functions pitches <- extract_pitch_parallel(files, n_cores = 2) formants <- extract_formant_parallel(files, n_cores = 2) intensities <- extract_intensity_parallel(files, n_cores = 2) ``` **Parallel functions:** - `analyze_files_parallel()` - Generic parallel file processing - `process_sounds_parallel()` - Parallel processing of loaded sounds - `extract_pitch_parallel()` - Parallel pitch extraction - `extract_formant_parallel()` - Parallel formant extraction - `extract_intensity_parallel()` - Parallel intensity extraction - `benchmark_parallel()` - Measure your own workload across core counts --- ## Tier Comparison Examples The examples below use `system.time()` so you can measure each approach against your own files and hardware — the numbers vary too much by machine, file size, and workload to be worth quoting here. ### Example 1: Single File Analysis ```{r example1} sound <- Sound(wav_file) # Tier 1: Standard API system.time({ pitch <- sound$to_pitch() mean_f0 <- pitch$get_mean(0, 0, "hertz") sd_f0 <- pitch$get_standard_deviation(0, 0, "hertz") min_f0 <- pitch$get_minimum(0, 0, "hertz") max_f0 <- pitch$get_maximum(0, 0, "hertz") }) # measure on your own data # Tier 2: Direct API system.time({ pitch_ptr <- to_pitch_direct(sound$.xptr) stats <- get_pitch_stats_direct(pitch_ptr) }) # fewer crossings; measure on your own data ``` ### Example 2: Batch Processing ```{r example2} # In practice `files` is a vector of paths to distinct recordings, e.g. # list.files("audio/", full.names = TRUE)[1:100]; this example repeats the # bundled test file to keep the vignette self-contained and fast to build. files <- rep(wav_file, 20) # Tier 1: Sequential system.time({ sounds <- lapply(files, Sound) pitches <- lapply(sounds, function(s) s$to_pitch()) }) # measure on your own data # Tier 3: Batch system.time({ sounds <- lapply(files, Sound) pitches <- sound_to_pitch_batch(sounds) }) # scales with cores; measure on your own data # Tier 3: Parallel (2 cores) system.time({ pitches <- extract_pitch_parallel(files, n_cores = 2) }) # includes file I/O; measure on your own data ``` ### Example 3: Formant Tracking Over Time ```{r example3} sound <- Sound(wav_file) formant <- sound$to_formant() times <- seq(0.1, 0.5, by = 0.001) # Tier 1: Loop system.time({ f1_values <- vapply(times, function(t) { formant$get_value_at_time(1, t, "hertz") }, numeric(1)) }) # one R->C crossing per time point # Tier 3: Vectorized system.time({ f1_values <- get_formants_at_times(formant, times, formant_numbers = 1) }) # one R->C crossing for the whole vector — this is where batching pays off ``` --- ## Decision Tree: Which Tier Should I Use? ``` Are you processing < 10 files? ├─ YES → Use Tier 1 (standard API) └─ NO ↓ Are you processing < 100 files? ├─ YES → Use Tier 2 (direct API) if profiling shows it helps └─ NO ↓ Are you processing > 100 files? ├─ Single machine → Use Tier 3 batch + parallel └─ Cluster → Use Tier 2 direct + your cluster manager ``` --- ## Best Practices ### 1. Start with Tier 1, Optimize Later Don't prematurely optimize. Use the standard API first: ```{r best_practice1} # Good: Clear and maintainable pitch <- sound$to_pitch()$smooth() mean_f0 <- pitch$get_mean(0, 0, "hertz") ``` Only move to Tier 2/3 when: - Profiling your own code shows a bottleneck - You're processing >100 files - Runtime matters for your workflow ### 2. Batch Similar Operations ```{r best_practice2} # Tier 1: Multiple individual queries f1 <- formant$get_value_at_time(1, 0.5, "hertz") f2 <- formant$get_value_at_time(2, 0.5, "hertz") f3 <- formant$get_value_at_time(3, 0.5, "hertz") f4 <- formant$get_value_at_time(4, 0.5, "hertz") # Tier 2: Single batch query formants <- get_formants_direct(formant, time = 0.5) ``` ### 3. Reuse Pointers in Loops ```{r best_practice3} # Avoid: Repeated single-value queries in a loop for (time in times) { val <- get_pitch_value_direct(pitch, time) } # Prefer: One call for the whole vector of times # (get_pitch_at_times() takes the Pitch object itself, not an XPtr — it # extracts the pointer internally) values <- get_pitch_at_times(pitch, times) ``` ### 4. Choose a Core Count That Fits Your Workload Not all workloads scale linearly with core count, and small files can make parallelization overhead outweigh any benefit. Use `benchmark_parallel()` to test a range of core counts on a representative subset of your own files before committing to a setting for a full run: ```{r best_practice4} # Test different core counts on a subset benchmark_results <- benchmark_parallel( files[1:10], # Use subset for a quick test function(s) s$to_pitch()$get_mean(0, 0, "hertz"), core_counts = c(1, 2) # extend to c(1, 2, 4, 8) etc. up to your machine's cores ) print(benchmark_results) # Inspect the returned table to see where returns diminish on your machine ``` ### 5. Combine Batch and Parallel Processing ```{r best_practice5} # Define the per-file constants *inside* the worker function rather than # capturing them from the enclosing environment. On Windows (and on macOS, # where analyze_files_parallel() also uses a PSOCK cluster to avoid known # fork/event-loop issues), each worker is a separate R process that only # receives the function and file path — free variables referenced from the # calling environment are not automatically exported and would error with # "object not found" unless passed via clusterExport() or defined locally # like this. analyze_files_parallel(files, function(sound) { start_times <- c(0.1, 0.3) end_times <- c(0.2, 0.4) analysis_times <- seq(0.01, 0.05, by = 0.01) # Use batch operations within each worker parts <- sound$extract_parts_batch(start_times, end_times) formants <- sound_to_formant_batch(parts) # Vectorized queries lapply(formants, function(f) { get_formants_at_times(f, analysis_times) }) }, n_cores = 2) ``` --- ## Troubleshooting ### Parallel Processing Issues **macOS/Linux:** - Uses `mclapply()` (fork-based parallelism) - Shares memory between processes **Windows:** - Uses `parLapply()` (socket-based parallelism) - Creates separate R processes - May need to export objects explicitly **Memory constraints:** - Each core loads files independently - With 4 cores and 100MB files, plan for roughly 400MB RAM - Reduce `n_cores` if you hit memory limits ### Runtime Not Improving? If parallelizing or batching your own workload isn't helping, check: 1. **I/O bound?** - File reading may be the bottleneck, not analysis 2. **Small files?** - Parallelization overhead can exceed any benefit 3. **Disk speed?** - Try processing from SSD instead of HDD 4. **CPU usage** - Use `htop` or Task Manager to see whether cores are saturated --- ## Summary Table | Tier | Use Case | Functions | Learning Curve | |------|----------|-----------|----------------| | 1 | Interactive, <10 files | `sound$to_pitch()` | Easy | | 2 | Loops, 10-100 files | `to_pitch_direct()` | Medium | | 3 | Production, >100 files | `sound_to_pitch_batch()` | Medium | | 3+ | Large datasets | `extract_pitch_parallel()` | Easy | --- ## Further Reading - `vignette("getting-started")` - Introduction to pladdrr - `?analyze_files_parallel` - Parallel processing documentation - `?get_pitch_stats_direct` - Direct API reference