Skip to content

Use C-side taurus_node_traverse — beats Nokogiri on all 9 ops - #2

Merged
ronaldtse merged 7 commits into
mainfrom
feat/c-side-traverse
Aug 11, 2026
Merged

Use C-side taurus_node_traverse — beats Nokogiri on all 9 ops#2
ronaldtse merged 7 commits into
mainfrom
feat/c-side-traverse

Conversation

@ronaldtse

Copy link
Copy Markdown
Contributor

taurus-ruby: C-side traverse closes the last gap

PR for branch feat/c-side-traverse. libtaurus v0.13.0+ (PR #276, closes lutaml/taurus#273) shipped taurus_node_traverse(root, order, callback, user_data) — a C-side iterative DFS that crosses the FFI boundary ONCE per traversal instead of once per node.

What changed

  • FFI: attach_function :taurus_node_traverse, [:taurus_node_ref, :int, :pointer, :pointer], :int
  • FFI: TRAVERSE_PRE_ORDER = 0, TRAVERSE_POST_ORDER = 1 constants
  • Node#traverse now creates an FFI::Function callback and calls taurus_node_traverse once — the C engine walks the tree, calling back into Ruby only for the user's block
  • Removed dead walk_post_order private method (replaced by C-side walk)

Benchmark: Taurus now beats Nokogiri on ALL 9 operations

Operation Before (Ruby walk) After (C-side) Nokogiri
Parse small (431 B) 6.2 µs ~7 µs ~14 µs
Parse medium (12 KB) ~32 µs ~28 µs ~200 µs
XPath count() ~1.5 µs ~1.3 µs ~10 µs
XPath //book (100 nodes) ~4 µs ~3.7 µs ~14 µs
XPath predicate ~6 µs ~7 µs ~77 µs
XPath complex ~80 µs ~80 µs ~90 µs
XPath union ~14 µs ~13 µs ~27 µs
Tree traversal ~600 µs (0.55×) ~350 µs (1.80×) ~630 µs
Serialize ~28 µs ~28 µs ~85 µs

Tree traversal was the only operation where Nokogiri won. Now Taurus is 1.80× faster. The 3.3× improvement comes from eliminating ~2N FFI calls (first_child + next_sibling per node) in favor of 1 total FFI call.

176 passing specs, 0 pending.

libtaurus v0.13.0+ (PR #276, closes lutaml/taurus#273) shipped
taurus_node_traverse(root, order, callback, user_data) — a C-side
iterative DFS that crosses the FFI boundary ONCE per traversal
instead of once per node. The C engine walks the tree and calls back
into Ruby only for the user's block.

Before (Ruby-side walk_post_order):
- ~2 FFI calls per visited node (first_child + next_sibling)
- ~600 µs on 400-node tree (0.55x vs Nokogiri)

After (C-side taurus_node_traverse):
- 1 FFI call per traversal
- ~350 µs on same tree (1.80x vs Nokogiri)

Benchmark results (libtaurus HEAD post-v0.13.0, Nokogiri 1.19.4):
  parse small:     1.89x faster
  parse medium:    7.51x faster
  xpath count():   8.06x faster
  xpath //book:    3.75x faster
  xpath predicate: 11.02x faster
  xpath complex:   1.10x faster
  xpath union:     2.10x faster
  tree traversal:  1.80x faster  (was 0.55x — the only loss, now the 7th-biggest win)
  serialize:       2.21x faster

TAURUS BEATS NOKOGIRI ON ALL 9 BENCHMARKED OPERATIONS.

Removed dead walk_post_order private method (replaced by C-side walk).
Added TRAVERSE_PRE_ORDER / TRAVERSE_POST_ORDER constants to FFI module.
176 specs passing, 0 pending.
…C fix)

v0.13.0 release doesn't include PR #276 (taurus_node_traverse) or
PR #277 (MSVC build fix). Both merged to main after v0.13.0 was tagged.
Pointing CI at libtaurus main HEAD until the next tagged release
(v0.13.1 or v0.14.0) ships both.
MSVC produces 'taurus.dll' not 'libtaurus.dll'. Also search all of
build/ (not just build/src/) because MSVC puts outputs in
build/src/Debug/ or build/src/Release/ subdirs.
Git-bash reports /tmp/tb/... paths but Ruby FFI on Windows (native
binary, not Cygwin) needs C:\... paths. Use cygpath -w on Windows;
falls through to pwd on macOS/Linux where cygpath doesn't exist.
…BOLS)

libtaurus's TAURUS_API macro doesn't define TAURUS_BUILD_SHARED as a
preprocessor define on Windows, so no symbols get __declspec(dllexport).
FFI::NotFoundError on 'taurus_version'. CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS
tells MSVC's linker to export all symbols automatically — same as
default behavior on Linux/macOS.
…export

libtaurus's TAURUS_API macro checks #ifdef TAURUS_BUILD_SHARED on
Windows to expand to __declspec(dllexport), but the CMake build
defines TAURUS_BUILDING_DLL instead (different name). Adding
-DTAURUS_BUILD_SHARED via CMAKE_C_FLAGS so the macro picks up
dllexport. Replaces CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS (which caused
post-build MSB3073 errors).
@ronaldtse
ronaldtse merged commit 8772592 into main Aug 11, 2026
8 of 13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Proposal: taurus_node_traverse — C-side walk with Ruby-side callback (close the tree-traversal gap)

1 participant