Не удается воспроизвести ложную проблему совместного использования строк в Rust - PullRequest
0 голосов
/ 10 января 2019

Я пытаюсь воспроизвести пример 6 из Галереи эффектов кэша процессора .

В статье приведена эта функция (в C #) в качестве примера проверки ложного обмена:

private static int[] s_counter = new int[1024];
private void UpdateCounter(int position)
{
    for (int j = 0; j < 100000000; j++)
    {
        s_counter[position] = s_counter[position] + 3;
    }
}

Если мы создадим потоки, передающие этой функции аргументы 0, 1, 2, 3, вычисление займет много времени (автор получил 4,3 секунды). Если мы передадим, например, 16, 32, 48, 64, мы получим намного более хорошие результаты (0,28 секунды).

Я предложил следующую функцию в Rust:

pub fn cache_line_sharing(arr: [i32; 128], pos: usize) -> (i32, i32) {
    let arr = Arc::new(arr);
    let handles: Vec<_> = (0..4).map(|thread_number| {
        let arr = arr.clone();
        let pos = thread_number * pos;
        thread::spawn(move || unsafe {
            let p = (arr.as_ptr() as *mut i32).offset(pos as isize);
            for _ in 0..1_000_000 {
                *p = (*p).wrapping_add(3);
            }
        })
    }).collect();

    for handle in handles {
        handle.join().unwrap();
    }

    (arr[0], arr[1])
}

Сравнительный анализ с двумя наборами аргументов (0, 1, 2, 3 и 0, 16, 32, 48) дает мне почти идентичные результаты: 108,34 и 105,07 микросекунд.

Я использую критерий для оценки. У меня MacBook Pro 2015 с процессором Intel i5-5257U (2,70 ГГц). Моя система сообщает, что 64B размер строки кэша.

Если кто-то захочет увидеть мой полный код тестирования, вот ссылки: - lib.rs - cache_lines.rs

Я хочу понять проблему и найти способ воспроизвести результаты, аналогичные приведенным в статье.

1 Ответ

0 голосов
/ 11 января 2019

Ваша первая проблема заключается в том, что *p.wrapping_add(3) выполняет арифметику с указателем, а не с целым числом. Первая итерация цикла загружала значение через три пробела после p и сохраняла его при p, а Rust оптимизировал остальные 999999 итераций цикла как избыточные. Вы имели в виду (*p).wrapping_add(3).

После этого изменения Rust оптимизирует 1000000 дополнений на 3 в одно добавление на 3000000. Вы можете использовать read_volatile и write_volatile, чтобы избежать этой оптимизации.

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

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

pub fn cache_line_sharing(arr: [i32; 128], pos: usize) -> (i32, i32) {
    struct SyncWrapper(UnsafeCell<[i32; 128]>);
    unsafe impl Sync for SyncWrapper {}

    assert_ne!(pos, 0);
    let arr = Arc::new(SyncWrapper(UnsafeCell::new(arr)));
    let handles: Vec<_> = (0..4)
        .map(|thread_number| {
            let arr = arr.clone();
            let pos = thread_number * pos;
            thread::spawn(move || unsafe {
                let p: *mut i32 = &mut (*arr.0.get())[pos];
                for _ in 0..1_000_000 {
                    p.write_volatile(p.read_volatile().wrapping_add(3));
                }
            })
        })
        .collect();

    for handle in handles {
        handle.join().unwrap();
    }

    let arr = unsafe { *arr.0.get() };
    (arr[0], arr[1])
}
...