Skip to content

perf: Build the whole vertex-sequence list in one C pass - #2888

Open
schochastics wants to merge 1 commit into
perf/vs-create-vs-listfrom
perf/vs-list-c
Open

perf: Build the whole vertex-sequence list in one C pass#2888
schochastics wants to merge 1 commit into
perf/vs-create-vs-listfrom
perf/vs-list-c

Conversation

@schochastics

Copy link
Copy Markdown
Contributor

Split 3 of 4 out of #2696, per #2696 (comment) — this is the requested "C implementation of create_vs_list() without ALTREP" step.

Base: #2887 (review #2886 and #2887 first; this PR's diff is only the top commit). Last in the stack: #2696.

What changed

After #2887, create_vs_list() still drove the per-element work from R — an lapply closure call, as.integer(), a name subset and attributes<-, once per sequence. That is the dominant remaining cost when a function returns thousands of igraph.vs objects.

Rx_igraph_vs_list() in rinterface_extra.c does the whole loop in one pass. R only builds V(graph) once (that is what mints the shared weak reference and graph id) and hands the pieces to C:

create_vs_list <- function(graph, idx_list) {
  verts <- V(graph)
  .Call(
    Rx_igraph_vs_list,
    idx_list,
    if (is_named(graph)) vertex_attr(graph)$name else NULL,
    attr(verts, "env"),
    attr(verts, "graph")
  )
}

Each element gets a fresh integer payload, the corresponding names when the graph is named, the shared env/graph attributes, and the igraph.vs class. Names are built eagerly here — no ALTREP, as requested. That is the only thing #2696 changes on top.

Note the payload is duplicated when Rf_coerceVector() is a no-op, so a caller-owned integer vector is never mutated in place.

max_cliques(sample_gnp(200, 0.16)) on a named graph: 5.6 ms -> 2.65 ms.

Where that leaves the stack

median vs main
main 24.7 ms
#2886 (shared weakref) 7.8 ms 3.2×
#2887 (create_vs_list()) 5.6 ms 4.4×
this PR (C loop) 2.65 ms 9.3×

So the three merged PRs get essentially the whole win without any ALTREP. This is the point at which #2696's "if needed" can be judged on its own merits.

Correctness

Verified against the return.vs.es = FALSE numeric path:

  • integer payloads with identical values and names;
  • no names attribute at all on unnamed graphs;
  • NA and out-of-range IDs map to NA_STRING;
  • double ID vectors coerced correctly;
  • caller's input vectors unmutated;
  • the shared weak reference still releases the graph: get_vs_graph(seq) is NULL after rm(graph); gc().

Compiles without new warnings, and clean under gctorture(TRUE).

Full suite: FAIL 0 | WARN 0 | SKIP 7 | PASS 9299.

Also here

cpp11::cpp_register() added only the Rx_igraph_vs_list registration in src/cpp11.cpp. Two touchstone benchmarks for the batch path: ego_order2_named and all_simple_paths_named.

create_vs_list() drove the per-element work (lapply closure, as.integer,
name subset, attributes<-) from R, repeated once per sequence -- the
dominant remaining cost when functions like max_cliques() return
thousands of igraph.vs objects.

Move that loop into Rx_igraph_vs_list() in rinterface_extra.c. R now only
builds V(graph) once (to mint the shared weak reference and graph id) and
hands the pieces to C. Each element gets a fresh integer payload (guarded
against coerceVector aliasing so the caller's vectors are never mutated),
the corresponding names when the graph is named, the shared env/graph
attributes, and the igraph.vs class.

max_cliques(sample_gnp(200, 0.16)) on a named graph: ~5.6ms -> ~2.65ms,
and ~24.7ms -> ~2.65ms over the three PRs so far.

Correctness verified: integer payloads with identical values/names, no
names attribute on unnamed graphs, NA/out-of-range IDs map to NA_STRING,
double inputs coerced, inputs unmutated, and the shared weakref still
releases the graph after rm()+gc(). Full suite passes; clean under
gctorture(TRUE).

Also adds two touchstone benchmarks (ego_order2_named,
all_simple_paths_named) covering the batch-construction path.

cpp11::cpp_register() added only the Rx_igraph_vs_list registration.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

This is how benchmark results would change (along with a 95% confidence interval in relative change) if dd0e928 is merged into perf/vs-create-vs-list:

  • ✔️E_named: 223ms -> 222ms [-2.11%, +1.26%]
  • ✔️V_named: 504ms -> 509ms [-1.39%, +3.23%]
  • 🚀all_simple_paths_named: 208ms -> 133ms [-36.53%, -35.9%]
  • ✔️as_adjacency_matrix: 244ms -> 245ms [-0.38%, +1.2%]
  • ✔️as_biadjacency_matrix: 261ms -> 262ms [-0.42%, +0.92%]
  • ✔️as_data_frame_both: 251ms -> 252ms [-0.57%, +0.65%]
  • ✔️as_long_data_frame: 194ms -> 194ms [-0.51%, +0.54%]
  • 🚀ego_order2_named: 221ms -> 146ms [-34.33%, -33.68%]
  • 🚀es_attr_filter: 234ms -> 233ms [-1.29%, -0.07%]
  • ✔️graph_from_adjacency_matrix: 280ms -> 280ms [-0.73%, +0.47%]
  • ✔️graph_from_data_frame: 262ms -> 262ms [-0.69%, +0.62%]
  • 🚀head_of_named: 174ms -> 172ms [-1.73%, -0.82%]
  • 🚀max_cliques_named: 54.8ms -> 25.2ms [-54.37%, -53.69%]
  • ✔️vs_attr_filter: 301ms -> 301ms [-0.61%, +0.59%]
  • ✔️vs_by_name: 254ms -> 254ms [-0.26%, +0.71%]
    Further explanation regarding interpretation and methodology can be found in the documentation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant