Подтвердить что ты не робот

Почему копирование столбца 2D-массива по столбцу занимает больше времени, чем строка за строкой в ​​C?

#include <stdio.h>
#include <time.h>

#define N  32768

char a[N][N];
char b[N][N];

int main() {
    int i, j;

    printf("address of a[%d][%d] = %p\n", N, N, &a[N][N]);
    printf("address of b[%5d][%5d] = %p\n", 0, 0, &b[0][0]);

    clock_t start = clock();
    for (j = 0; j < N; j++)
        for (i = 0; i < N; i++)
            a[i][j] = b[i][j];
    clock_t end = clock();
    float seconds = (float)(end - start) / CLOCKS_PER_SEC;
    printf("time taken: %f secs\n", seconds);

    start = clock();
    for (i = 0; i < N; i++)
        for (j = 0; j < N; j++)
            a[i][j] = b[i][j];
    end = clock();
    seconds = (float)(end - start) / CLOCKS_PER_SEC;
    printf("time taken: %f secs\n", seconds);

    return 0;
}

Вывод:

address of a[32768][32768] = 0x80609080
address of b[    0][    0] = 0x601080
time taken: 18.063229 secs
time taken: 3.079248 secs

Почему копирование столбцов по столбцам занимает почти в 6 раз больше, чем копирование по строке? Я понимаю, что 2D-массив в основном представляет собой массив размера nxn, где A [i] [j] = A [i * n + j], но с использованием простой алгебры я вычислил, что головка машины Тьюринга (в основной памяти) расстояние введите описание изображения здесь в обоих случаях. Здесь nxn - размер массива, а x - расстояние между последним элементом первого массива и первым элементом второго массива.

4b9b3361

Ответ 1

Довольно похоже на это изображение (источник):

введите описание изображения здесь

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

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

Изменить: chqrlie в комментариях, конечно, правильный. Один из важных факторов здесь состоит в том, что только очень немногие из ваших столбцов вписываются в кеш L1 одновременно. Если ваши строки были намного меньше (скажем, общий размер вашего двухмерного массива был всего несколько килобайт), вы можете не увидеть влияние производительности на итерацию за столбец.

Ответ 2

При нормальном рисовании массива в виде прямоугольника адресация элементов массива в памяти линейна: от 0 до одного минус количество доступных байтов (почти на всех машинах).

Иерархии памяти (например, регистры < L1 cache < L2 cache < RAM < swap space на диске) оптимизированы для случая, когда локализуются обращения к памяти: образы, которые являются последовательными по времени касательными адресами, которые находятся близко друг к другу. Они еще более оптимизированы (например, с использованием стратегий предварительной выборки) для последовательного доступа в линейном порядке адресов; например 100101102...

В C прямоугольные массивы расположены в линейном порядке, объединяя все строки (вместо этого используются другие языки, такие как FORTRAN и Common Lisp). Поэтому наиболее эффективным способом чтения или записи массива является выполнение всех столбцов первой строки, а затем переход к остальным, строка за строкой.

Если вы переходите вниз по столбцам, последовательные штрихи разделяются на N байтов, где N - количество байтов в строке: 100, 10100, 20100, 30100... для случая N = 10000 байт. Затем вторая столбец 101, 10101, 20101 и т.д. Это самый худший случай для большинства схем кэширования.

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

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

Ответ 3

Существует 3 основных аспекта, которые влияют на время:

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

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

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

В качестве побочного примечания я бы рекомендовал, чтобы любое временное измерение выполнялось несколько раз и амортизировалось - это устранило проблему № 1.