Skip to content

Commit 9e3fe5d

Browse files
authored
rust: add secondary function with preallocated internal vecs (#8936)
* rust: add secondary function with preallocated internal vecs * docs: document pre allocation feature for rust implementation
1 parent dc92173 commit 9e3fe5d

2 files changed

Lines changed: 198 additions & 1 deletion

File tree

docs/source/languages/rust.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,47 @@ And example of usage, for the time being, can be found in
205205
- Safe getters in [SafeBuffer](https://docs.rs/flatbuffers-reflection/latest/flatbuffers_reflection/struct.SafeBuffer.html),
206206
which does verification when constructed so you can use it for any data source
207207

208+
## Buffer pre allocation in a latency-sensitive context
209+
210+
In latency-sensitive applications, dynamic memory allocations can introduce unpredictable latency spikes. The `FlatBufferBuilder` internally uses several `Vec`s that may reallocate during serialization:
211+
212+
- The backing buffer for the FlatBuffer data
213+
- `field_locs` for tracking field locations within tables
214+
- `written_vtable_revpos` for deduplicating vtables
215+
- `strings_pool` for shared string interning
216+
217+
To avoid allocations during serialization, you can preallocate all internal vectors upfront using the `with_internal_capacity` constructor:
218+
219+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~{.rs}
220+
// Preallocate: 1KB buffer, 8 field locations, 16 vtables, 32 shared strings
221+
let mut builder = FlatBufferBuilder::with_internal_capacity(1024, 8, 16, 32);
222+
223+
// All subsequent operations will not allocate (if capacities are sufficient)
224+
let name = builder.create_shared_string("MyMonster");
225+
// ... build your FlatBuffer ...
226+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
227+
228+
There are three variants available:
229+
230+
- `with_internal_capacity(size, field_locs, vtables, strings)` - Creates a new builder with all capacities preallocated
231+
- `from_vec_with_internal_capacity(buffer, field_locs, vtables, strings)` - Reuses an existing `Vec<u8>` as the backing buffer
232+
- `new_in_with_internal_capacity(allocator, field_locs, vtables, strings)` - Uses a custom `Allocator` with preallocated internal vecs
233+
234+
When combined with `reset()`, you can reuse the same builder across multiple serializations without any allocations after the initial setup:
235+
236+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~{.rs}
237+
let mut builder = FlatBufferBuilder::with_internal_capacity(1024, 8, 16, 32);
238+
239+
loop {
240+
// Build a FlatBuffer (allocation-free if capacities are sufficient)
241+
let data = build_message(&mut builder);
242+
send(data);
243+
244+
// Reset for reuse - clears state but retains allocated capacity
245+
builder.reset();
246+
}
247+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
248+
208249
## Useful tools created by others
209250

210251
* [flatc-rust](https://github.com/frol/flatc-rust) - FlatBuffers compiler

rust/flatbuffers/src/builder.rs

Lines changed: 157 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,60 @@ impl<'fbb> FlatBufferBuilder<'fbb, DefaultAllocator> {
166166
pub fn with_capacity(size: usize) -> Self {
167167
Self::from_vec(vec![0; size])
168168
}
169+
/// Create a FlatBufferBuilder that is ready for writing, with a
170+
/// ready-to-use capacity of the provided size and preallocated internal vecs.
171+
///
172+
/// The maximum valid value for `size` is `FLATBUFFERS_MAX_BUFFER_SIZE`.
173+
///
174+
/// # Arguments
175+
///
176+
/// * `size` - The initial capacity of the backing buffer in bytes.
177+
/// * `field_locs_capacity` - Preallocated capacity for the field locations vec.
178+
/// * `written_vtable_revpos_capacity` - Preallocated capacity for the written vtable reverse positions vec.
179+
/// * `strings_pool_capacity` - Preallocated capacity for the shared strings pool vec.
180+
pub fn with_internal_capacity(
181+
size: usize,
182+
field_locs_capacity: usize,
183+
written_vtable_revpos_capacity: usize,
184+
strings_pool_capacity: usize,
185+
) -> Self {
186+
Self::from_vec_with_internal_capacity(
187+
vec![0; size],
188+
field_locs_capacity,
189+
written_vtable_revpos_capacity,
190+
strings_pool_capacity,
191+
)
192+
}
193+
/// Create a FlatBufferBuilder that is ready for writing, reusing
194+
/// an existing vector and preallocated internal vecs.
195+
///
196+
/// # Arguments
197+
///
198+
/// * `buffer` - An existing `Vec<u8>` to reuse as the backing buffer.
199+
/// * `field_locs_capacity` - Preallocated capacity for the field locations vec.
200+
/// * `written_vtable_revpos_capacity` - Preallocated capacity for the written vtable reverse positions vec.
201+
/// * `strings_pool_capacity` - Preallocated capacity for the shared strings pool vec.
202+
pub fn from_vec_with_internal_capacity(
203+
buffer: Vec<u8>,
204+
field_locs_capacity: usize,
205+
written_vtable_revpos_capacity: usize,
206+
strings_pool_capacity: usize,
207+
) -> Self {
208+
// we need to check the size here because we create the backing buffer
209+
// directly, bypassing the typical way of using grow_allocator:
210+
assert!(
211+
buffer.len() <= FLATBUFFERS_MAX_BUFFER_SIZE,
212+
"cannot initialize buffer bigger than 2 gigabytes"
213+
);
214+
let allocator = DefaultAllocator::from_vec(buffer);
215+
Self::new_in_with_internal_capacity(
216+
allocator,
217+
field_locs_capacity,
218+
written_vtable_revpos_capacity,
219+
strings_pool_capacity,
220+
)
221+
}
222+
169223
/// Create a FlatBufferBuilder that is ready for writing, reusing
170224
/// an existing vector.
171225
pub fn from_vec(buffer: Vec<u8>) -> Self {
@@ -212,6 +266,40 @@ impl<'fbb, A: Allocator> FlatBufferBuilder<'fbb, A> {
212266
}
213267
}
214268

269+
/// Create a [`FlatBufferBuilder`] that is ready for writing with a custom [`Allocator`]
270+
/// and preallocated internal vecs.
271+
///
272+
/// # Arguments
273+
///
274+
/// * `allocator` - A custom [`Allocator`] to use as the backing buffer.
275+
/// * `field_locs_capacity` - Preallocated capacity for the field locations vec.
276+
/// * `written_vtable_revpos_capacity` - Preallocated capacity for the written vtable reverse positions vec.
277+
/// * `strings_pool_capacity` - Preallocated capacity for the shared strings pool vec.
278+
pub fn new_in_with_internal_capacity(
279+
allocator: A,
280+
field_locs_capacity: usize,
281+
written_vtable_revpos_capacity: usize,
282+
strings_pool_capacity: usize,
283+
) -> Self {
284+
let head = ReverseIndex::end();
285+
FlatBufferBuilder {
286+
allocator,
287+
head,
288+
289+
field_locs: Vec::with_capacity(field_locs_capacity),
290+
written_vtable_revpos: Vec::with_capacity(written_vtable_revpos_capacity),
291+
292+
nested: false,
293+
finished: false,
294+
295+
min_align: 0,
296+
force_defaults: false,
297+
strings_pool: Vec::with_capacity(strings_pool_capacity),
298+
299+
_phantom: PhantomData,
300+
}
301+
}
302+
215303
/// Destroy the [`FlatBufferBuilder`], returning its [`Allocator`] and the index
216304
/// into it that represents the start of valid data.
217305
pub fn collapse_in(self) -> (A, usize) {
@@ -232,7 +320,9 @@ impl<'fbb, A: Allocator> FlatBufferBuilder<'fbb, A> {
232320
/// new object.
233321
pub fn reset(&mut self) {
234322
// memset only the part of the buffer that could be dirty:
235-
self.allocator[self.head.range_to_end()].iter_mut().for_each(|x| *x = 0);
323+
self.allocator[self.head.range_to_end()]
324+
.iter_mut()
325+
.for_each(|x| *x = 0);
236326

237327
self.head = ReverseIndex::end();
238328
self.written_vtable_revpos.clear();
@@ -961,6 +1051,33 @@ impl<T> IndexMut<ReverseIndexRange> for [T] {
9611051
#[cfg(test)]
9621052
mod tests {
9631053
use super::*;
1054+
use core::sync::atomic::{AtomicUsize, Ordering};
1055+
use std::alloc::{GlobalAlloc, Layout, System};
1056+
1057+
static ALLOC_COUNT: AtomicUsize = AtomicUsize::new(0);
1058+
1059+
struct CountingAllocator;
1060+
1061+
unsafe impl GlobalAlloc for CountingAllocator {
1062+
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
1063+
ALLOC_COUNT.fetch_add(1, Ordering::Relaxed);
1064+
unsafe { System.alloc(layout) }
1065+
}
1066+
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
1067+
unsafe { System.dealloc(ptr, layout) }
1068+
}
1069+
}
1070+
1071+
#[global_allocator]
1072+
static GLOBAL: CountingAllocator = CountingAllocator;
1073+
1074+
fn reset_alloc_count() {
1075+
ALLOC_COUNT.store(0, Ordering::Relaxed);
1076+
}
1077+
1078+
fn alloc_count() -> usize {
1079+
ALLOC_COUNT.load(Ordering::Relaxed)
1080+
}
9641081

9651082
#[test]
9661083
fn reverse_index_test() {
@@ -970,4 +1087,43 @@ mod tests {
9701087
assert_eq!(&buf[idx.range_to(idx + 1)], &[4]);
9711088
assert_eq!(idx.to_forward_index(&buf), 4);
9721089
}
1090+
1091+
#[test]
1092+
fn with_internal_capacity_preallocates_vecs() {
1093+
let mut builder = FlatBufferBuilder::with_internal_capacity(64, 8, 16, 32);
1094+
1095+
assert!(builder.allocator.len() >= 64);
1096+
assert!(builder.field_locs.capacity() >= 8);
1097+
assert!(builder.written_vtable_revpos.capacity() >= 16);
1098+
assert!(builder.strings_pool.capacity() >= 32);
1099+
1100+
assert!(builder.field_locs.is_empty());
1101+
assert!(builder.written_vtable_revpos.is_empty());
1102+
assert!(builder.strings_pool.is_empty());
1103+
1104+
// Reset the allocation counter after builder construction
1105+
reset_alloc_count();
1106+
1107+
// Add a shared string and verify it lands in the pool
1108+
let s1 = builder.create_shared_string("hello");
1109+
assert_eq!(builder.strings_pool.len(), 1);
1110+
1111+
// Adding the same string again should reuse the pooled entry
1112+
let s2 = builder.create_shared_string("hello");
1113+
assert_eq!(builder.strings_pool.len(), 1);
1114+
assert_eq!(s1.value(), s2.value());
1115+
1116+
// A different string should add a new entry
1117+
let _s3 = builder.create_shared_string("world");
1118+
assert_eq!(builder.strings_pool.len(), 2);
1119+
1120+
// With sufficient preallocated capacity, no additional allocations
1121+
// should have occurred for the internal vecs during the operations above
1122+
let allocs = alloc_count();
1123+
assert_eq!(
1124+
allocs, 0,
1125+
"expected 0 allocations after builder construction, got {}",
1126+
allocs
1127+
);
1128+
}
9731129
}

0 commit comments

Comments
 (0)