Я решил посмотреть скорость работы с файлами в Rust. Меня посетила простая идея: держать индекс до записи в памяти. На маленьких размерах записей (10 байт) всё шло очень даже хорошо.
результат простого теста для 10 байт
insert took: 3.112033812s
get took: 1.433445888s
Затем 100 байт.
результат простого теста для 100 байт
insert took: 3.02511339s
get took: 1.395097325s
А затем 500.
результат простого теста для 500 байт
insert took: 11.586355301s
get took: 1.778387251s
Как мы можем заметить, скорость чтения незначительно изменилась, но записи - резко уменьшилась!
Но затем я поставил 1000 байт и результаты вызвали у меня много вопросов.
результат простого теста для 1000 байт
insert took: 44.390536515s
get took: 132.18342292s
Я могу понять, почему падает скорость записи (но не в 4 раза, при изменении размера в 2), но что происходит с чтением? Почему скорость чтения падает в 78 раз?
Привожу код и буду только рад услышать его критику.
use positioned_io::ReadAt;
struct CustomStorage {
infos: DashMap<Vec<u8>, (u32, u64)>,
atomic_indexes: Vec<Arc<AtomicU64>>,
files: Vec<Arc<RwLock<File>>>,
read_files: Vec<Arc<RwLock<File>>>,
mask: usize
}
impl CustomStorage {
fn new(size: usize) -> Self {
let size = {
if size.is_power_of_two() {
size
} else {
size.next_power_of_two()
}
};
let lob = f64::log2(size as f64) as u32;
let mask = (1 << lob) - 1;
let mut files = Vec::with_capacity(size);
let mut read_files = Vec::with_capacity(size);
let mut atomic_indexes = Vec::with_capacity(size);
std::fs::DirBuilder::new().create("data1").unwrap();
for i in 0..size {
files.push(Arc::new(RwLock::new(File::create(format!("data1/test{}.txt", i)).unwrap())));
read_files.push(Arc::new(RwLock::new(File::open(format!("data1/test{}.txt", i)).unwrap())));
atomic_indexes.push(Arc::new(AtomicU64::new(0)));
}
Self {
infos: DashMap::new(),
atomic_indexes,
files,
read_files,
mask
}
}
#[inline(always)]
fn insert(&self, key: Vec<u8>, mut value: Vec<u8>) {
let res = self.get_file(&key);
if res.is_none() {
return;
}
let kl = key.len();
let vl = value.len();
let size = (4 + kl + vl) as u64;
let mut buf = Vec::with_capacity(size as usize);
buf.push((kl >> 8) as u8);
buf.push(kl as u8);
buf.append(&mut key.clone());
buf.push((vl >> 8) as u8);
buf.push(vl as u8);
buf.append(&mut value);
let index;
let (file, atomic_index) = unsafe { res.unwrap_unchecked() };
{
let mut file = file.write().unwrap();
file.write_all(&buf).expect("failed to write");
index = atomic_index.fetch_add(size, std::sync::atomic::Ordering::SeqCst);
}
self.infos.insert(key, (size as u32, index));
}
#[inline(always)]
fn get(&self, key: &Vec<u8>) -> Option<Vec<u8>>{
let res = self.get_index_and_file(key);
if res.is_none() {
return None;
}
let (file, info) = unsafe { res.unwrap_unchecked() };
let file = file.read().unwrap();
let mut buf = vec![0; info.0 as usize];
file.read_at(info.1, &mut buf).expect("failed to read");
return Some(buf);
}
#[inline(always)]
fn get_file(&self, key: &Vec<u8>) -> Option<(Arc<RwLock<File>>, Arc<AtomicU64>)> {
let mut hasher = DefaultHasher::new();
key.hash(&mut hasher);
let number = hasher.finish() as usize & self.mask;
return Some((self.files[number].clone(), self.atomic_indexes[number].clone()));
}
#[inline(always)]
fn get_index_and_file(&self, key: &Vec<u8>) -> Option<(Arc<RwLock<File>>, (u32, u64))> {
let mut hasher = DefaultHasher::new();
key.hash(&mut hasher);
let number = hasher.finish() as usize & self.mask;
let info;
{
let index_ = self.infos.get(key);
if index_.is_none() {
return None;
}
info = unsafe { *index_.unwrap_unchecked() };
}
return Some((self.read_files[number].clone(), info));
}
fn bench(keys: Arc<Vec<Vec<u8>>>, values: Arc<Vec<Vec<u8>>>) {
let custom_storage = Arc::new(Self::new(128));
let mut joins = Vec::with_capacity(PAR);
let start = std::time::Instant::now();
for i in 0..PAR {
let space = custom_storage.clone();
let keys = keys.clone();
let values = values.clone();
joins.push(std::thread::spawn(move || {
for j in i * COUNT..(i + 1) * COUNT {
space.insert(keys[j].clone(), values[j].clone());
}
}))
}
for join in joins {
join.join().unwrap();
}
println!("insert took: {:?}", start.elapsed());
let mut joins = Vec::with_capacity(PAR);
let start = std::time::Instant::now();
for i in 0..PAR {
let space = custom_storage.clone();
let keys = keys.clone();
joins.push(std::thread::spawn(move || {
for j in i * COUNT..(i + 1) * COUNT {
space.get(&keys[j]);
}
}))
}
for join in joins {
join.join().unwrap();
}
println!("get took: {:?}", start.elapsed());
}
}
Константы для теста
const N: usize = 3_000_000;
const SIZE: usize = 1000;
const PAR: usize = 256;
const COUNT: usize = N/PAR;