Как лучше всего распараллелить код, изменяя несколько фрагментов одного и того же вектора Rust?

Допустим, мы хотим удвоить (на месте) каждый элемент в каждом из срезов вектора, где срезы определяются списком пар - (начальная, конечная) позиции. Следующий код идиоматически выражает намерение, но не компилируется из-за изменяемого заимствования вектора внутри параллели for_each:

use rayon::prelude::*;

fn main() {
    let mut data = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
    let slice_pairs = vec![(0, 3), (4, 7), (8, 10)];

    slice_pairs.into_par_iter().for_each(|(start, end)| {
        let slice = &mut data[start..end];
        for elem in slice.iter_mut() {
            *elem *= 2;
        }
    });

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

Здесь существует реальная вероятность гонок данных — чтобы их исключить, нужно проверить, не перекрываются ли срезы. Вопрос в том, как лучше всего это сделать в Rust: с помощью небезопасного кода или безопасного API. В следующем коде unsafe используется для «сделай это»; мой вопрос в том, есть ли лучший способ, чем приведенный ниже (который преобразует базовый указатель вектора в i64 и обратно, чтобы «слепить» средство проверки заимствования на проблему.)

use rayon::prelude::*;
use std::mem;

fn main() {
    let mut data = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
    let slice_pairs = vec![(0, 4), (4, 7), (7, 10)];

    let ptr_outer = data.as_mut_ptr();
    let ptr_int : i64 = unsafe { mem::transmute(ptr_outer) };

    slice_pairs.into_par_iter().for_each(|(start, end)| {
        unsafe {
            let ptr : *mut i32 = mem::transmute(ptr_int);
            let slice = std::slice::from_raw_parts_mut(ptr.add(start), end - start);

            for elem in slice.iter_mut() {
                *elem *= 2;
            }
        }
    });

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

В случае удвоения есть другой способ рассмотрения, но O(N) в памяти - выделить вектор атомарных целых чисел с инициализацией 1 (это будут множители реальных данных), параллельно обрабатывать диапазоны, умножать атомарные значения (они обеспечивают синхронизировать). Затем запустите параллельное умножение фактических данных на множители по частям (без перекрытия, поэтому синхронизация не требуется).

Alexey S. Larionov 15.03.2024 15:04

@AlexeyS.Larionov Не нужно выделять и копировать, есть AtomicX::from_mut_slice() (ночью, стабильно можно использовать небезопасный код).

Chayim Friedman 16.03.2024 20:06

Я отредактировал свой ответ, добавив непроверенную (и небезопасную) версию split_many(), которую можно использовать, если известно, что регионы не перекрываются.

user4815162342 17.03.2024 10:35
Почему Python в конце концов умрет
Почему Python в конце концов умрет
Последние 20 лет были действительно хорошими для Python. Он прошел путь от "просто языка сценариев" до основного языка, используемого для написания...
3
3
120
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Я бы предложил сначала преобразовать slice_pairs в последовательность изменяемых срезов, а затем использовать все эти срезы параллельно.

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

Обратите внимание, что я пытался использовать .map().collect() вместо явного цикла с .push(), чтобы построить последовательность срезов, но мне это не удалось...
Компилятор сообщил, что замыкание FnMut в .map() не может вернуть ссылку; может быть кто-то сможет исправить мой код...

use rayon::prelude::*;

fn main() {
    let mut data = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
    let slice_pairs = vec![(0, 3), (4, 7), (8, 10)];

    // build a sequence of mutable slices
    let mut slices = Vec::with_capacity(slice_pairs.len());
    let mut remaining = data.as_mut_slice();
    let mut idx = 0;
    for (start, end) in slice_pairs {
        let (_skip, tail) = remaining.split_at_mut(start - idx);
        let (sl, tail) = tail.split_at_mut(end - start);
        remaining = tail;
        idx = end;
        slices.push(sl);
    }
    println!("slices: {:?}", slices);

    // parallel usage of the mutable slices
    slices.into_par_iter().for_each(|sl| {
        for elem in sl.iter_mut() {
            *elem *= 2;
        }
    });

    println!("data: {:?}", data);
}
/*
slices: [[1, 2, 3], [5, 6, 7], [9, 10]]
data: [2, 4, 6, 4, 10, 12, 14, 8, 18, 20]
*/

Я думаю, что split_at_mut или что-то подобное — это то, что вам нужно, и я думаю, что в Rayon нет ни split_according_to_a_list_of_pairs_mut, ни итератора, который «просто делает это». Спасибо!

Yossi Kreinin 15.03.2024 19:56

@YossiKreinin Однако вы определенно можете написать split_according_to_a_list_of_pairs_mut() в безопасном Rust, используя split_at_mut() в качестве строительного блока. См. split_many() в решении, которое я написал до того, как понял, что prog-fh опередил меня: play.rust-lang.org/…

user4815162342 15.03.2024 21:57

@user4815162342 user4815162342 спасибо, это выглядит хорошо, и я думаю, так и должно быть сделано в безопасном коде. Интересно, есть ли более чистый способ использовать небезопасную версию, которая, кстати, быстрее, поскольку ей не нужно сортировать фрагменты. (Чище, как без преобразования указателей или иным образом более лаконично)

Yossi Kreinin 16.03.2024 09:25

@YossiKreinin Небезопасная версия требует сортировки фрагментов или проверки того, что они не перекрываются каким-либо другим образом. Единственная причина, по которой он сейчас работает быстро, заключается в том, что он совершенно не подходит для произвольного slice_pairs.

user4815162342 16.03.2024 11:32

@YossiKreinin Я добавил свой код в качестве ответа, потому что отдельная реализация split_many() может быть полезна будущим посетителям.

user4815162342 16.03.2024 11:35
Ответ принят как подходящий

Вы можете использовать split_at_mut(), чтобы разбить фрагмент на несколько фрагментов, используя безопасный код:

fn split_many<'a, T>(mut slice: &'a mut [T], regions: &[(usize, usize)]) -> Vec<&'a mut [T]> {
    let mut regions = regions.to_vec();
    regions.sort_by_key(|&(b, _e)| b);
    let mut ret = vec![];
    let mut offset = 0;
    for (b, e) in regions {
        assert!(b >= offset && e >= b); // prohibit overlaps
        let (chosen, rest) = slice.split_at_mut(e - offset);
        ret.push(&mut chosen[b - offset..]);
        offset = e;
        slice = rest;
    }
    ret
}

