Skip to content

Commit 3e7e7b3

Browse files
authored
Switch Rust global allocator to tcmalloc. (#5344)
# Description Switch Rust global allocator to tcmalloc. tcmalloc yielded performance improvements for C++, and it's also significantly better than the system allocator on the apply time benchmarks (~30-50ms of apply time savings on large TPL benchmarks, like 6000 SAC TPS). Rust implementation relies on the tcmalloc functions already linked into the Core binary. That's why we're using a custom allocator implementation instead of using an existing tcmalloc wrapper for Rust. # Checklist - [ ] Reviewed the [contributing](https://github.com/stellar/stellar-core/blob/master/CONTRIBUTING.md#submitting-changes) document - [ ] Rebased on top of master (no merge commits) - [ ] Ran `clang-format` v8.0.0 (via `make format` or the Visual Studio extension) - [ ] Compiles - [ ] Ran all tests - [ ] If change impacts performance, include supporting evidence per the [performance document](https://github.com/stellar/stellar-core/blob/master/performance-eval/performance-eval.md)
2 parents d0a4a75 + 8bb75c6 commit 3e7e7b3

4 files changed

Lines changed: 113 additions & 1 deletion

File tree

src/Makefile.am

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,16 @@ else
5353
CARGO_FEATURE_TRACY =
5454
endif
5555

56+
# Enable the custom tcmalloc global allocator in the Rust crate whenever the C++
57+
# build links tcmalloc (see configure.ac). The global allocator lives only in
58+
# the top-level stellar-core crate, so this is intentionally not passed to the
59+
# soroban sub-crate builds below.
60+
if USE_TCMALLOC
61+
CARGO_FEATURE_TCMALLOC = --features tcmalloc
62+
else
63+
CARGO_FEATURE_TCMALLOC =
64+
endif
65+
5666
# Generated sources that are built separately (not in SRC_CXX_FILES)
5767
# These are reused by both stellar-core and fuzz targets.
5868
GENERATED_VERSION_SOURCES = main/StellarCoreVersion.cpp main/XDRFilesSha256.cpp
@@ -345,7 +355,8 @@ $(LIBRUST_STELLAR_CORE): $(RUST_HOST_DEPFILES) $(SRC_RUST_FILES) Makefile $(RUST
345355
$(RUST_PROFILE_ARG) \
346356
--locked \
347357
--target-dir $(abspath $(RUST_TARGET_DIR)) \
348-
$(CARGO_FEATURE_FASTDEV) $(CARGO_FEATURE_TRACY) $(CARGO_FEATURE_NEXT) $(CARGO_FEATURE_TESTUTILS)
358+
$(CARGO_FEATURE_FASTDEV) $(CARGO_FEATURE_TRACY) $(CARGO_FEATURE_NEXT) $(CARGO_FEATURE_TESTUTILS) \
359+
$(CARGO_FEATURE_TCMALLOC)
349360

350361
else # !ENABLE_FASTDEV_UNSAFE_FOR_PRODUCTION
351362

@@ -456,6 +467,7 @@ $(LIBRUST_STELLAR_CORE): $(RUST_HOST_DEPFILES) $(SRC_RUST_FILES) $(ALL_SOROBAN_L
456467
--locked \
457468
--target-dir $(abspath $(RUST_TARGET_DIR)) \
458469
$(CARGO_FEATURE_TRACY) $(CARGO_FEATURE_NEXT) $(CARGO_FEATURE_TESTUTILS) \
470+
$(CARGO_FEATURE_TCMALLOC) \
459471
-- \
460472
$(ALL_SOROBAN_EXTERN_ARGS) \
461473
$(ALL_SOROBAN_DEPEND_ARGS)

src/rust/Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,12 @@ fastdev = ["dep:soroban-env-host-p27",
209209

210210
tracy = ["dep:tracy-client"]
211211

212+
# Use the C++ tcmalloc (gperftools) allocator that is linked into stellar-core
213+
# as the Rust global allocator (see src/tcmalloc.rs). This is enabled by the
214+
# build system (src/Makefile.am) whenever the C++ build uses tcmalloc
215+
# (USE_TCMALLOC). Builds without it use Rust's default allocator.
216+
tcmalloc = []
217+
212218
# The "next" feature enables not-yet-released features that (a) break protocol
213219
# but (b) are still under development, in between protocol releases. This
214220
# feature is not enabled by default, and is only intended for temporary use when

src/rust/src/lib.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,17 @@ mod soroban_test_extra_protocol;
4343

4444
mod soroban_fuzz;
4545

46+
// When built against a C++ tcmalloc, route all Rust allocations (including the
47+
// linked-in soroban hosts) through it via a custom global allocator. See
48+
// tcmalloc.rs for details. Gated on the `tcmalloc` feature so tcmalloc-less
49+
// builds keep using Rust's default allocator.
50+
#[cfg(feature = "tcmalloc")]
51+
mod tcmalloc;
52+
53+
#[cfg(feature = "tcmalloc")]
54+
#[global_allocator]
55+
static GLOBAL_ALLOCATOR: tcmalloc::TcMalloc = tcmalloc::TcMalloc;
56+
4657
use soroban_module_cache::SorobanModuleCache;
4758

4859
mod bridge;

src/rust/src/tcmalloc.rs

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// Copyright 2026 Stellar Development Foundation and contributors. Licensed
2+
// under the Apache License, Version 2.0. See the COPYING file at the root
3+
// of this distribution or at http://www.apache.org/licenses/LICENSE-2.0
4+
5+
//! Rust global allocator backed by the C++ tcmalloc (gperftools) allocator that
6+
//! is linked into stellar-core. This is only compiled with the `tcmalloc`
7+
//! feature, which the build system enables whenever the C++ build uses tcmalloc
8+
//! (see `USE_TCMALLOC` in configure.ac / src/Makefile.am). Builds without
9+
//! tcmalloc (sanitizer or non-Linux builds) simply use Rust's default
10+
//! allocator.
11+
//!
12+
//! The `tc_*` symbols are provided by `libtcmalloc_minimal.a` and are left
13+
//! undefined in `librust_stellar_core.a`; they are resolved at the final C++
14+
//! link step, exactly like the `shim_*` C++ functions imported by the bridge.
15+
//! No `#[link]` attribute or build script is required.
16+
17+
use core::ffi::c_void;
18+
use std::alloc::{GlobalAlloc, Layout};
19+
20+
extern "C" {
21+
fn tc_malloc(size: usize) -> *mut c_void;
22+
fn tc_free(ptr: *mut c_void);
23+
fn tc_realloc(ptr: *mut c_void, size: usize) -> *mut c_void;
24+
fn tc_calloc(nmemb: usize, size: usize) -> *mut c_void;
25+
fn tc_memalign(alignment: usize, size: usize) -> *mut c_void;
26+
}
27+
28+
// tcmalloc, like the system malloc, guarantees alignment suitable for any
29+
// fundamental type (alignof(max_align_t)): 16 bytes on 64-bit, 8 on 32-bit.
30+
// Requests with a larger alignment need to go through tc_memalign.
31+
#[cfg(target_pointer_width = "64")]
32+
const MIN_ALIGN: usize = 16;
33+
#[cfg(target_pointer_width = "32")]
34+
const MIN_ALIGN: usize = 8;
35+
36+
pub struct TcMalloc;
37+
38+
// This implementation mirrors the standard library's unix allocator (
39+
// https://github.com/rust-lang/rust/blob/225e91c03da22cd4b9792b83c1cfc97967101614/library/std/src/sys/alloc/unix.rs).
40+
// Normally aligned allocations use tc_malloc/tc_calloc/tc_realloc, over-aligned
41+
// ones use tc_memalign, and tc_free deallocates memory from any of these.
42+
unsafe impl GlobalAlloc for TcMalloc {
43+
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
44+
if layout.align() <= MIN_ALIGN && layout.align() <= layout.size() {
45+
tc_malloc(layout.size()) as *mut u8
46+
} else {
47+
tc_memalign(layout.align(), layout.size()) as *mut u8
48+
}
49+
}
50+
51+
unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
52+
if layout.align() <= MIN_ALIGN && layout.align() <= layout.size() {
53+
tc_calloc(1, layout.size()) as *mut u8
54+
} else {
55+
let ptr = self.alloc(layout);
56+
if !ptr.is_null() {
57+
core::ptr::write_bytes(ptr, 0, layout.size());
58+
}
59+
ptr
60+
}
61+
}
62+
63+
unsafe fn dealloc(&self, ptr: *mut u8, _layout: Layout) {
64+
tc_free(ptr as *mut c_void);
65+
}
66+
67+
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
68+
if layout.align() <= MIN_ALIGN && layout.align() <= new_size {
69+
tc_realloc(ptr as *mut c_void, new_size) as *mut u8
70+
} else {
71+
// tc_realloc cannot preserve an over-aligned allocation's alignment,
72+
// so allocate fresh, copy, and free the old block by hand.
73+
let new_layout = Layout::from_size_align_unchecked(new_size, layout.align());
74+
let new_ptr = self.alloc(new_layout);
75+
if !new_ptr.is_null() {
76+
let copy = core::cmp::min(layout.size(), new_size);
77+
core::ptr::copy_nonoverlapping(ptr, new_ptr, copy);
78+
self.dealloc(ptr, layout);
79+
}
80+
new_ptr
81+
}
82+
}
83+
}

0 commit comments

Comments
 (0)