Что каждый программист должен знать о памяти [2007] Ulrich Drepper

A Solid Average

Нужно ли её читать — вопрос. Уж точно не “каждому”.

Книге уже 17 лет, но информация актуальна: устройство памяти и кэшей практически не изменилось. Разве L3 уже не диковинка.

На примерах кода показаны последствия изменения рабочего набора данных, количества потоков, расположения критического слова. Автор показывает, как влияют размеры кэша/TLB, количество промахов и переключений контекста, механизм предварительной загрузки.

Читается сухо, но альтернатив по теме я не встречал. Если знаете что-то похожее — буду благодарен за ссылки.

Я бы советовал читать выборочно: части 2, 3 и 5 — “Кэш-память процессора”, “Виртуальная память”, “Оптимизация кэша”. Возможно, начало 6 части и 7 — “Мультипотоковая оптимизация”, “Инструменты”.

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

Английская версия книги доступна по ссылке: What every programmer should know about memory

Практика

Не удержался и провёл эксперимент на своей машине: проверял, можно ли определить размер кэшей с помощью кода — можно.

cache latencies

Визуализировал всё на Python — можно попросить любой AI инструмент, он этот код напишет на раз два. Впрочем, и код ниже был написан с помощью AI - в нём нет ничего необычного, разве пара моментов с madvise, разогревом и black_box, чтобы компилятор ничего не оптимизировал.

Программа создаёт связный список в памяти, пробегает его несколько раз и измеряет среднюю задержку доступа. Таким образом можно наглядно увидеть влияние кэширования и размера рабочего набора.

#[repr(C)]
struct Node {
    next: *mut Node,
    _pad: [usize; 0],
}

struct LinkedListArena {
    ptr: *mut u8,
    layout: Layout,
    head: *mut Node,
    num_elements: usize,
    total_bytes: usize,
}

impl LinkedListArena {
    fn new(num_elements: usize, element_size: usize) -> Self {
        assert!(element_size >= mem::size_of::<Node>());
        assert!(element_size % mem::size_of::<usize>() == 0);
        let total_bytes = num_elements
            .checked_mul(element_size)
            .expect("size overflow when computing total_bytes");

        let align = 64usize.max(mem::align_of::<Node>());
        let layout = Layout::from_size_align(total_bytes, align).expect("invalid layout");

        let ptr = unsafe { alloc_zeroed(layout) };
        if ptr.is_null() {
            panic!("memory allocation failed");
        }

        // Optionally advise the kernel about access pattern and lock into RAM to avoid swapping
        unsafe {
            let _ = madvise(ptr as *mut c_void, total_bytes, MADV_RANDOM);
            if mlock(ptr as *const c_void, total_bytes) != 0 {
                eprintln!("Warning: mlock failed (continuing anyway). Consider running with privileges or ulimit adjustments.");
            }
        }

        unsafe {
            for i in 0..num_elements {
                let current_node_ptr = ptr.add(i * element_size) as *mut Node;
                let next_idx = (i + 1) % num_elements;
                let next_node_ptr = ptr.add(next_idx * element_size) as *mut Node;
                ptr::write(&mut (*current_node_ptr).next, next_node_ptr);
            }
        }
        let head = ptr as *mut Node;

        Self {
            ptr,
            layout,
            head,
            num_elements,
            total_bytes,
        }
    }

    fn run_benchmark(&self, warmup_iterations: usize, iterations: usize) -> f64 {
        unsafe {
            let mut cur = self.head;
            for _ in 0..(warmup_iterations * self.num_elements) {
                cur = (*black_box(cur)).next;
            }

            let start = Instant::now();
            for _ in 0..(iterations * self.num_elements) {
                cur = (*black_box(cur)).next;
            }
            let elapsed = start.elapsed();
            // prevent compiler from optimizing away the traversal
            black_box(cur);

            let total_steps = (iterations * self.num_elements) as f64;
            elapsed.as_nanos() as f64 / total_steps
        }
    }
}

impl Drop for LinkedListArena {
    fn drop(&mut self) {
        unsafe {
            let _ = munlock(self.ptr as *const c_void, self.total_bytes);
            dealloc(self.ptr, self.layout);
        }
    }
}

fn main() {
    const TOTAL_STEPS: usize = 10_000_000;
    const WARMUP_ITERATIONS: usize = 2;

    let args: Vec<String> = env::args().collect();
    if args.len() != 3 {
        eprintln!("Usage: {} working_set_2_pow npad", args[0]);
        std::process::exit(1);
    }

    let working_set: usize = args[1].parse().expect("working_set_kb must be an integer");
    let npad: usize = args[2].parse().expect("NPAD must be an integer");

    let working_set_bytes = 1 << working_set;
    let element_size = std::mem::size_of::<Node>() + npad * std::mem::size_of::<usize>();
    let num_elements = working_set_bytes / element_size;

    if num_elements < 2 {
        panic!("Too few elements (num_elements < 2). Try larger working_set_kb or smaller element_stride.");
    }

    let iterations = (TOTAL_STEPS + num_elements - 1) / num_elements;
    let arena = LinkedListArena::new(num_elements, element_size);
    let avg_ns = arena.run_benchmark(WARMUP_ITERATIONS, iterations);

    let working_set_kb = working_set_bytes / 1024;
    println!(
        "working_set {}kb, npad:{}: {}ns",
        working_set_kb, npad, avg_ns
    );
}

Метки:

Разделы:

Дата изменения: