agentsclimarketplace

R performance

Skill alexvantwisk/supeRpowers/skills/r-performance

Use when optimizing R code for speed or memory. Provides a profile-first methodology covering bench, profvis, data.table, vectorization, Rcpp, parallel processing, memory management, and proof-of-improvement benchmarks. Do NOT use for general data wrangling at normal scale — use r-data-analysis instead. Do NOT use for debugging errors — use r-debugging instead. Do NOT use for ML model performance metrics (accuracy, ROC, AUC) — use r-tidymodels instead.From its SKILL.md

Install
npx -y skills add alexvantwisk/supeRpowers --skill r-performance

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 2 stars2 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

10.3 KB, as published. Nobody here has run it

R Performance Optimization

Profile first. Optimize the measured bottleneck. Prove the improvement. All code uses base pipe |>, <- for assignment, and tidyverse style.

Lazy references:

  • Read references/profiling-workflow.md for systematic profiling and flame-graph interpretation
  • Read references/data-table-translation.md for complete dplyr ↔ data.table side-by-side syntax

Agent dispatch: Dispatch to r-code-reviewer after optimization for code quality review.


Decision Tree: When to Optimize What

Code is slow / using too much memory?
  └─ Profile first with profvis/bench
       └─ Bottleneck is I/O?          → fread/fwrite, vroom, chunked reading
       └─ Bottleneck is computation?
            └─ Loops / vectorizable?  → vectorize, purrr, vapply
            └─ Data manipulation?     → data.table (>1M rows)
            └─ Numerical inner loop?  → Rcpp
            └─ Independent tasks?     → mirai + purrr::in_parallel()

Never parallelize before profiling — overhead often exceeds gains.


Profiling & Benchmarking

library(profvis)
library(bench)

# Visual flame graph (BEST for finding hot spots)
profvis::profvis({
  your_code_here()
})

# Microbenchmarking — always check = TRUE to validate equivalence
bench::mark(
  approach_a = dplyr_version(df),
  approach_b = dt_version(dt),
  check = TRUE,
  iterations = 100
)

# Parameter grid benchmarks
bench::press(
  rows = c(1e4, 1e5, 1e6),
  {
    dt <- data.table::as.data.table(generate_data(rows))
    bench::mark(dt[, .(mean_v = mean(value)), by = group])
  }
)

# Quick check (one-off)
system.time(slow_function(df))

Read references/profiling-workflow.md for the full 5-step workflow.


data.table

Boundary: Optimization-focused data.table usage for large-scale data. For general data wrangling at normal scale, use r-data-analysis instead.

Use when: dataset >1M rows, memory-constrained, or performance-critical loops.

library(data.table)

dt <- data.table::fread("large.csv")        # fast I/O
data.table::fwrite(dt, "output.csv")        # fast write

# Syntax: dt[i, j, by]
dt[value > 0, .(mean_v = mean(value), n = .N), by = .(group, year)]

# Reference semantics — modify in place (no copy)
dt[, new_col := value * 2]                  # := adds/modifies column
dt[, c("a", "b") := NULL]                  # remove columns
data.table::setnames(dt, "old", "new")      # rename in place
data.table::setDT(existing_df)             # convert df to DT in place

# Keys for fast subsetting / joining
data.table::setkey(dt, id, date)
dt["id_001"]                               # fast key lookup

# Chaining
dt[value > 0][order(-value)][, head(.SD, 3), by = group]

Read references/data-table-translation.md for dplyr ↔ data.table translation.


Large-data tiers

TierSituationTools
AFits in RAMdplyr, data.table, collapse
BLarger than RAM, one machinearrow::open_dataset() (multi-file Parquet), duckdb/duckplyr
CParallel computemirai + purrr::in_parallel(); crew for pipelines
Read references/large-data-tiers.md for arrow/duckdb/mirai recipes.

Vectorization

# AVOID: growing vector in loop
result <- c()
for (x in items) result <- c(result, transform(x))  # O(n²) copies

# CORRECT: pre-allocate
result <- vector("numeric", length(items))
for (i in seq_along(items)) result[[i]] <- transform(items[[i]])

# BETTER: vectorized
result <- transform(items)          # if transform is vectorized
result <- vapply(items, f, numeric(1))  # vapply > sapply (type-safe)

# Tidyverse style
library(purrr)
results <- purrr::map_dbl(items, transform)    # returns numeric vector
results <- purrr::map(items, build_row) |> purrr::list_rbind()  # data frame

# Column-wise with across
df |> mutate(across(where(is.numeric), \(x) x / max(x, na.rm = TRUE)))

# Cumulative operations (no loop needed)
df |> mutate(running_total = cumsum(value), rolling_max = cummax(value))

Memory Management

library(lobstr)

lobstr::obj_size(large_df)           # actual memory footprint
tracemem(df)                         # track copies

# Avoid unnecessary copies — data.table reference semantics
data.table::setDT(df)                # in-place, no copy
dt[, col := value]                   # in-place column add

# Lazy / chunked reading for files larger than RAM
library(vroom)
df <- vroom::vroom("huge.csv",       # lazy column reads
  col_select = c(id, date, value))

