Скорость выполнения программы C

У меня есть одна проблема на моем экзамене по предмету Принципала языка программирования. Я долго думал, но я все еще не понимал проблемы

проблема: Ниже приведена программа C, которая выполняется в среде MSVC++ 6.0 на ПК с конфигурацией ~ CPU Intel 1.8 GHz, Ram 512MB

#define M 10000
#define N 5000
int a[M][N];

void main() {
    int i, j;
    time_t start, stop;

    // Part A
    start = time(0);
    for (i = 0; i < M; i++)
        for (j = 0; j < N; j++)
            a[i][j] = 0;
    stop = time(0);
    printf("%dn", stop - start);

    // Part B
    start = time(0);
    for (j = 0; j < N; j++)
        for (i = 0; i < M; i++)
            a[i][j] = 0;
    stop = time(0);
    printf("%dn", stop - start);
}

объясните, почему часть a выполняется только в 1s, но он принял участие B 8s до конца?

5 ответов


строкам против столбцам.

сначала вспомните, что все многомерные массивы представлены в памяти как continguous блок памяти. Таким образом,многомерный массив A(m, n) может быть представлен в памяти как

a00 a01 a02 ... a0n a10 a11 a12 ... a1n a20 ... АМН

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

a00  a01  a02  ...  a0n  a10  a11  a12  ...  a1n  a20 ... amn

1    2    3         n    n+1  n+2  n+3 ...   2n   2n+1    mn

во втором цикле вы пропускаете в памяти и проходите через массив, пересекая элементы в следующем порядке

a00  a10  a20  ...  am0  a01  a11  a21  ...  am1  a02  ...  amn

или, возможно, более четко,

a00  a01  a02  ...  a10  a11  a12  ...  a20 ... amn
1    m+1  2m+1      2    m+2  2m+2      3       mn

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


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


из - за аппаратной архитектурной оптимизации. Часть A выполняет операции с последовательными адресами памяти, что позволяет аппаратному обеспечению существенно ускорить обработку вычислений. Часть B в основном прыгает в памяти все время, что побеждает много аппаратных оптимизаций.

ключевым понятием для этого конкретного случая является кэш процессора.


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

поэтому, когда вы итерируете через него линейно (с M в качестве переменной внешнего цикла), вы действительно идете линейно через память. Что-то, что кэш CPU обрабатывает очень хорошо.

, когда вы переходите с N во внешнем цикле, то вы всегда делает более или менее случайные скачки в памяти (по крайней мере, для аппаратного обеспечения это выглядит так). Вы получаете доступ к первой ячейке, затем перемещаете M целых чисел дальше и делаете то же самое и т. д. Поскольку ваши страницы в памяти обычно составляют около 4 Кб, это вызывает доступ к другой странице для каждого итерация внутреннего цикла. Таким образом, почти любая стратегия кэширования терпит неудачу, и вы видите значительное замедление.

проблема здесь в том, как Ваш массив заложен в память.

в памяти компьютера обычно выделяются массивы, такие как, что сначала все столбцы первой строки объединяются, затем второй строки и так далее.

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

сейчас идет дальнейшая проблема: современные процессоры имеют кэши. У них есть несколько кэшей, и они имеют так называемые "линии кэша" для кэша первого уровня. Что это значит. Доступ к памяти быстрый, но недостаточно быстрый. Современные процессоры работают намного быстрее. Так у них на кристалле кэш который ускорит. Кроме того, они больше не обращаются к отдельным ячейкам памяти, но они заполняют одну полную строку кэша в одной выборке. Это также для производительности. Но это поведение дает все преимущества операций, которые обрабатывают данные линейно. Когда вы обращаетесь сначала ко всем столбцам в строке, затем к следующей строке и так далее-фактически вы работаете линейно. Когда вы сначала обрабатываете все первые столбцы всех строк, вы "прыгаете" в памяти. Таким образом, вы всегда заставляете новую строку кэша заполняться, может быть обработано всего несколько байтов, тогда строка кэша, возможно, недействительна при следующем прыжке ....

таким образом, column-major-order плох для современных процессоров, так как он не работает линейно.