Eugene-Usachev
@Eugene-Usachev

Почему скорость чтения из файла резко деградирует?

Я решил посмотреть скорость работы с файлами в 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;

  • Вопрос задан
  • 322 просмотра
Решения вопроса 1
dimonchik2013
@dimonchik2013
non progredi est regredi
ну, раз никто не ответил, чуток лекции от меня

во-первых как сказал ув. Василий Банников -тестировать надо только IO диска
вот это вот детский сад, так нельзя
Сначала я прогнал тест на Windows... я и не стал проверять на Windows неделю назад (работал в Docker).
условия должны быть неизменны

во-вторых, как говорю я - надо изучить что уже известно по этому вопросу:
  • вот тут товарищи тоже задаются года с 2018
  • а вот и кое-какой продукт

да, это про IO а не файлы, но - с твоей задачей где-то рядом, если вообще не то что надо , но там много вопросов - ответов, которые расширят твое понимание - например, разное поведение в разных ОС

в третьих, есть такая штука как кеш диска (а еще есть кеш у харварного рейда, но не всегда), да так что в этой вашей команде DD
dd if=/tmp/test.img of=/dev/null bs=1M count=1024
есть спец опция для отклбчения кеша, иначе получается космик цифры

в четвертых - есть проблема храннеия мелких файлов и вообще файловой системы, тут приведу только два-три слова: самопис ( там почитаешь про суть проблемы), пром1 пром2

в общем, задачка сильно посложнее чем просто погонять байты
и, это конечно не мое дело - но "а зачем"? что ты будешь делать с полученной инфой?

если все же "а зачем" осталось актуальным - я бы делал так:
1) прогнал прогу для дисков из пример выше
2) посмотрел бы вдумчиво это видео (увы, не про Rust, но докладчик знает толк в извращениях (с)), в том числе и ввиду твоих проблем с генерацией в памяти

воообще - сам подход правильный, надо знать максимумы что может язык, если хочешь называться профессионалом, но само решение... поверь - куда проще отталкиваться от уже написанного кем-то рабочего кода
Ответ написан
Комментировать
Пригласить эксперта
Ответы на вопрос 1
Eugene-Usachev
@Eugene-Usachev Автор вопроса
Чёрная магия, не иначе. Несколько раз возвращался к коду на неделе и так и не исправил эту ошибку. Решил выписать все теории и проверить залпом.

Проблема не Оперативной Памяти. Как вы могли заметить, я держу в памяти все ключи и значения. Начал их генерировать на ходу, стало хуже.

Проблема не в том, что размер данных слишком большой. Поставил 100 байт, но 30 000 000 повторов, стало ещё хуже (оно и логично).

Дальше я заметил крайне странную ситуацию. Сначала я прогнал тест на Windows и получил 2.5 секунды на вставку и 17 секунд на получение (при 100 байт), однако проблема этого теста в том, что IDE пытается заиндексировать новые файлы, так что я и не стал проверять на Windows неделю назад (работал в Docker), но тут решил проверить. Получилось (на 1000 байт) 10.8 секунд на запись и 24 секунды на получение. Этот тест вызвал у меня ещё больше вопросов.

Я программист с учебным опыт около полутора лет и не могу это объяснить. Может кто-нибудь объяснить такую ситуацию?
Ответ написан
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы