From dec29d70ce6357fea4ae7bcf1647d973ac349f19 Mon Sep 17 00:00:00 2001 From: David Schoch Date: Fri, 4 Sep 2026 13:07:09 +0200 Subject: [PATCH] perf: batch-construct vertex sequence lists via create_vs_list() Functions returning many vertex sequences used `lapply(res, unsafe_create_vs, graph = graph, verts = V(graph))`, which re-read the graph reference, graph id and name source from `verts` on every object. Add `create_vs_list(graph, idx_list)`, which hoists all of that per-graph work out of the loop and builds each sequence with a single `attributes<-`, so per-object cost drops to an integer coercion, a name subset and one attribute set. The `VERTEXSET_LIST` OUTCONV template in tools/stimulus/types-RR.yaml now emits `create_vs_list(graph, res)`; the generated R/aaa-*.R files are updated to match (27 sites), along with the hand-written call sites in cliques.R, cohesive.blocks.R, components.R, conversion.R, interface.R, paths.R and structural-properties.R (10 sites). `unsafe_create_vs()` stays as the single-object form. Beyond the speedup this gives the construction loop a single named entry point, so it can later be moved wholesale (e.g. into C) without touching any call site. Edge sequences are left as-is: simple_es_index() already propagates the shared weak reference from a single E(graph), so unsafe_create_es() does not pay the per-object cost. An es batch form remains a follow-up. max_cliques(sample_gnp(200, 0.16)) on a named graph: ~7.8ms -> ~5.6ms. Output, names, NA handling and graph recovery are unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) Co-Authored-By: Claude Opus 5 (1M context) --- R/aaa-cliques.R | 18 ++++++++-------- R/aaa-cycles.R | 2 +- R/aaa-flows.R | 6 +++--- R/aaa-graphlets.R | 4 ++-- R/aaa-isomorphism.R | 2 +- R/aaa-paths.R | 14 ++++++------ R/aaa-separators.R | 4 ++-- R/aaa-structural.R | 4 ++-- R/cliques.R | 4 ++-- R/cohesive.blocks.R | 7 +----- R/components.R | 7 +----- R/conversion.R | 2 +- R/interface.R | 2 +- R/iterators.R | 41 ++++++++++++++++++++++++++++++++++++ R/paths.R | 2 +- R/structural-properties.R | 16 +++----------- tools/stimulus/types-RR.yaml | 2 +- 17 files changed, 79 insertions(+), 58 deletions(-) diff --git a/R/aaa-cliques.R b/R/aaa-cliques.R index 83f6472fdf4..42bcaf53a44 100644 --- a/R/aaa-cliques.R +++ b/R/aaa-cliques.R @@ -107,7 +107,7 @@ cliques_impl <- function( max ) if (igraph_opt("return.vs.es")) { - res <- lapply(res, unsafe_create_vs, graph = graph, verts = V(graph)) + res <- create_vs_list(graph, res) } res } @@ -163,7 +163,7 @@ largest_cliques_impl <- function( graph ) if (igraph_opt("return.vs.es")) { - res <- lapply(res, unsafe_create_vs, graph = graph, verts = V(graph)) + res <- create_vs_list(graph, res) } res } @@ -303,7 +303,7 @@ maximal_cliques_impl <- function( max_size ) if (igraph_opt("return.vs.es")) { - res <- lapply(res, unsafe_create_vs, graph = graph, verts = V(graph)) + res <- create_vs_list(graph, res) } res } @@ -338,7 +338,7 @@ maximal_cliques_subset_impl <- function( max_size ) if (igraph_opt("return.vs.es")) { - res$res <- lapply(res$res, unsafe_create_vs, graph = graph, verts = V(graph)) + res$res <- create_vs_list(graph, res$res) } if (!details) { res <- res$res @@ -383,7 +383,7 @@ independent_vertex_sets_impl <- function( max_size ) if (igraph_opt("return.vs.es")) { - res <- lapply(res, unsafe_create_vs, graph = graph, verts = V(graph)) + res <- create_vs_list(graph, res) } res } @@ -420,7 +420,7 @@ largest_independent_vertex_sets_impl <- function( graph ) if (igraph_opt("return.vs.es")) { - res <- lapply(res, unsafe_create_vs, graph = graph, verts = V(graph)) + res <- create_vs_list(graph, res) } res } @@ -438,7 +438,7 @@ maximal_independent_vertex_sets_impl <- function( graph ) if (igraph_opt("return.vs.es")) { - res <- lapply(res, unsafe_create_vs, graph = graph, verts = V(graph)) + res <- create_vs_list(graph, res) } res } @@ -468,7 +468,7 @@ largest_weighted_cliques_impl <- function( vertex_weights ) if (igraph_opt("return.vs.es")) { - res <- lapply(res, unsafe_create_vs, graph = graph, verts = V(graph)) + res <- create_vs_list(graph, res) } res } @@ -531,7 +531,7 @@ weighted_cliques_impl <- function( maximal ) if (igraph_opt("return.vs.es")) { - res <- lapply(res, unsafe_create_vs, graph = graph, verts = V(graph)) + res <- create_vs_list(graph, res) } res } diff --git a/R/aaa-cycles.R b/R/aaa-cycles.R index f7671c6bde0..c97622ce2f7 100644 --- a/R/aaa-cycles.R +++ b/R/aaa-cycles.R @@ -380,7 +380,7 @@ simple_cycles_impl <- function( max_cycle_length ) if (igraph_opt("return.vs.es")) { - res$vertices <- lapply(res$vertices, unsafe_create_vs, graph = graph, verts = V(graph)) + res$vertices <- create_vs_list(graph, res$vertices) } if (igraph_opt("return.vs.es")) { res$edges <- lapply(res$edges, unsafe_create_es, graph = graph, es = E(graph)) diff --git a/R/aaa-flows.R b/R/aaa-flows.R index 44f5b8faf0a..e01723400b3 100644 --- a/R/aaa-flows.R +++ b/R/aaa-flows.R @@ -17,7 +17,7 @@ cohesive_blocks_impl <- function( graph ) if (igraph_opt("return.vs.es")) { - res$blocks <- lapply(res$blocks, unsafe_create_vs, graph = graph, verts = V(graph)) + res$blocks <- create_vs_list(graph, res$blocks) } class(res) <- "cohesiveBlocks" res @@ -176,7 +176,7 @@ all_st_cuts_impl <- function( res$cuts <- lapply(res$cuts, unsafe_create_es, graph = graph, es = E(graph)) } if (igraph_opt("return.vs.es")) { - res$partition1s <- lapply(res$partition1s, unsafe_create_vs, graph = graph, verts = V(graph)) + res$partition1s <- create_vs_list(graph, res$partition1s) } res } @@ -225,7 +225,7 @@ all_st_mincuts_impl <- function( res$cuts <- lapply(res$cuts, unsafe_create_es, graph = graph, es = E(graph)) } if (igraph_opt("return.vs.es")) { - res$partition1s <- lapply(res$partition1s, unsafe_create_vs, graph = graph, verts = V(graph)) + res$partition1s <- create_vs_list(graph, res$partition1s) } res } diff --git a/R/aaa-graphlets.R b/R/aaa-graphlets.R index e9604885b0c..2a156b1c273 100644 --- a/R/aaa-graphlets.R +++ b/R/aaa-graphlets.R @@ -26,7 +26,7 @@ graphlets_candidate_basis_impl <- function( weights ) if (igraph_opt("return.vs.es")) { - res$cliques <- lapply(res$cliques, unsafe_create_vs, graph = graph, verts = V(graph)) + res$cliques <- create_vs_list(graph, res$cliques) } res } @@ -57,7 +57,7 @@ graphlets_impl <- function( niter ) if (igraph_opt("return.vs.es")) { - res$cliques <- lapply(res$cliques, unsafe_create_vs, graph = graph, verts = V(graph)) + res$cliques <- create_vs_list(graph, res$cliques) } res } diff --git a/R/aaa-isomorphism.R b/R/aaa-isomorphism.R index 343419a9d95..af52bd6da37 100644 --- a/R/aaa-isomorphism.R +++ b/R/aaa-isomorphism.R @@ -174,7 +174,7 @@ automorphism_group_impl <- function( sh ) if (igraph_opt("return.vs.es")) { - res$generators <- lapply(res$generators, unsafe_create_vs, graph = graph, verts = V(graph)) + res$generators <- create_vs_list(graph, res$generators) } if (!details) { res <- res$generators diff --git a/R/aaa-paths.R b/R/aaa-paths.R index 35daacbeb2d..95a29f097c4 100644 --- a/R/aaa-paths.R +++ b/R/aaa-paths.R @@ -784,7 +784,7 @@ get_all_shortest_paths_dijkstra_impl <- function( mode ) if (igraph_opt("return.vs.es")) { - res$vpaths <- lapply(res$vpaths, unsafe_create_vs, graph = graph, verts = V(graph)) + res$vpaths <- create_vs_list(graph, res$vpaths) } if (igraph_opt("return.vs.es")) { res$epaths <- lapply(res$epaths, unsafe_create_es, graph = graph, es = E(graph)) @@ -826,7 +826,7 @@ get_all_shortest_paths_impl <- function( mode ) if (igraph_opt("return.vs.es")) { - res$vpaths <- lapply(res$vpaths, unsafe_create_vs, graph = graph, verts = V(graph)) + res$vpaths <- create_vs_list(graph, res$vpaths) } if (igraph_opt("return.vs.es")) { res$epaths <- lapply(res$epaths, unsafe_create_es, graph = graph, es = E(graph)) @@ -931,7 +931,7 @@ get_k_shortest_paths_impl <- function( mode ) if (igraph_opt("return.vs.es")) { - res$vpaths <- lapply(res$vpaths, unsafe_create_vs, graph = graph, verts = V(graph)) + res$vpaths <- create_vs_list(graph, res$vpaths) } if (igraph_opt("return.vs.es")) { res$epaths <- lapply(res$epaths, unsafe_create_es, graph = graph, es = E(graph)) @@ -1206,7 +1206,7 @@ get_shortest_paths_bellman_ford_impl <- function( mode ) if (igraph_opt("return.vs.es")) { - res$vertices <- lapply(res$vertices, unsafe_create_vs, graph = graph, verts = V(graph)) + res$vertices <- create_vs_list(graph, res$vertices) } if (igraph_opt("return.vs.es")) { res$edges <- lapply(res$edges, unsafe_create_es, graph = graph, es = E(graph)) @@ -1258,7 +1258,7 @@ get_shortest_paths_dijkstra_impl <- function( mode ) if (igraph_opt("return.vs.es")) { - res$vertices <- lapply(res$vertices, unsafe_create_vs, graph = graph, verts = V(graph)) + res$vertices <- create_vs_list(graph, res$vertices) } if (igraph_opt("return.vs.es")) { res$edges <- lapply(res$edges, unsafe_create_es, graph = graph, es = E(graph)) @@ -1300,7 +1300,7 @@ get_shortest_paths_impl <- function( mode ) if (igraph_opt("return.vs.es")) { - res$vertices <- lapply(res$vertices, unsafe_create_vs, graph = graph, verts = V(graph)) + res$vertices <- create_vs_list(graph, res$vertices) } if (igraph_opt("return.vs.es")) { res$edges <- lapply(res$edges, unsafe_create_es, graph = graph, es = E(graph)) @@ -1453,7 +1453,7 @@ get_widest_paths_impl <- function( mode ) if (igraph_opt("return.vs.es")) { - res$vertices <- lapply(res$vertices, unsafe_create_vs, graph = graph, verts = V(graph)) + res$vertices <- create_vs_list(graph, res$vertices) } if (igraph_opt("return.vs.es")) { res$edges <- lapply(res$edges, unsafe_create_es, graph = graph, es = E(graph)) diff --git a/R/aaa-separators.R b/R/aaa-separators.R index ef97067ea63..5705728e6a8 100644 --- a/R/aaa-separators.R +++ b/R/aaa-separators.R @@ -14,7 +14,7 @@ all_minimal_st_separators_impl <- function( graph ) if (igraph_opt("return.vs.es")) { - res <- lapply(res, unsafe_create_vs, graph = graph, verts = V(graph)) + res <- create_vs_list(graph, res) } res } @@ -86,7 +86,7 @@ minimum_size_separators_impl <- function( graph ) if (igraph_opt("return.vs.es")) { - res <- lapply(res, unsafe_create_vs, graph = graph, verts = V(graph)) + res <- create_vs_list(graph, res) } res } diff --git a/R/aaa-structural.R b/R/aaa-structural.R index de9543e847b..e8adb9454ad 100644 --- a/R/aaa-structural.R +++ b/R/aaa-structural.R @@ -336,7 +336,7 @@ biconnected_components_impl <- function( res$component_edges <- lapply(res$component_edges, unsafe_create_es, graph = graph, es = E(graph)) } if (igraph_opt("return.vs.es")) { - res$components <- lapply(res$components, unsafe_create_vs, graph = graph, verts = V(graph)) + res$components <- create_vs_list(graph, res$components) } if (igraph_opt("return.vs.es")) { res$articulation_points <- create_vs(graph, res$articulation_points) @@ -1290,7 +1290,7 @@ neighborhood_impl <- function( mindist ) if (igraph_opt("return.vs.es")) { - res <- lapply(res, unsafe_create_vs, graph = graph, verts = V(graph)) + res <- create_vs_list(graph, res) } res } diff --git a/R/cliques.R b/R/cliques.R index 29fefbe4a65..35b4fc32c51 100644 --- a/R/cliques.R +++ b/R/cliques.R @@ -358,7 +358,7 @@ max_cliques <- function( res <- lapply(res, function(x) x + 1) if (igraph_opt("return.vs.es")) { - res <- lapply(res, unsafe_create_vs, graph = graph, verts = V(graph)) + res <- create_vs_list(graph, res) } res @@ -749,7 +749,7 @@ ivs <- function(graph, min = NULL, max = NULL) { res <- lapply(res, `+`, 1) if (igraph_opt("return.vs.es")) { - res <- lapply(res, unsafe_create_vs, graph = graph, verts = V(graph)) + res <- create_vs_list(graph, res) } res diff --git a/R/cohesive.blocks.R b/R/cohesive.blocks.R index 18eef59d2ef..5812a07f3b2 100644 --- a/R/cohesive.blocks.R +++ b/R/cohesive.blocks.R @@ -402,12 +402,7 @@ cohesive_blocks <- function( res$labels <- V(graph)$name } if (igraph_opt("return.vs.es")) { - res$blocks <- lapply( - res$blocks, - unsafe_create_vs, - graph = graph, - verts = V(graph) - ) + res$blocks <- create_vs_list(graph, res$blocks) } res$vcount <- vcount(graph) diff --git a/R/components.R b/R/components.R index e1ef087a952..13484c4c8b7 100644 --- a/R/components.R +++ b/R/components.R @@ -386,12 +386,7 @@ biconnected_components <- function(graph) { res$component.edges <- res$component_edges } if (igraph_opt("return.vs.es")) { - res$components <- lapply( - res$components, - unsafe_create_vs, - graph = graph, - verts = V(graph) - ) + res$components <- create_vs_list(graph, res$components) } if (igraph_opt("return.vs.es")) { res$articulation_points <- create_vs(graph, res$articulation_points) diff --git a/R/conversion.R b/R/conversion.R index 61791722a72..9bdfe3bf858 100644 --- a/R/conversion.R +++ b/R/conversion.R @@ -926,7 +926,7 @@ as_adj_list <- function( res <- .Call(Rx_igraph_get_adjlist, graph, mode, loops, multiple) res <- lapply(res, `+`, 1) if (igraph_opt("return.vs.es")) { - res <- lapply(res, unsafe_create_vs, graph = graph, verts = V(graph)) + res <- create_vs_list(graph, res) } if (is_named(graph)) { names(res) <- V(graph)$name diff --git a/R/interface.R b/R/interface.R index 59ce5d4178e..a3422e2283f 100644 --- a/R/interface.R +++ b/R/interface.R @@ -849,7 +849,7 @@ adjacent_vertices <- function( res <- lapply(res, `+`, 1) if (igraph_opt("return.vs.es")) { - res <- lapply(res, unsafe_create_vs, graph = graph, verts = V(graph)) + res <- create_vs_list(graph, res) } if (is_named(graph)) { diff --git a/R/iterators.R b/R/iterators.R index 70781b467ba..a03ce0427ef 100644 --- a/R/iterators.R +++ b/R/iterators.R @@ -330,6 +330,47 @@ unsafe_create_vs <- function(graph, idx, verts = NULL) { res } +# Build a list of vertex sequences from a list of vertex-ID vectors. +# +# This is the batch form of `unsafe_create_vs()` and replaces the +# `lapply(idx_list, unsafe_create_vs, graph = graph, verts = V(graph))` +# pattern. All the per-graph work -- `V(graph)`, the shared weak reference, +# the graph id and the vertex-name source -- is hoisted out of the loop, so +# each sequence costs one `as.integer()`, one name subset and one +# `attributes<-` instead of a closure call that re-reads all of it. +# +# Having a single named entry point for "turn this list of ID vectors into a +# list of vertex sequences" also means the construction loop can be moved +# wholesale (e.g. into C) without touching any of the ~37 call sites. +create_vs_list <- function(graph, idx_list) { + verts <- V(graph) + vs_env <- attr(verts, "env") + vs_graph <- attr(verts, "graph") + vertex_names <- attr(verts, "names") + if (is.null(vertex_names)) { + lapply(idx_list, function(idx) { + res <- as.integer(idx) + attributes(res) <- list( + class = "igraph.vs", + env = vs_env, + graph = vs_graph + ) + res + }) + } else { + lapply(idx_list, function(idx) { + res <- as.integer(idx) + attributes(res) <- list( + names = vertex_names[res], + class = "igraph.vs", + env = vs_env, + graph = vs_graph + ) + res + }) + } +} + # Internal function to quickly convert integer vectors to igraph.es # for use after C code, when NA and bounds checking is unnecessary # Also allows us to construct V(graph) outside the function call in diff --git a/R/paths.R b/R/paths.R index 5ee7971e8ed..64994396f6e 100644 --- a/R/paths.R +++ b/R/paths.R @@ -173,7 +173,7 @@ all_simple_paths <- function( res <- get.all.simple.paths.pp(res) if (igraph_opt("return.vs.es")) { - res <- lapply(res, unsafe_create_vs, graph = graph, verts = V(graph)) + res <- create_vs_list(graph, res) } res } diff --git a/R/structural-properties.R b/R/structural-properties.R index c2970ebc5a9..44785f57350 100644 --- a/R/structural-properties.R +++ b/R/structural-properties.R @@ -1708,12 +1708,7 @@ shortest_paths <- function( if (igraph_opt("return.vs.es")) { if (!is.null(res$vpath)) { - res$vpath <- lapply( - res$vpath, - unsafe_create_vs, - graph = graph, - verts = V(graph) - ) + res$vpath <- create_vs_list(graph, res$vpath) } if (!is.null(res$epath)) { res$epath <- lapply( @@ -1821,12 +1816,7 @@ all_shortest_paths <- function( } if (igraph_opt("return.vs.es")) { - res$vpaths <- lapply( - res$vpaths, - unsafe_create_vs, - graph = graph, - verts = V(graph) - ) + res$vpaths <- create_vs_list(graph, res$vpaths) } # Transitional, eventually, remove $res @@ -2948,7 +2938,7 @@ ego <- function( res <- lapply(res, function(x) x + 1) if (igraph_opt("return.vs.es")) { - res <- lapply(res, unsafe_create_vs, graph = graph, verts = V(graph)) + res <- create_vs_list(graph, res) } res diff --git a/tools/stimulus/types-RR.yaml b/tools/stimulus/types-RR.yaml index bfb5ca69ed8..3c9ac878978 100644 --- a/tools/stimulus/types-RR.yaml +++ b/tools/stimulus/types-RR.yaml @@ -472,7 +472,7 @@ VERTEXSET_LIST: OUTCONV: OUT: |- if (igraph_opt("return.vs.es")) { - %I% <- lapply(%I%, unsafe_create_vs, graph = %I1%, verts = V(%I1%)) + %I% <- create_vs_list(%I1%, %I%) } EDGESET_LIST: