Почему _mm_stream_ps приводит к отсутствию кэша L1 / LL? - PullRequest
7 голосов
/ 30 января 2012

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

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

void tim(const char *name, void (*func)()) {
    struct timespec t1, t2;
    clock_gettime(CLOCK_REALTIME, &t1);
    func();
    clock_gettime(CLOCK_REALTIME, &t2);
    printf("%s : %f s.\n", name, (t2.tv_sec - t1.tv_sec) + (float) (t2.tv_nsec - t1.tv_nsec) / 1000000000);
}

const int CACHE_LINE = 64;
const int FACTOR = 1024;
float *arr;
int length;

void func1() {
    for(int i = 0; i < length; i++) {
        arr[i] = 5.0f;
    }
}

void func2() {
    for(int i = 0; i < length; i += 4) {
        arr[i] = 5.0f;
        arr[i+1] = 5.0f;
        arr[i+2] = 5.0f;
        arr[i+3] = 5.0f;
    }
}

void func3() {
    __m128 buf = _mm_setr_ps(5.0f, 5.0f, 5.0f, 5.0f);
    for(int i = 0; i < length; i += 4) {
        _mm_stream_ps(&arr[i], buf);
    }
}

void func4() {
    __m128 buf = _mm_setr_ps(5.0f, 5.0f, 5.0f, 5.0f);
    for(int i = 0; i < length; i += 16) {
        _mm_stream_ps(&arr[i], buf);
        _mm_stream_ps(&arr[4], buf);
        _mm_stream_ps(&arr[8], buf);
        _mm_stream_ps(&arr[12], buf);
    }
}

int main() {
    length = CACHE_LINE * FACTOR * FACTOR;

    arr = malloc(length * sizeof(float));
    tim("func1", func1);
    free(arr);

    arr = malloc(length * sizeof(float));
    tim("func2", func2);
    free(arr);

    arr = malloc(length * sizeof(float));
    tim("func3", func3);
    free(arr);

    arr = malloc(length * sizeof(float));
    tim("func4", func4);
    free(arr);

    return 0;
}

Функция 1 - простой подход, функция 2 использует развертывание цикла.Функция 3 использует movntps, который фактически был вставлен в сборку, по крайней мере, когда я проверил на -O0.В функции 4 я попытался выдать несколько инструкций movntps одновременно, чтобы помочь процессору объединить свои записи.Я скомпилировал код с gcc -g -lrt -std=gnu99 -OX -msse4.1 test.c, где X является одним из [0..3].Результаты ... интересно сказать в лучшем случае:

-O0
func1 : 0.407794 s.
func2 : 0.320891 s.
func3 : 0.161100 s.
func4 : 0.401755 s.
-O1
func1 : 0.194339 s.
func2 : 0.182536 s.
func3 : 0.101712 s.
func4 : 0.383367 s.
-O2
func1 : 0.108488 s.
func2 : 0.088826 s.
func3 : 0.101377 s.
func4 : 0.384106 s.
-O3
func1 : 0.078406 s.
func2 : 0.084927 s.
func3 : 0.102301 s.
func4 : 0.383366 s.

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

Итак, возникают вопросы: почему эти (L1 + LL) ошибки по-прежнему происходят, даже если я использую инструкции потоковой передачи NTA?Почему особенно func4 такой медленный ?!Может кто-нибудь объяснить / предположить, что здесь происходит?

Ответы [ 2 ]

8 голосов
/ 31 января 2012
  1. Вероятно, ваш тест измеряет в основном производительность выделения памяти, а не только производительность записи.Ваша ОС может распределять страницы памяти не по malloc, а по первому прикосновению внутри ваших func* функций.ОС может также выполнять некоторые перестановки памяти после выделения большого объема памяти, поэтому любые тесты, выполняемые сразу после выделения памяти, могут быть ненадежными.
  2. В вашем коде aliasing проблема: компилятор не можетГарантируйте, что указатель вашего массива не изменяется в процессе заполнения этого массива, поэтому он должен всегда загружать значение arr из памяти, а не использовать регистр.Это может стоить некоторого снижения производительности.Самый простой способ избежать алиасов - это скопировать arr и length в локальные переменные и использовать только локальные переменные для заполнения массива.Есть много известных советов, чтобы избежать глобальных переменных.Псевдоним является одной из причин.
  3. _mm_stream_ps работает лучше, если массив выровнен на 64 байта.В вашем коде выравнивание не гарантируется (на самом деле malloc выравнивает его по 16 байтам).Эта оптимизация заметна только для коротких массивов.
  4. Рекомендуется звонить _mm_mfence после того, как вы закончили с _mm_stream_ps.Это нужно для правильности, а не для производительности.
2 голосов
/ 30 января 2012

Не должно быть func4 так:

void func4() {
    __m128 buf = _mm_setr_ps(5.0f, 5.0f, 5.0f, 5.0f);
    for(int i = 0; i < length; i += 16) {
        _mm_stream_ps(&arr[i], buf);
        _mm_stream_ps(&arr[i+4], buf);
        _mm_stream_ps(&arr[i+8], buf);
        _mm_stream_ps(&arr[i+12], buf);
    }
}
...