Имея этот помощник, вы можете выразить параллельную манипуляцию на месте «очевидным» способом:

fn main() {
    let mut data = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
    let slice_pairs = vec![(0, 3), (4, 7), (8, 10)];

    split_many(&mut data, &slice_pairs)
        .into_par_iter()
        .for_each(|region| {
            for elem in region.iter_mut() {
                *elem *= 2;
            }
        });

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

Детская площадка

Обратите внимание: пока регионы представлены произвольными индексами, предоставленными во время выполнения, необходимо выполнить первоначальный проход через них, чтобы убедиться, что они не перекрываются (split_many() паникует, если обнаруживает перекрытие). Невыполнение этого требования было бы неразумно, поскольку простой выбор перекрывающихся регионов привел бы к неопределенному поведению. Однако если вы контролируете код, генерирующий регионы, и знаете, что они не перекрываются, вы можете создать более быструю небезопасную версию split_many(). Опираясь на внешние гарантии, ему не нужно ни сортировать, ни выделять новый набор регионов, и он может даже напрямую вернуть ParallelIterator:

/// Split `slice` into `regions` and iterate over them in parallel.
/// Safety: regions must not overlap.
unsafe fn split_many_unchecked<'a, T: Send + Sync>(
    slice: &'a mut [T],
    regions: &'a [(usize, usize)],
) -> impl ParallelIterator<Item = &'a mut [T]> + 'a {
    struct Wrap<T>(*mut T);
    unsafe impl<T> Sync for Wrap<T> {}
    unsafe impl<T> Send for Wrap<T> {}
    let slice = Wrap(slice.as_mut_ptr());
    regions.par_iter().map(move |&(b, e)| {
        let _ = &slice; // prevent closure from capturing slice.0
        std::slice::from_raw_parts_mut(slice.0.add(b), e - b)
    })
}

Детская площадка

Другие вопросы по теме