Контекст

У меня есть случай, когда несколько потоков должны обновить объекты, хранящиеся в общем векторе. Однако вектор очень большой, а количество элементов для обновления относительно невелико.

Проблема

В минимальном примере набор элементов для обновления может быть идентифицирован с помощью набора (хеш-кодов), содержащего индексы элементов для обновления. Код может выглядеть следующим образом:

let mut big_vector_of_elements = generate_data_vector();

while has_things_to_do() {
    let indices_to_update = compute_indices();
    indices_to_update.par_iter() // Rayon parallel iteration
       .map(|index| big_vector_of_elements[index].mutate())
       .collect()?;
}

Это явно запрещено в Rust: big_vector_of_elements нельзя заимствовать в нескольких потоках одновременно. Однако включение каждого элемента, например, в блокировку Mutex, кажется ненужным: этот конкретный случай был бы безопасным без явной синхронизации. Поскольку индексы поступают из набора, они гарантированно будут отличаться. Никакие две итерации в par_iter не касаются одного и того же элемента вектора.

Повторяю мой вопрос

Как лучше всего написать программу, которая мутирует элементы в векторе параллельно, где синхронизация уже решается выбором индексов, но где компилятор не понимает последнее?

Почти оптимальным решением было бы обернуть все элементы в big_vector_of_elements в некоторый гипотетический замок UncontendedMutex, который был бы вариантом Mutex, который смехотворно быстр в непредусмотренном случае и который может принимать произвольно долго, когда происходит раздор (или даже паники). В идеале UncontendedMutex<T> также должен быть того же размера и выравнивания, что и T, для любого T.

Связанные, но разные вопросы:

На несколько вопросов можно ответить с помощью «использовать параллельный итератор Района», «использовать chunks_mut» или «использовать split_at_mut»:

Эти ответы здесь не кажутся уместными, поскольку эти решения подразумевают итерацию по всему big_vector_of_elements, а затем для каждого элемента выясняется, нужно ли что-либо менять. По сути, это означает, что такое решение будет выглядеть следующим образом:

let mut big_vector_of_elements = generate_data_vector();

while has_things_to_do() {
    let indices_to_update = compute_indices();
    for (index, mut element) in big_vector_of_elements.par_iter().enumerate() {
        if indices_to_update.contains(index) {
            element.mutate()?;
        }
    }
}

Это решение требует времени, пропорционального размеру big_vector_of_elements, тогда как первое решение зацикливается только на несколько элементов, пропорциональных размеру indices_to_update.

17
Thierry 1 Май 2019 в 19:40

3 ответа

Лучший ответ

Вы можете отсортировать indices_to_update и извлечь изменяемые ссылки, вызвав split_*_mut.

let len = big_vector_of_elements.len();

while has_things_to_do() {
    let mut tail = big_vector_of_elements.as_mut_slice();

    let mut indices_to_update = compute_indices();
    // I assumed compute_indices() returns unsorted vector
    // to highlight the importance of sorted order
    indices_to_update.sort();

    let mut elems = Vec::new();

    for idx in indices_to_update {
        // cut prefix, so big_vector[idx] will be tail[0]
        tail = tail.split_at_mut(idx - (len - tail.len())).1;

        // extract tail[0]
        let (elem, new_tail) = tail.split_first_mut().unwrap();
        elems.push(elem);

        tail = new_tail;
    }
}

Дважды проверьте все в этом коде; Я не проверял это. Тогда вы можете позвонить elems.par_iter(...) или как угодно.

4
Shepmaster 3 Май 2019 в 02:11

Я думаю, что это разумное место для использования кода unsafe. Сама логика безопасна, но не может быть проверена компилятором, потому что она опирается на знания вне системы типов (контракт BTreeSet, который сам зависит от реализации Ord и друзей для {{X3 } } ) .

В этом примере мы предварительно проверяем все индексы с помощью range, поэтому каждый вызов add безопасен. Поскольку мы принимаем набор, мы знаем, что все индексы не пересекаются, поэтому мы не вводим изменяемый псевдоним. Важно получить необработанный указатель из среза, чтобы избежать наложения псевдонима между самим срезом и возвращаемыми значениями.

use std::collections::BTreeSet;

fn uniq_refs<'i, 'd: 'i, T>(
    data: &'d mut [T],
    indices: &'i BTreeSet<usize>,
) -> impl Iterator<Item = &'d mut T> + 'i {
    let start = data.as_mut_ptr();
    let in_bounds_indices = indices.range(0..data.len());

    // I copied this from a Stack Overflow answer
    // without reading the text that explains why this is safe
    in_bounds_indices.map(move |&i| unsafe { &mut *start.add(i) })
}

use std::iter::FromIterator;

fn main() {
    let mut scores = vec![1, 2, 3];

    let selected_scores: Vec<_> = {
        // The set can go out of scope after we have used it.
        let idx = BTreeSet::from_iter(vec![0, 2]);
        uniq_refs(&mut scores, &idx).collect()
    };

    for score in selected_scores {
        *score += 1;
    }

    println!("{:?}", scores);
}

Как только вы использовали эту функцию, чтобы найти все отдельные изменяемые ссылки, вы можете использовать Rayon для их параллельного изменения:

use rayon::prelude::*; // 1.0.3

fn example(scores: &mut [i32], indices: &BTreeSet<usize>) {
    let selected_scores: Vec<_> = uniq_refs(scores, indices).collect();
    selected_scores.into_par_iter().for_each(|s| *s *= 2);

    // Or

    uniq_refs(scores, indices).par_bridge().for_each(|s| *s *= 2);
}

Смотрите также:

2
Shepmaster 8 Май 2019 в 15:28

Возможно, вы ищете структуру данных с разделенными наборами, форму разделения, определяемую наборы индексов для элементов списка. Хорошая реализация этой структуры в Rust позволит вам безопасно и эффективно проходить и изменять значения каждого набора параллельно, поскольку наборы, как известно, не пересекаются.

К счастью, есть partitions ящик, который обеспечивает реализацию с несвязным множеством. Как только PartitionVec построен, каждый набор может быть повторен независимо, используя all_sets_mut() метод¹. Следующий код использует rayon для параллельной обработки трех наборов чисел, каждый из которых состоит из 2 элементов.

use partitions::{partition_vec, partitions_count_expr, PartitionVec};
use rayon::prelude::*;

let mut partition_vec = partition_vec![
    2 => 0, // value 2 in set 0
    4 => 0, // value 4 in set 0
    6 => 1, // value 6 in set 1
    8 => 1,
    10 => 2,
    12 => 2,
];
println!("Before: {:?}", partition_vec.as_slice());

let sets: Vec<_> = partition_vec.all_sets_mut().collect();
sets.into_par_iter().for_each(|set| {
    for (_index, value) in set {
        *value = (*value + 1) * 10;
    }
});

println!("After: {:?}", partition_vec.as_slice());

Выход:

Before: [2, 4, 6, 8, 10, 12]
After: [30, 50, 70, 90, 110, 130]

Остальная проблема заключается в создании этого секционированного вектора, но ящик уже имеет средства для превращения стандарта Vec в PartitionedVec и обратно. По умолчанию каждому значению присваивается одноэлементный набор. Функция compute_indices(), предложенная в вопросе, будет манипулировать этим вектором, чтобы заранее создать намеченные множества.

¹ Вероятно, из-за деталей реализации (начиная с версии 0.2.4) соответствующий итератор для неизменяемого доступа, полученный с помощью all_sets(), не может быть безопасно перемещен между потоками, что делает его непригодным для параллельной обработки.

-2
E_net4 in a full meta jacket 1 Май 2019 в 17:57