mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-19 19:16:17 +00:00
956bd19417
* perf(ecstore): reuse erasure-decode shard buffers across stripes ParallelReader::read allocated and zero-filled a fresh `vec![0u8; shard_size]` per shard on every erasure stripe. Erasure::decode builds one ParallelReader and loops stripes, so those buffers can be reused: BitrotReader::read overwrites buf[..n] and the caller truncates to n, so leftover bytes from the prior stripe are never observed. The decode loop now hands each stripe's shard buffers back to the reader, which reuses them (resized to shard_size) on the next stripe. The heal path, which consumes its buffers into Bytes, never hands them back and is unchanged. This path runs on every object GET. Streaming-decode benchmark (Erasure::decode end-to-end, median): 4+2 16MiB 1MiB-block: -9.6% verify-on, -23.8% verify-off 6+3 24MiB 1MiB-block: -9.7% verify-on, -22.5% verify-off 4+2 4MiB 64KiB-block: -5.5% verify-on, -16.3% verify-off Adds a streaming-decode benchmark covering the ParallelReader path and a regression test that decodes a multi-stripe object with missing data shards (so reconstructed buffers are recycled) with bitrot verification both on and off, asserting byte-exact output. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * perf(ecstore): tighten decode buffer reuse and bench harness Address PR review feedback on the erasure-decode buffer-reuse change: - Only claim a recycled shard buffer inside the `Some(reader)` branch of `ParallelReader::read`, so the take is co-located with the read that uses it and missing shards no longer touch the recycle slot. - Move per-shard `BitrotReader` construction into Criterion `iter_batched` setup so reader/UUID construction stays out of the timed decode path, and assert decode success plus full output length each iteration. Re-measured with the cleaner harness (median, before -> after, all p < 0.05): verify-on -5.3%..-11.7%, verify-off -15.8%..-25.3%. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * perf(ecstore): drop dead recycle slots and tidy decode bench Address the second PR review pass: - In `Erasure::decode`, clear recycled slots for missing-shard (None-reader) indices before handing buffers back. `ParallelReader::read` only reuses slots with an active reader, and `decode_data` always re-allocates the reconstructed shard, so retaining those buffers only held memory that could never be reused in degraded reads. - Move the output sink into the benchmark's `iter_batched` setup and validate decode success/output length once per config instead of inside the timed loop, so neither sink construction nor assertions touch the measurement. Re-measured with the final harness (median, before -> after, all p < 0.05): verify-on -5.2%..-9.8%, verify-off -19.4%..-25.8%. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
447 lines
17 KiB
Rust
447 lines
17 KiB
Rust
// Copyright 2024 RustFS Team
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
//! Reed-Solomon SIMD erasure coding performance benchmarks.
|
|
//!
|
|
//! This benchmark tests the performance of the high-performance SIMD Reed-Solomon implementation.
|
|
//!
|
|
//! ## Running Benchmarks
|
|
//!
|
|
//! ```bash
|
|
//! # Run all benchmarks
|
|
//! cargo bench
|
|
//!
|
|
//! # Run specific benchmark
|
|
//! cargo bench --bench erasure_benchmark
|
|
//!
|
|
//! # Generate HTML report
|
|
//! cargo bench --bench erasure_benchmark -- --output-format html
|
|
//!
|
|
//! # Test encoding performance only
|
|
//! cargo bench encode
|
|
//!
|
|
//! # Test decoding performance only
|
|
//! cargo bench decode
|
|
//! ```
|
|
//!
|
|
//! ## Test Configurations
|
|
//!
|
|
//! The benchmarks test various scenarios:
|
|
//! - Different data sizes: 1KB, 64KB, 1MB, 16MB
|
|
//! - Different erasure coding configurations: (4,2), (6,3), (8,4)
|
|
//! - Both encoding and decoding operations
|
|
//! - SIMD optimization for different shard sizes
|
|
|
|
use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
|
|
use rustfs_ecstore::erasure_coding::{BitrotReader, BitrotWriter, Erasure, calc_shard_size};
|
|
use rustfs_utils::HashAlgorithm;
|
|
use std::hint::black_box;
|
|
use std::io::Cursor;
|
|
use std::time::Duration;
|
|
use tokio::runtime::Runtime;
|
|
|
|
/// Benchmark configuration structure
|
|
#[derive(Clone, Debug)]
|
|
struct BenchConfig {
|
|
/// Number of data shards
|
|
data_shards: usize,
|
|
/// Number of parity shards
|
|
parity_shards: usize,
|
|
/// Test data size (bytes)
|
|
data_size: usize,
|
|
/// Block size (bytes)
|
|
block_size: usize,
|
|
/// Configuration name
|
|
name: String,
|
|
}
|
|
|
|
impl BenchConfig {
|
|
fn new(data_shards: usize, parity_shards: usize, data_size: usize, block_size: usize) -> Self {
|
|
Self {
|
|
data_shards,
|
|
parity_shards,
|
|
data_size,
|
|
block_size,
|
|
name: format!("{}+{}_{}KB_{}KB-block", data_shards, parity_shards, data_size / 1024, block_size / 1024),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Generate test data
|
|
fn generate_test_data(size: usize) -> Vec<u8> {
|
|
(0..size).map(|i| (i % 256) as u8).collect()
|
|
}
|
|
|
|
/// Benchmark: Encoding performance
|
|
fn bench_encode_performance(c: &mut Criterion) {
|
|
let configs = vec![
|
|
// Small data tests - 1KB
|
|
BenchConfig::new(4, 2, 1024, 1024),
|
|
BenchConfig::new(6, 3, 1024, 1024),
|
|
BenchConfig::new(8, 4, 1024, 1024),
|
|
// Medium data tests - 64KB
|
|
BenchConfig::new(4, 2, 64 * 1024, 64 * 1024),
|
|
BenchConfig::new(6, 3, 64 * 1024, 64 * 1024),
|
|
BenchConfig::new(8, 4, 64 * 1024, 64 * 1024),
|
|
// Large data tests - 1MB
|
|
BenchConfig::new(4, 2, 1024 * 1024, 1024 * 1024),
|
|
BenchConfig::new(6, 3, 1024 * 1024, 1024 * 1024),
|
|
BenchConfig::new(8, 4, 1024 * 1024, 1024 * 1024),
|
|
// Extra large data tests - 16MB
|
|
BenchConfig::new(4, 2, 16 * 1024 * 1024, 16 * 1024 * 1024),
|
|
BenchConfig::new(6, 3, 16 * 1024 * 1024, 16 * 1024 * 1024),
|
|
];
|
|
|
|
for config in configs {
|
|
let data = generate_test_data(config.data_size);
|
|
|
|
// Test SIMD encoding performance
|
|
let mut group = c.benchmark_group("encode_simd");
|
|
group.throughput(Throughput::Bytes(config.data_size as u64));
|
|
group.sample_size(10);
|
|
group.measurement_time(Duration::from_secs(5));
|
|
|
|
group.bench_with_input(BenchmarkId::new("simd_impl", &config.name), &(&data, &config), |b, (data, config)| {
|
|
let erasure = Erasure::new(config.data_shards, config.parity_shards, config.block_size);
|
|
b.iter(|| {
|
|
let shards = erasure.encode_data(black_box(data)).unwrap();
|
|
black_box(shards);
|
|
});
|
|
});
|
|
group.finish();
|
|
|
|
// Test direct reed-solomon-erasure implementation for large shards (>= 512 bytes)
|
|
let shard_size = calc_shard_size(config.data_size, config.data_shards);
|
|
if shard_size >= 512 && config.parity_shards > 0 {
|
|
use reed_solomon_erasure::galois_8::ReedSolomon;
|
|
|
|
let mut rse_group = c.benchmark_group("encode_rse_direct");
|
|
rse_group.throughput(Throughput::Bytes(config.data_size as u64));
|
|
rse_group.sample_size(10);
|
|
rse_group.measurement_time(Duration::from_secs(5));
|
|
|
|
if let Ok(rs) = ReedSolomon::new(config.data_shards, config.parity_shards) {
|
|
let total_shards = config.data_shards + config.parity_shards;
|
|
let per_shard_size = calc_shard_size(config.data_size, config.data_shards);
|
|
let need_total = per_shard_size * total_shards;
|
|
|
|
rse_group.bench_with_input(
|
|
BenchmarkId::new("rse_direct", &config.name),
|
|
&(&data, need_total, per_shard_size),
|
|
|b, (data, need_total, per_shard_size)| {
|
|
b.iter(|| {
|
|
let mut buffer = vec![0u8; *need_total];
|
|
let copy_len = data.len().min(buffer.len());
|
|
buffer[..copy_len].copy_from_slice(&data[..copy_len]);
|
|
|
|
let mut slices: Vec<&mut [u8]> = buffer.chunks_exact_mut(*per_shard_size).collect();
|
|
rs.encode(&mut slices).unwrap();
|
|
black_box(buffer);
|
|
});
|
|
},
|
|
);
|
|
}
|
|
rse_group.finish();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Benchmark: Decoding performance
|
|
fn bench_decode_performance(c: &mut Criterion) {
|
|
let configs = vec![
|
|
// Medium data tests - 64KB
|
|
BenchConfig::new(4, 2, 64 * 1024, 64 * 1024),
|
|
BenchConfig::new(6, 3, 64 * 1024, 64 * 1024),
|
|
// Large data tests - 1MB
|
|
BenchConfig::new(4, 2, 1024 * 1024, 1024 * 1024),
|
|
BenchConfig::new(6, 3, 1024 * 1024, 1024 * 1024),
|
|
// Extra large data tests - 16MB
|
|
BenchConfig::new(4, 2, 16 * 1024 * 1024, 16 * 1024 * 1024),
|
|
];
|
|
|
|
for config in configs {
|
|
let data = generate_test_data(config.data_size);
|
|
let erasure = Erasure::new(config.data_shards, config.parity_shards, config.block_size);
|
|
|
|
// Pre-encode data
|
|
let encoded_shards = erasure.encode_data(&data).unwrap();
|
|
|
|
// Test SIMD decoding performance
|
|
let mut group = c.benchmark_group("decode_simd");
|
|
group.throughput(Throughput::Bytes(config.data_size as u64));
|
|
group.sample_size(10);
|
|
group.measurement_time(Duration::from_secs(5));
|
|
|
|
group.bench_with_input(
|
|
BenchmarkId::new("simd_impl", &config.name),
|
|
&(&encoded_shards, &config),
|
|
|b, (shards, config)| {
|
|
let erasure = Erasure::new(config.data_shards, config.parity_shards, config.block_size);
|
|
b.iter(|| {
|
|
// Simulate data loss - lose one data shard and one parity shard
|
|
let mut shards_opt: Vec<Option<Vec<u8>>> = shards.iter().map(|shard| Some(shard.to_vec())).collect();
|
|
|
|
// Lose last data shard and first parity shard
|
|
shards_opt[config.data_shards - 1] = None;
|
|
shards_opt[config.data_shards] = None;
|
|
|
|
erasure.decode_data(black_box(&mut shards_opt)).unwrap();
|
|
black_box(&shards_opt);
|
|
});
|
|
},
|
|
);
|
|
group.finish();
|
|
|
|
// Test direct reed-solomon-erasure decoding for large shards
|
|
let shard_size = calc_shard_size(config.data_size, config.data_shards);
|
|
if shard_size >= 512 && config.parity_shards > 0 {
|
|
use reed_solomon_erasure::galois_8::ReedSolomon;
|
|
|
|
if let Ok(rs) = ReedSolomon::new(config.data_shards, config.parity_shards) {
|
|
let mut rse_group = c.benchmark_group("decode_rse_direct");
|
|
rse_group.throughput(Throughput::Bytes(config.data_size as u64));
|
|
rse_group.sample_size(10);
|
|
rse_group.measurement_time(Duration::from_secs(5));
|
|
|
|
rse_group.bench_with_input(
|
|
BenchmarkId::new("rse_direct", &config.name),
|
|
&(&encoded_shards, &config),
|
|
|b, (shards, config)| {
|
|
b.iter(|| {
|
|
let mut shards_opt: Vec<Option<Vec<u8>>> = shards.iter().map(|s| Some(s.to_vec())).collect();
|
|
shards_opt[config.data_shards - 1] = None;
|
|
shards_opt[config.data_shards] = None;
|
|
|
|
rs.reconstruct_data(&mut shards_opt).unwrap();
|
|
black_box(shards_opt);
|
|
});
|
|
},
|
|
);
|
|
rse_group.finish();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Benchmark: Impact of different shard sizes on performance
|
|
fn bench_shard_size_impact(c: &mut Criterion) {
|
|
let shard_sizes = vec![64, 128, 256, 512, 1024, 2048, 4096, 8192];
|
|
let data_shards = 4;
|
|
let parity_shards = 2;
|
|
|
|
let mut group = c.benchmark_group("shard_size_impact");
|
|
group.sample_size(10);
|
|
group.measurement_time(Duration::from_secs(3));
|
|
|
|
for shard_size in shard_sizes {
|
|
let total_data_size = shard_size * data_shards;
|
|
let data = generate_test_data(total_data_size);
|
|
|
|
group.throughput(Throughput::Bytes(total_data_size as u64));
|
|
|
|
// Test SIMD implementation
|
|
group.bench_with_input(BenchmarkId::new("simd", format!("shard_{shard_size}B")), &data, |b, data| {
|
|
let erasure = Erasure::new(data_shards, parity_shards, total_data_size);
|
|
b.iter(|| {
|
|
let shards = erasure.encode_data(black_box(data)).unwrap();
|
|
black_box(shards);
|
|
});
|
|
});
|
|
}
|
|
group.finish();
|
|
}
|
|
|
|
/// Benchmark: Impact of coding configurations on performance
|
|
fn bench_coding_configurations(c: &mut Criterion) {
|
|
let configs = vec![
|
|
(2, 1), // Minimal redundancy
|
|
(3, 2), // Medium redundancy
|
|
(4, 2), // Common configuration
|
|
(6, 3), // 50% redundancy
|
|
(8, 4), // 50% redundancy, more shards
|
|
(10, 5), // 50% redundancy, many shards
|
|
(12, 6), // 50% redundancy, very many shards
|
|
];
|
|
|
|
let data_size = 1024 * 1024; // 1MB test data
|
|
let data = generate_test_data(data_size);
|
|
|
|
let mut group = c.benchmark_group("coding_configurations");
|
|
group.throughput(Throughput::Bytes(data_size as u64));
|
|
group.sample_size(10);
|
|
group.measurement_time(Duration::from_secs(5));
|
|
|
|
for (data_shards, parity_shards) in configs {
|
|
let config_name = format!("{data_shards}+{parity_shards}");
|
|
|
|
group.bench_with_input(BenchmarkId::new("encode", &config_name), &data, |b, data| {
|
|
let erasure = Erasure::new(data_shards, parity_shards, data_size);
|
|
b.iter(|| {
|
|
let shards = erasure.encode_data(black_box(data)).unwrap();
|
|
black_box(shards);
|
|
});
|
|
});
|
|
}
|
|
group.finish();
|
|
}
|
|
|
|
/// Benchmark: Memory usage patterns
|
|
fn bench_memory_patterns(c: &mut Criterion) {
|
|
let data_shards = 4;
|
|
let parity_shards = 2;
|
|
let block_size = 1024 * 1024; // 1MB block
|
|
|
|
let mut group = c.benchmark_group("memory_patterns");
|
|
group.sample_size(10);
|
|
group.measurement_time(Duration::from_secs(5));
|
|
|
|
// Test reusing the same Erasure instance
|
|
group.bench_function("reuse_erasure_instance", |b| {
|
|
let erasure = Erasure::new(data_shards, parity_shards, block_size);
|
|
let data = generate_test_data(block_size);
|
|
|
|
b.iter(|| {
|
|
let shards = erasure.encode_data(black_box(&data)).unwrap();
|
|
black_box(shards);
|
|
});
|
|
});
|
|
|
|
// Test creating new Erasure instance each time
|
|
group.bench_function("new_erasure_instance", |b| {
|
|
let data = generate_test_data(block_size);
|
|
|
|
b.iter(|| {
|
|
let erasure = Erasure::new(data_shards, parity_shards, block_size);
|
|
let shards = erasure.encode_data(black_box(&data)).unwrap();
|
|
black_box(shards);
|
|
});
|
|
});
|
|
|
|
group.finish();
|
|
}
|
|
|
|
/// Benchmark: end-to-end streaming decode through `Erasure::decode`.
|
|
///
|
|
/// Unlike `bench_decode_performance` (which calls `decode_data` on in-memory
|
|
/// shards), this drives the async `ParallelReader` path that reads each shard
|
|
/// through a `BitrotReader` per erasure stripe. That is the path executed on
|
|
/// every object GET, and the one where per-stripe shard buffers are allocated.
|
|
fn bench_streaming_decode(c: &mut Criterion) {
|
|
let rt = Runtime::new().unwrap();
|
|
let hash_algo = HashAlgorithm::HighwayHash256;
|
|
|
|
// (data_shards, parity_shards, object_size, block_size)
|
|
let configs = vec![
|
|
(4usize, 2usize, 16 * 1024 * 1024usize, 1024 * 1024usize),
|
|
(6, 3, 24 * 1024 * 1024, 1024 * 1024),
|
|
(4, 2, 4 * 1024 * 1024, 64 * 1024),
|
|
];
|
|
|
|
for (data_shards, parity_shards, data_size, block_size) in configs {
|
|
let data = generate_test_data(data_size);
|
|
let erasure = Erasure::new(data_shards, parity_shards, block_size);
|
|
let shard_size = erasure.shard_size();
|
|
let total_len = data.len();
|
|
|
|
// Pre-encode the object into per-shard bitrot streams once (setup).
|
|
let total_shards = data_shards + parity_shards;
|
|
let shard_bufs: Vec<Vec<u8>> = rt.block_on(async {
|
|
let mut writers: Vec<BitrotWriter<Cursor<Vec<u8>>>> = (0..total_shards)
|
|
.map(|_| BitrotWriter::new(Cursor::new(Vec::new()), shard_size, hash_algo.clone()))
|
|
.collect();
|
|
let mut off = 0;
|
|
while off < data.len() {
|
|
let end = (off + block_size).min(data.len());
|
|
let shards = erasure.encode_data(&data[off..end]).unwrap();
|
|
for (i, shard) in shards.iter().enumerate() {
|
|
writers[i].write(shard).await.unwrap();
|
|
}
|
|
off = end;
|
|
}
|
|
writers.into_iter().map(|w| w.into_inner().into_inner()).collect()
|
|
});
|
|
|
|
// verify=true mirrors the production default (bitrot verification on);
|
|
// verify=false isolates the buffer-handling cost from the hash pass.
|
|
for verify in [true, false] {
|
|
let skip_verify = !verify;
|
|
let mut group = c.benchmark_group("streaming_decode");
|
|
group.throughput(Throughput::Bytes(total_len as u64));
|
|
group.sample_size(10);
|
|
group.measurement_time(Duration::from_secs(5));
|
|
|
|
let name = format!(
|
|
"{}+{}_{}KB_{}KB-block_verify-{}",
|
|
data_shards,
|
|
parity_shards,
|
|
data_size / 1024,
|
|
block_size / 1024,
|
|
verify
|
|
);
|
|
|
|
// Validate decode once per config (untimed) so a regression in
|
|
// success or output length fails the benchmark without putting
|
|
// assertions inside the measured loop.
|
|
{
|
|
let readers: Vec<Option<BitrotReader<Cursor<&[u8]>>>> = shard_bufs
|
|
.iter()
|
|
.map(|buf| Some(BitrotReader::new(Cursor::new(buf.as_slice()), shard_size, hash_algo.clone(), skip_verify)))
|
|
.collect();
|
|
let mut sink = tokio::io::sink();
|
|
let (written, err) = rt.block_on(erasure.decode(&mut sink, readers, 0, total_len, total_len));
|
|
assert!(err.is_none(), "decode failed: {err:?}");
|
|
assert_eq!(written, total_len, "decode wrote {written} of {total_len} bytes");
|
|
}
|
|
|
|
group.bench_function(BenchmarkId::new("decode", name), |b| {
|
|
b.iter_batched(
|
|
|| {
|
|
// Setup (untimed): per-shard readers positioned at the
|
|
// object start, plus the no-op sink, so reader/UUID and
|
|
// sink construction stay out of the timed decode path.
|
|
let readers: Vec<Option<BitrotReader<Cursor<&[u8]>>>> = shard_bufs
|
|
.iter()
|
|
.map(|buf| {
|
|
Some(BitrotReader::new(Cursor::new(buf.as_slice()), shard_size, hash_algo.clone(), skip_verify))
|
|
})
|
|
.collect();
|
|
(readers, tokio::io::sink())
|
|
},
|
|
|(readers, mut sink)| {
|
|
rt.block_on(async {
|
|
let (written, _err) = erasure.decode(black_box(&mut sink), readers, 0, total_len, total_len).await;
|
|
black_box(written);
|
|
});
|
|
},
|
|
criterion::BatchSize::SmallInput,
|
|
);
|
|
});
|
|
group.finish();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Benchmark group configuration
|
|
criterion_group!(
|
|
benches,
|
|
bench_encode_performance,
|
|
bench_decode_performance,
|
|
bench_streaming_decode,
|
|
bench_shard_size_impact,
|
|
bench_coding_configurations,
|
|
bench_memory_patterns
|
|
);
|
|
|
|
criterion_main!(benches);
|