Почему последовательное чтение быстрее, чем последовательная запись?

Вопрос или проблема

У меня есть код для транспонирования матрицы, который выглядит так:

for (size_t ii = 0; ii < ii_end; ii+=TILE_SIZE) {
    for (size_t jj = 0; jj < jj_end; jj+=TILE_SIZE) {
        for (size_t i = 0; i < TILE_SIZE; i++) {
            for (size_t j = 0; j < TILE_SIZE; j++) {
                out[(jj + j)* n + ii + i] = in[(ii + i) * n + jj + j];
            }
        }
    }
}

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

Вторая версия того же алгоритма работает наоборот: out последовательно записывает в память, а in считывает с пропуском. Она выглядит так:

for (size_t ii = 0; ii < ii_end; ii+=TILE_SIZE) {
    for (size_t jj = 0; jj < jj_end; jj+=TILE_SIZE) {
        for (size_t i = 0; i < TILE_SIZE; i++) {
            for (size_t j = 0; j < TILE_SIZE; j++) {
                out[(ii + i)* n + jj + j] = in[(jj + j) * n + ii + i];
            }
        }
    }
}

Вторая версия всегда быстрее для всех размеров матриц (я тестировал размеры 20, 50, 100, 200, 500, 1000, 2000 и 5000). Обе версии выполняют похожее количество инструкций. Я также измерял общий объем данных в памяти (чтения и записи) с помощью LIKWID, и первая версия передает немного меньше данных из памяти в ЦП.

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

Я работаю на ноутбуке Intel(R) Core(TM) i5-10210U. LIKWID использует счетчики событий DRAM_READS и DRAM_WRITES для расчета общего объема памяти.

Буду благодарен за любую информацию. Также, если вы можете указать мне ресурсы о технологиях оптимизации в подсистемах памяти современных процессоров, я буду очень признателен.

Ответ или решение

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

Введение

В условиях работы с алгоритмами обработки данных, таких как транспонирование матриц, важно учитывать не только сложность алгоритма, но и то, как именно организована память и взаимодействие процессора с ней. В вашем случае, присутствует явное различие в производительности двух подходов: один выполняет последовательные чтения и неравномерные записи, а другой — наоборот. Это поведение может быть объяснено несколькими ключевыми аспектами архитектуры современных процессоров и систем памяти.

Архитектура кеша

Современные процессоры имеют несколько уровней кеша (L1, L2, L3), которые значительно уменьшают время доступа к данным. Кеширование работает таким образом, что данные, которые недавно были использованы, будут изолированы в быстрое хранилище рядом с процессором. Когда происходит чтение данных, кеш контролирует, есть ли данные уже в нем. Если они находятся в кеше, чтение будет происходить почти мгновенно, следовательно, производительность возрастает.

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

Принципы работы с памятью

При выполнении первой версии вашего алгоритма, где данные читаются последовательно, а записи имеют стрельчатый характер, происходит большее количество конфликтов кеша (cache thrashing). Это объясняется тем, что высоко вероятно, что операции записи могут затруднять операции чтения из-за высоких временных затрат на кэширование и обновление кеша.

Во второй версии, где происходит последовательная запись, данные эффективно упаковываются в кеш и сразу же записываются в основную память. Это создает больше возможностей для write combining, когда несколько данных могут быть объединены в один кеш-линию, позволяя процессору экономить время на взаимодействии с памятью.

Проблемы производительности

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

  • Латентность доступа к памяти: последовательные чтения могут обрабатывать данные быстрее, потому что они могут значительно снизить число обращений к основной памяти.
  • Параллелизм операций: при записи данных в потоках, которые не перекрываются, происходит блокировка, что увеличивает общее время выполнения.
  • Кэширование: фрагментированная запоминаемость при записи влияет на скорость, в отличие от последовательного чтения, которое более эффективно использует кеш.

Заключение

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

  • Intel Software Developer’s Guide
  • Computer Architecture: A Quantitative Approach от Hennessy и Patterson
  • High-Performance Memory Systems от Vassiliadis и других

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

Оцените материал
Добавить комментарий

Капча загружается...