library(readr)
readr::read_csv_chunked("huge.csv",
  callback = DataFrameCallback$new(\(chunk, pos) process(chunk)),
  chunk_size = 50000
)

R uses copy-on-modify: any modification of a shared object triggers a full copy. Use data.table::setDT() and := to work in-place and avoid copies.


Rcpp

Use when: tight numerical loops, simulation inner loops, custom algorithms.

# Inline (prototyping)
Rcpp::cppFunction("
  NumericVector cumsum_capped(NumericVector x, double cap) {
    int n = x.size();
    NumericVector out(n);
    double running = 0;
    for (int i = 0; i < n; i++) {
      running = std::min(running + x[i], cap);
      out[i] = running;
    }
    return out;
  }
")

# From file (production)
Rcpp::sourceCpp("src/my_algo.cpp")   # exports functions automatically

# Package integration
usethis::use_rcpp()                  # adds LinkingTo, creates src/

Rcpp sugar (sum(), mean(), pow(), ifelse()) lets you write C++-speed code with R-like syntax. Use NumericVector, IntegerVector, LogicalVector.


Parallel Processing

Lead with mirai (modern backend); purrr integrates via in_parallel().

library(mirai); library(purrr)
daemons(4)                                   # start workers
results <- map(items, in_parallel(\(x) slow_fn(x)))   # purrr >= 1.1
daemons(0)                                   # stop workers

crew (mirai-powered) is the pipeline-scale backend — see r-targets. furrr and future still work for existing code; both are covered in references/large-data-tiers.md.

When NOT to parallelize: task takes <100ms, data-transfer overhead dominates, or memory per worker exceeds available RAM.


Gotchas

TrapWhy It FailsFix
result <- c(result, x) in loopO(n²) copies — each append copies the entire vectorPre-allocate with vector() or use purrr::map()
df <- rbind(df, row) in loopO(n²) copies — same growth problem as aboveCollect rows in a list, then dplyr::bind_rows() once
apply() on data frame columnsSilently coerces entire data frame to matrix (all character if mixed types)Use dplyr::across() or vapply() instead
paste() in tight loopNot vectorized across iterations; slow for 1M+ itemsUse stringr::str_c() or vectorized paste0() outside the loop
df$col <- ... on large shared dfTriggers full copy-on-modify of the entire data frameConvert with data.table::setDT() and use := for in-place modification
sapply() instead of vapply()Return type depends on input length — silently returns list or matrixAlways use vapply(x, f, FUN.VALUE = numeric(1)) for type safety
bench::mark() with check = FALSEMay compare non-equivalent operations; benchmark "wins" are meaninglessKeep check = TRUE (default); only disable after manually verifying equivalence
setDT() mutates the original data frameCaller's copy silently becomes a data.table — breaks downstream code expecting a data.frameUse as.data.table(df) when the original must stay unchanged
profvis not showing the real bottleneckprofvis only surfaces R-level code; C-level and compiled bottlenecks are invisibleCombine with Rprof(line.profiling = TRUE) or bench::mark() to isolate compiled-code time
Rewriting entire codebase when user asked to speed up one functionScope creep — premature optimization of non-bottleneck code wastes effortProfile first, optimize only the measured hotspot, and prove the improvement with bench::mark()

Examples

Happy Path: Profile, identify bottleneck, vectorize fix

Prompt: "This function is slow on 1M rows — find the bottleneck and fix it."

# Input — profile reveals row-binding in a loop
profvis::profvis({
  result <- data.frame()
  for (i in seq_len(nrow(df))) {
    result <- rbind(result, transform_row(df[i, ]))  # O(n^2) copies
  }
})

# Output — vectorized replacement (100x faster)
result <- df |>
  dplyr::mutate(score = value * weight + offset)

# Prove the improvement
bench::mark(
  loop_version   = slow_fn(df),
  vector_version = fast_fn(df),
  check = TRUE
)
#>   expression          min   median `itr/sec` mem_alloc
#>   loop_version      4.2s     4.5s      0.22    1.8GB
#>   vector_version   42ms    48ms       21.       15MB

Edge Case: setDT() mutates the caller's original data frame

Prompt: "I converted my df to data.table but now my original df is also a data.table."

# WRONG — setDT() modifies in place; original_df is silently mutated
original_df <- data.frame(id = 1:5, value = rnorm(5))
dt <- original_df
data.table::setDT(dt)
class(original_df)
#> [1] "data.table" "data.frame"   # original_df is now a data.table!

# CORRECT — use as.data.table() to create an independent copy
original_df <- data.frame(id = 1:5, value = rnorm(5))
dt <- data.table::as.data.table(original_df)
class(original_df)
#> [1] "data.frame"                  # original_df is untouched

More example prompts:

  • "This dplyr pipeline is too slow on 5M rows -- convert to data.table."
  • "Parallelize this purrr::map() call that processes 500 files."
  • "Rewrite this simulation inner loop in Rcpp."

Keep looking

Skills are one crate of 627,559. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.